Browse Source

downloader: Support HTTPClient/Channel and HTTP mount in emscripten

Current pathname on the server is mounted by default
rdb 4 years ago
parent
commit
e7b48d77a7

+ 1 - 1
panda/src/downloader/httpAuthorization.cxx

@@ -16,7 +16,7 @@
 #include "urlSpec.h"
 #include "urlSpec.h"
 #include "string_utils.h"
 #include "string_utils.h"
 
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 
 using std::string;
 using std::string;
 
 

+ 1 - 1
panda/src/downloader/httpAuthorization.h

@@ -20,7 +20,7 @@
 // use any OpenSSL code, because it is a support module for HTTPChannel, which
 // use any OpenSSL code, because it is a support module for HTTPChannel, which
 // *does* use OpenSSL code.
 // *does* use OpenSSL code.
 
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 
 #include "referenceCount.h"
 #include "referenceCount.h"
 #include "httpEnum.h"
 #include "httpEnum.h"

+ 1 - 1
panda/src/downloader/httpBasicAuthorization.cxx

@@ -13,7 +13,7 @@
 
 
 #include "httpBasicAuthorization.h"
 #include "httpBasicAuthorization.h"
 
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 
 using std::string;
 using std::string;
 
 

+ 1 - 1
panda/src/downloader/httpBasicAuthorization.h

@@ -20,7 +20,7 @@
 // use any OpenSSL code, because it is a support module for HTTPChannel, which
 // use any OpenSSL code, because it is a support module for HTTPChannel, which
 // *does* use OpenSSL code.
 // *does* use OpenSSL code.
 
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 
 #include "httpAuthorization.h"
 #include "httpAuthorization.h"
 
 

+ 3 - 0
panda/src/downloader/httpChannel.h

@@ -453,6 +453,9 @@ std::ostream &operator << (std::ostream &out, HTTPChannel::State state);
 
 
 #include "httpChannel.I"
 #include "httpChannel.I"
 
 
+#elif defined(__EMSCRIPTEN__)
+#include "httpChannel_emscripten.h"
+
 #endif  // HAVE_OPENSSL
 #endif  // HAVE_OPENSSL
 
 
 #endif
 #endif

+ 442 - 0
panda/src/downloader/httpChannel_emscripten.I

@@ -0,0 +1,442 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file httpChannel_emscripten.I
+ * @author rdb
+ * @date 2021-02-10
+ */
+
+/**
+ * Returns the HTTPClient object that owns this channel.
+ */
+INLINE HTTPClient *HTTPChannel::
+get_client() const {
+  return _client;
+}
+
+/**
+ * Returns true if the last-requested document was successfully retrieved and
+ * is ready to be read, false otherwise.
+ */
+INLINE bool HTTPChannel::
+is_valid() const {
+  return get_status_code() / 100 == 2;
+}
+
+/**
+ * Returns the URL that was used to retrieve the most recent document:
+ * whatever URL was last passed to get_document() or get_header().  If a
+ * redirect has transparently occurred, this will return the new, redirected
+ * URL (the actual URL at which the document was located).
+ */
+INLINE const URLSpec &HTTPChannel::
+get_url() const {
+  return _document_spec.get_url();
+}
+
+/**
+ * Returns the DocumentSpec associated with the most recent document.  This
+ * includes its actual URL (following redirects) along with the identity tag
+ * and last-modified date, if supplied by the server.
+ *
+ * This structure may be saved and used to retrieve the same version of the
+ * document later, or to conditionally retrieve a newer version if it is
+ * available.
+ */
+INLINE const DocumentSpec &HTTPChannel::
+get_document_spec() const {
+  return _document_spec;
+}
+
+/**
+ * Returns the HTML return code from the document retrieval request.  This
+ * will be in the 200 range if the document is successfully retrieved, or some
+ * other value in the case of an error.
+ *
+ * Some proxy errors during an https-over-proxy request would return the same
+ * status code as a different error that occurred on the host server.  To
+ * differentiate these cases, status codes that are returned by the proxy
+ * during the CONNECT phase (except code 407) are incremented by 1000.
+ */
+INLINE int HTTPChannel::
+get_status_code() const {
+  return _status_entry._status_code;
+}
+
+/**
+ * Returns the string as returned by the server describing the status code for
+ * humans.  This may or may not be meaningful.
+ */
+INLINE std::string HTTPChannel::
+get_status_string() const {
+  return _status_entry._status_string;
+}
+
+/**
+ * If the document failed to connect because of a 401 (Authorization
+ * required), this method will return the "realm" returned by the server in
+ * which the requested document must be authenticated.  This string may be
+ * presented to the user to request an associated username and password (which
+ * then should be stored in HTTPClient::set_username()).
+ */
+INLINE const std::string &HTTPChannel::
+get_www_realm() const {
+  return _www_realm;
+}
+
+/**
+ * Specifies the Content-Type header, useful for applications that require
+ * different types of content, such as JSON.
+ */
+INLINE void HTTPChannel::
+set_content_type(std::string content_type) {
+  _content_type = content_type;
+}
+
+/**
+ * Returns the value of the Content-Type header.
+ */
+INLINE std::string HTTPChannel::
+get_content_type() const {
+  return _content_type;
+}
+
+/**
+ * This may be called immediately after a call to get_document() or some
+ * related function to specify the expected size of the document we are
+ * retrieving, if we happen to know.  This is used as the return value to
+ * get_file_size() only in the case that the server does not tell us the
+ * actual file size.
+ */
+INLINE void HTTPChannel::
+set_expected_file_size(size_t file_size) {
+  _expected_file_size = file_size;
+  _got_expected_file_size = true;
+}
+
+/**
+ * Returns true if the size of the file we are currently retrieving was told
+ * us by the server and thus is reliably known, or false if the size reported
+ * by get_file_size() represents an educated guess (possibly as set by
+ * set_expected_file_size(), or as inferred from a chunked transfer encoding
+ * in progress).
+ */
+INLINE bool HTTPChannel::
+is_file_size_known() const {
+  return _got_file_size;
+}
+
+/**
+ * Returns the first byte of the file requested by the request.  This will
+ * normally be 0 to indicate that the file is being requested from the
+ * beginning, but if the file was requested via a get_subdocument() call, this
+ * will contain the first_byte parameter from that call.
+ */
+INLINE size_t HTTPChannel::
+get_first_byte_requested() const {
+  return _first_byte_requested;
+}
+
+/**
+ * Returns the last byte of the file requested by the request.  This will
+ * normally be 0 to indicate that the file is being requested to its last
+ * byte, but if the file was requested via a get_subdocument() call, this will
+ * contain the last_byte parameter from that call.
+ */
+INLINE size_t HTTPChannel::
+get_last_byte_requested() const {
+  return _last_byte_requested;
+}
+
+/**
+ * Returns the first byte of the file (that will be) delivered by the server
+ * in response to the current request.  Normally, this is the same as
+ * get_first_byte_requested(), but some servers will ignore a subdocument
+ * request and always return the whole file, in which case this value will be
+ * 0, regardless of what was requested to get_subdocument().
+ */
+INLINE size_t HTTPChannel::
+get_first_byte_delivered() const {
+  return _first_byte_delivered;
+}
+
+/**
+ * Returns the last byte of the file (that will be) delivered by the server in
+ * response to the current request.  Normally, this is the same as
+ * get_last_byte_requested(), but some servers will ignore a subdocument
+ * request and always return the whole file, in which case this value will be
+ * 0, regardless of what was requested to get_subdocument().
+ */
+INLINE size_t HTTPChannel::
+get_last_byte_delivered() const {
+  return _last_byte_delivered;
+}
+
+/**
+ * Stops whatever file transaction is currently in progress, closes the
+ * connection, and resets to begin anew.  You shouldn't ever need to call
+ * this, since the channel should be able to reset itself cleanly between
+ * requests, but it is provided in case you are an especially nervous type.
+ *
+ * Don't call this after every request unless you set
+ * set_persistent_connection() to false, since calling reset() rudely closes
+ * the connection regardless of whether we have told the server we intend to
+ * keep it open or not.
+ */
+INLINE void HTTPChannel::
+reset() {
+  reset_for_new_request();
+  _status_list.clear();
+}
+
+/**
+ * Preserves the previous status code (presumably a failure) from the previous
+ * connection attempt.  If the subsequent connection attempt also fails, the
+ * returned status code will be the better of the previous code and the
+ * current code.
+ *
+ * This can be called to daisy-chain subsequent attempts to download the same
+ * document from different servers.  After all servers have been attempted,
+ * the final status code will reflect the attempt that most nearly succeeded.
+ */
+INLINE void HTTPChannel::
+preserve_status() {
+  _status_list.push_back(_status_entry);
+}
+
+/**
+ * Resets the extra headers that were previously added via calls to
+ * send_extra_header().
+ */
+INLINE void HTTPChannel::
+clear_extra_headers() {
+  _send_extra_headers.clear();
+}
+
+/**
+ * Specifies an additional key: value pair that is added into the header sent
+ * to the server with the next request.  This is passed along with no
+ * interpretation by the HTTPChannel code.  You may call this repeatedly to
+ * append multiple headers.
+ *
+ * This is persistent for one request only; it must be set again for each new
+ * request.
+ */
+INLINE void HTTPChannel::
+send_extra_header(const std::string &key, const std::string &value) {
+  _send_extra_headers.push_back(std::make_pair(key, value));
+}
+
+/**
+ * Opens the named document for reading, if available.  Returns true if
+ * successful, false otherwise.
+ */
+INLINE bool HTTPChannel::
+get_document(const DocumentSpec &url) {
+  if (!begin_request(HTTPEnum::M_get, url, std::string(), false, 0, 0)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Retrieves only the specified byte range of the indicated document.  If
+ * last_byte is 0, it stands for the last byte of the document.  When a
+ * subdocument is requested, get_file_size() and get_bytes_downloaded() will
+ * report the number of bytes of the subdocument, not of the complete
+ * document.
+ */
+INLINE bool HTTPChannel::
+get_subdocument(const DocumentSpec &url, size_t first_byte, size_t last_byte) {
+  if (!begin_request(HTTPEnum::M_get, url, std::string(), false, first_byte, last_byte)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Like get_document(), except only the header associated with the document is
+ * retrieved.  This may be used to test for existence of the document; it
+ * might also return the size of the document (if the server gives us this
+ * information).
+ */
+INLINE bool HTTPChannel::
+get_header(const DocumentSpec &url) {
+  if (!begin_request(HTTPEnum::M_head, url, std::string(), false, 0, 0)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Posts form data to a particular URL and retrieves the response.
+ */
+INLINE bool HTTPChannel::
+post_form(const DocumentSpec &url, const std::string &body) {
+  if (!begin_request(HTTPEnum::M_post, url, body, false, 0, 0)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Uploads the indicated body to the server to replace the indicated URL, if
+ * the server allows this.
+ */
+INLINE bool HTTPChannel::
+put_document(const DocumentSpec &url, const std::string &body) {
+  if (!begin_request(HTTPEnum::M_put, url, body, false, 0, 0)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Requests the server to remove the indicated URL.
+ */
+INLINE bool HTTPChannel::
+delete_document(const DocumentSpec &url) {
+  if (!begin_request(HTTPEnum::M_delete, url, std::string(), false, 0, 0)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Sends an OPTIONS message to the server, which should query the available
+ * options, possibly in relation to a specified URL.
+ */
+INLINE bool HTTPChannel::
+get_options(const DocumentSpec &url) {
+  if (!begin_request(HTTPEnum::M_options, url, std::string(), false, 0, 0)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Begins a non-blocking request to retrieve a given document.  This method
+ * will return immediately, even before a connection to the server has
+ * necessarily been established; you must then call run() from time to time
+ * until the return value of run() is false.  Then you may check is_valid()
+ * and get_status_code() to determine the status of your request.
+ *
+ * If a previous request had been pending, that request is discarded.
+ */
+INLINE void HTTPChannel::
+begin_get_document(const DocumentSpec &url) {
+  begin_request(HTTPEnum::M_get, url, std::string(), true, 0, 0);
+}
+
+/**
+ * Begins a non-blocking request to retrieve only the specified byte range of
+ * the indicated document.  If last_byte is 0, it stands for the last byte of
+ * the document.  When a subdocument is requested, get_file_size() and
+ * get_bytes_downloaded() will report the number of bytes of the subdocument,
+ * not of the complete document.
+ */
+INLINE void HTTPChannel::
+begin_get_subdocument(const DocumentSpec &url, size_t first_byte,
+                      size_t last_byte) {
+  begin_request(HTTPEnum::M_get, url, std::string(), true, first_byte, last_byte);
+}
+
+/**
+ * Begins a non-blocking request to retrieve a given header.  See
+ * begin_get_document() and get_header().
+ */
+INLINE void HTTPChannel::
+begin_get_header(const DocumentSpec &url) {
+  begin_request(HTTPEnum::M_head, url, std::string(), true, 0, 0);
+}
+
+/**
+ * Posts form data to a particular URL and retrieves the response, all using
+ * non-blocking I/O.  See begin_get_document() and post_form().
+ *
+ * It is important to note that you *must* call run() repeatedly after calling
+ * this method until run() returns false, and you may not call any other
+ * document posting or retrieving methods using the HTTPChannel object in the
+ * interim, or your form data may not get posted.
+ */
+INLINE void HTTPChannel::
+begin_post_form(const DocumentSpec &url, const std::string &body) {
+  begin_request(HTTPEnum::M_post, url, body, true, 0, 0);
+}
+
+/**
+ * Returns the number of bytes downloaded during the last (or current)
+ * download_to_file() or download_to_ram operation().  This can be used in
+ * conjunction with get_file_size() to report the percent complete (but be
+ * careful, since get_file_size() may return 0 if the server has not told us
+ * the size of the file).
+ */
+INLINE size_t HTTPChannel::
+get_bytes_downloaded() const {
+  return _bytes_downloaded;
+}
+
+/**
+ * When download throttling is in effect (set_download_throttle() has been set
+ * to true) and non-blocking I/O methods (like begin_get_document()) are used,
+ * this returns the number of bytes "requested" from the server so far: that
+ * is, the theoretical maximum value for get_bytes_downloaded(), if the server
+ * has been keeping up with our demand.
+ *
+ * If this number is less than get_bytes_downloaded(), then the server has not
+ * been supplying bytes fast enough to meet our own download throttle rate.
+ *
+ * When download throttling is not in effect, or when the blocking I/O methods
+ * (like get_document(), etc.) are used, this returns 0.
+ */
+INLINE size_t HTTPChannel::
+get_bytes_requested() const {
+  return _bytes_requested;
+}
+
+/**
+ * Returns true when a download_to() or download_to_ram() has executed and the
+ * file has been fully downloaded.  If this still returns false after
+ * processing has completed, there was an error in transmission.
+ *
+ * Note that simply testing is_download_complete() does not prove that the
+ * requested document was successfully retrieved--you might have just
+ * downloaded the "404 not found" stub (for instance) that a server would
+ * provide in response to some error condition.  You should also check
+ * is_valid() to prove that the file you expected has been successfully
+ * retrieved.
+ */
+INLINE bool HTTPChannel::
+is_download_complete() const {
+  if (_download_dest != DD_none) {
+    return get_state() == S_done;
+  }
+  return false;
+}
+
+/**
+ *
+ */
+INLINE HTTPChannel::StatusEntry::
+StatusEntry() {
+  _status_code = SC_incomplete;
+}

+ 790 - 0
panda/src/downloader/httpChannel_emscripten.cxx

@@ -0,0 +1,790 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file httpChannel_emscripten.cxx
+ * @author rdb
+ * @date 2021-02-10
+ */
+
+#include "httpChannel_emscripten.h"
+
+#ifdef __EMSCRIPTEN__
+
+#include "string_utils.h"
+
+#include <emscripten/em_asm.h>
+
+#define _NOTIFY_HTTP_CHANNEL_ID   "[" << this << "] "
+
+#ifndef CPPPARSER
+extern "C" char *EMSCRIPTEN_KEEPALIVE
+_extend_string(std::string *str, int length) {
+  size_t offset = str->size();
+  str->resize(offset + (size_t)length);
+  return (char *)str->data() + offset;
+}
+
+extern "C" void EMSCRIPTEN_KEEPALIVE
+_write_stream(std::ostream *strm, const char *data, int length) {
+  strm->write(data, (size_t)length);
+}
+
+extern "C" bool EMSCRIPTEN_KEEPALIVE
+_http_channel_run(HTTPChannel *channel) {
+  return channel->run();
+}
+#endif
+
+TypeHandle HTTPChannel::_type_handle;
+
+/**
+ *
+ */
+HTTPChannel::
+HTTPChannel(HTTPClient *client) :
+  _client(client)
+{
+  if (downloader_cat.is_debug()) {
+    downloader_cat.debug()
+      << _NOTIFY_HTTP_CHANNEL_ID
+    << "created.\n";
+  }
+
+  EM_ASM({
+    if (!window._httpChannels) {
+      window._httpChannels = {};
+    }
+    var xhr = new XMLHttpRequest();
+    window._httpChannels[$0] = xhr;
+  }, this);
+
+  // _nonblocking is true if the XHR is actually in non-blocking mode.
+  _nonblocking = false;
+
+  // _wanted_nonblocking is true if the user specifically requested one of the
+  // non-blocking interfaces.  It is false if the XHR is only incidentally
+  // non-blocking (for instance, because ASYNCIFY is on).
+  _wanted_nonblocking = false;
+
+  _first_byte_requested = 0;
+  _last_byte_requested = 0;
+  _first_byte_delivered = 0;
+  _last_byte_delivered = 0;
+  _expected_file_size = 0;
+  _file_size = 0;
+  _transfer_file_size = 0;
+  _got_expected_file_size = false;
+  _got_file_size = false;
+  _got_transfer_file_size = false;
+  _bytes_downloaded = 0;
+  _bytes_requested = 0;
+  _status_entry = StatusEntry();
+  _content_type = "application/x-www-form-urlencoded";
+  _download_dest = DD_none;
+  _download_to_ramfile = nullptr;
+  _download_to_stream = nullptr;
+}
+
+/**
+ *
+ */
+HTTPChannel::
+~HTTPChannel() {
+  EM_ASM({
+    var xhr = window._httpChannels[$0];
+    if (xhr) {
+      xhr.onprogress = null;
+      xhr.onreadystatechange = null;
+      delete window._httpChannels[$0];
+    }
+  }, this);
+
+  if (downloader_cat.is_debug()) {
+    downloader_cat.debug()
+      << _NOTIFY_HTTP_CHANNEL_ID
+    << "destroyed.\n";
+  }
+
+  reset_download_to();
+}
+
+/**
+ * Returns the HTML header value associated with the indicated key, or empty
+ * string if the key was not defined in the message returned by the server.
+ */
+std::string HTTPChannel::
+get_header_value(const std::string &key) const {
+  Headers::const_iterator hi = _headers.find(downcase(key));
+  if (hi != _headers.end()) {
+    return (*hi).second;
+  }
+  return std::string();
+}
+
+/**
+ * Returns the size of the file, if it is known.  Returns the value set by
+ * set_expected_file_size() if the file size is not known, or 0 if this value
+ * was not set.
+ *
+ * If the file is dynamically generated, the size may not be available until a
+ * read has started (e.g.  open_read_body() has been called); and even then it
+ * may increase as more of the file is read due to the nature of HTTP/1.1
+ * requests which can change their minds midstream about how much data they're
+ * sending you.
+ */
+std::streamsize HTTPChannel::
+get_file_size() const {
+  if (_got_file_size) {
+    return _file_size;
+  } else if (_got_transfer_file_size) {
+    return _transfer_file_size;
+  } else if (_got_expected_file_size) {
+    return _expected_file_size;
+  } else {
+    return 0;
+  }
+}
+
+/**
+ * Outputs a list of all headers defined by the server to the indicated output
+ * stream.
+ */
+void HTTPChannel::
+write_headers(std::ostream &out) const {
+  Headers::const_iterator hi;
+  for (hi = _headers.begin(); hi != _headers.end(); ++hi) {
+    out << (*hi).first << ": " << (*hi).second << "\n";
+  }
+}
+
+/**
+ * This must be called from time to time when non-blocking I/O is in use.  It
+ * checks for data coming in on the socket and writes data out to the socket
+ * when possible, and does whatever processing is required towards completing
+ * the current task.
+ *
+ * The return value is true if the task is still pending (and run() will need
+ * to be called again in the future), or false if the current task is
+ * complete.
+ */
+bool HTTPChannel::
+run() {
+  if (downloader_cat.is_spam()) {
+    downloader_cat.spam()
+      << _NOTIFY_HTTP_CHANNEL_ID
+      << "run().\n";
+  }
+
+  State state = get_state();
+  switch (state) {
+  case S_unsent:
+    // Invalid.
+    return false;
+
+  case S_opened:
+    if (!run_send()) {
+      return false;
+    }
+    break;
+
+  case S_headers_received:
+    if (!run_headers_received()) {
+      return false;
+    }
+    break;
+
+  case S_loading:
+    if (_download_dest != DD_none) {
+      return false;
+    }
+    break;
+
+  case S_done:
+    close_download_stream();
+    return false;
+  }
+
+  // If we get here, we must be running in non-blocking mode.
+  if (!_wanted_nonblocking && emscripten_has_asyncify()) {
+    // But we are pretending to be in blocking mode, so we must yield until the
+    // state changes.
+    while (get_state() != state) {
+      emscripten_sleep(0);
+    }
+  }
+
+  return true;
+}
+
+/**
+ * Returns the current readyState of the XMLHttpRequest object.
+ */
+HTTPChannel::State HTTPChannel::
+get_state() const {
+  return (State)EM_ASM_INT(return window._httpChannels[$0].readyState, this);
+}
+
+/**
+ * Calls XHR.send().
+ */
+bool HTTPChannel::
+run_send() {
+  for (const ExtraHeader &header : _send_extra_headers) {
+    EM_ASM({
+      var xhr = window._httpChannels[$0];
+      xhr.setRequestHeader(UTF8ToString($1), UTF8ToString($2));
+    }, this, header.first.c_str(), header.second.c_str());
+  }
+
+  if (_method == HTTPEnum::M_get || _method == HTTPEnum::M_head) {
+    // No body is sent with GET / HEAD requests.
+    EM_ASM({
+      var xhr = window._httpChannels[$0];
+      xhr.send(null);
+    }, this);
+  }
+  else {
+    EM_ASM({
+      var xhr = window._httpChannels[$0];
+      xhr.setRequestHeader("Content-Type", UTF8ToString($2));
+      xhr.send(UTF8ToString($1));
+    }, this, _body.c_str(), _content_type.c_str());
+  }
+
+  return true;
+}
+
+/**
+ * Called when the headers have been received.
+ */
+bool HTTPChannel::
+run_headers_received() {
+  char status_string[512];
+  char *header_str = nullptr;
+  status_string[0] = 0;
+
+  // Fetch the status code, text and response headers from JavaScript.
+  int status_code = EM_ASM_INT({
+    var xhr = window._httpChannels[$0];
+    stringToUTF8(xhr.statusText, $1, 512);
+
+    var headers = xhr.getAllResponseHeaders();
+    var len = lengthBytesUTF8(headers) + 1;
+    var buffer = _malloc(len);
+    stringToUTF8(headers, buffer, len);
+    setValue($2, buffer, '*');
+
+    return xhr.status;
+  }, this, status_string, &header_str);
+  _status_entry._status_code = status_code;
+
+  // Parse the response header string.
+  char *ptr = header_str;
+  char *delim = strstr(ptr, ": ");
+  while (delim != nullptr) {
+    std::string key(ptr, delim);
+    ptr = delim + 2;
+
+    std::string value;
+    delim = strstr(ptr, "\r\n");
+    if (delim != nullptr) {
+      value.assign(ptr, delim);
+      ptr = delim + 2;
+      delim = strstr(ptr, ": ");
+    }
+    else {
+      // The XHR spec prescribes that there is always another CRLF
+      // after the last header, but we handle this case anyway.
+      value.assign(ptr);
+    }
+
+    _headers[std::move(key)] = std::move(value);
+  }
+  free(header_str);
+
+  // Look for key properties in the header fields.
+  if (status_code == 206) {
+    std::string content_range = get_header_value("Content-Range");
+    if (content_range.empty()) {
+      downloader_cat.warning()
+        << _NOTIFY_HTTP_CHANNEL_ID
+        << "Got 206 response without Content-Range header!\n";
+      _status_entry._status_code = SC_invalid_http;
+      return false;
+
+    } else {
+      if (!parse_content_range(content_range)) {
+        downloader_cat.warning()
+          << _NOTIFY_HTTP_CHANNEL_ID
+          << "Couldn't parse Content-Range: " << content_range << "\n";
+        _status_entry._status_code = SC_invalid_http;
+        return false;
+      }
+    }
+
+  } else {
+    _first_byte_delivered = 0;
+    _last_byte_delivered = 0;
+  }
+  if (downloader_cat.is_debug()) {
+    if (_first_byte_requested != 0 || _last_byte_requested != 0 ||
+        _first_byte_delivered != 0 || _last_byte_delivered != 0) {
+      downloader_cat.debug()
+        << _NOTIFY_HTTP_CHANNEL_ID
+        << "Requested byte range " << _first_byte_requested
+        << " to " << _last_byte_delivered
+        << "; server delivers range " << _first_byte_delivered
+        << " to " << _last_byte_delivered
+        << "\n";
+    }
+  }
+
+  // Set the _document_spec to reflect what we just retrieved.
+  _document_spec = DocumentSpec(_request.get_url());
+  std::string tag = get_header_value("ETag");
+  if (!tag.empty()) {
+    _document_spec.set_tag(HTTPEntityTag(tag));
+  }
+  std::string date = get_header_value("Last-Modified");
+  if (!date.empty()) {
+    _document_spec.set_date(HTTPDate(date));
+  }
+
+  // In case we've got a download in effect, now we know what the first byte
+  // of the subdocument request will be, so we can open the file and position
+  // it.
+  if (status_code / 100 == 1 || status_code == 204 || status_code == 304) {
+    // Never mind on the download.
+    reset_download_to();
+  }
+
+  if (!open_download_file()) {
+    return false;
+  }
+
+  if (_download_dest == DD_ram) {
+    std::string *dest = &_download_to_ramfile->_data;
+    EM_ASM({
+      var xhr = window._httpChannels[$0];
+      var loaded = 0;
+      xhr.onprogress = function (ev) {
+        var chunk = this.responseText.slice(loaded, ev.loaded);
+        var ptr = __extend_string($1, chunk.length);
+        writeAsciiToMemory(chunk, ptr, true);
+        loaded = ev.loaded;
+      };
+    }, this, dest);
+  }
+  else if (_download_dest == DD_stream) {
+    std::ostream *dest = _download_to_stream;
+    char buffer[4096];
+    EM_ASM({
+      var xhr = window._httpChannels[$0];
+      var loaded = 0;
+      xhr.onprogress = function (ev) {
+        while (loaded < ev.loaded) {
+          var size = Math.min(ev.loaded - read, 4096);
+          writeAsciiToMemory(this.responseText.substr(read, size), $2, true);
+          __write_stream($1, $2, size);
+          loaded += size;
+        }
+      };
+    }, this, dest, buffer);
+  }
+
+  _got_expected_file_size = false;
+  _got_file_size = false;
+  _got_transfer_file_size = false;
+
+  std::string content_length = get_header_value("Content-Length");
+  if (!content_length.empty()) {
+    _file_size = atoi(content_length.c_str());
+    _got_file_size = true;
+  }
+  else if (status_code == 206) {
+    // Well, we didn't get a content-length from the server, but we can infer
+    // the number of bytes based on the range we're given.
+    _file_size = _last_byte_delivered - _first_byte_delivered + 1;
+    _got_file_size = true;
+  }
+
+  // Reset these for the next request.
+  clear_extra_headers();
+
+  return (_download_dest != DD_none);
+}
+
+/**
+ * Begins a new document request to the server, throwing away whatever request
+ * was currently pending if necessary.
+ */
+bool HTTPChannel::
+begin_request(HTTPEnum::Method method, const DocumentSpec &url,
+              const std::string &body, bool nonblocking,
+              size_t first_byte, size_t last_byte) {
+
+  downloader_cat.info()
+    << _NOTIFY_HTTP_CHANNEL_ID
+    << "begin " << method << " " << url << "\n";
+
+  reset_for_new_request();
+
+  _wanted_nonblocking = nonblocking;
+  _nonblocking = nonblocking || emscripten_has_asyncify();
+
+  _request = url;
+  _document_spec = DocumentSpec();
+  _method = method;
+  _body = body;
+
+  _first_byte_requested = first_byte;
+  _last_byte_requested = last_byte;
+
+  bool result = (bool)EM_ASM_INT(({
+    var methods = ["OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT"];
+    var xhr = window._httpChannels[$0];
+    try {
+      xhr.open(methods[$1], UTF8ToString($2), !!$3);
+      xhr.withCredentials = true;
+      xhr.overrideMimeType("text/plain; charset=x-user-defined");
+      if ($4 != 0 || $5 != 0) {
+        xhr.setRequestHeader("Range", "bytes=" + $4 + "-" + ($5 || ""));
+      }
+      xhr.onprogress = null;
+      xhr.onreadystatechange = null;
+      return 1;
+    }
+    catch (ex) {
+      (console.error || console.log)(ex);
+      return 0;
+    }
+  }), this, method, _request.get_url().c_str(), (int)_nonblocking, (int)first_byte, (int)last_byte);
+
+  if (!result) {
+    return false;
+  }
+
+  if (_wanted_nonblocking) {
+    // Call run() automatically when the state changes.
+    EM_ASM({
+      var xhr = window._httpChannels[$0];
+      xhr.onreadystatechange = function () {
+        var xhr = window._httpChannels[$0];
+        if (!xhr || !__http_channel_run($0)) {
+          xhr.onreadystatechange = null;
+        }
+      };
+    }, this);
+  }
+  else {
+    return run_send() && run_headers_received();
+  }
+
+  return true;
+}
+
+/**
+ * Resets the internal state variables in preparation for beginning a new
+ * request.
+ */
+void HTTPChannel::
+reset_for_new_request() {
+  if (downloader_cat.is_spam()) {
+    downloader_cat.spam()
+      << _NOTIFY_HTTP_CHANNEL_ID
+      << "reset_for_new_request.\n";
+  }
+
+  EM_ASM({
+    var xhr = window._httpChannels[$0];
+    xhr.onprogress = null;
+    xhr.onreadystatechange = null;
+    if (xhr.readyState !== 0) {
+      try {
+        xhr.abort();
+      }
+      catch (ex) {
+      }
+    }
+  }, this);
+
+  reset_download_to();
+
+  _status_entry = StatusEntry();
+
+  _bytes_downloaded = 0;
+  _bytes_requested = 0;
+}
+
+/**
+ * If a download has been requested, opens the file on disk (or prepares the
+ * RamFile or stream) and seeks within it to the appropriate
+ * _first_byte_delivered position, so that downloaded bytes will be written to
+ * the appropriate point within the file.  Returns true if the starting
+ * position is valid, false otherwise (in which case the state is set to
+ * S_failure).
+ */
+bool HTTPChannel::
+open_download_file() {
+  _subdocument_resumes = (_subdocument_resumes && _first_byte_delivered != 0);
+
+  if (_subdocument_resumes) {
+    if (_download_dest == DD_ram) {
+      if (_first_byte_delivered > _download_to_ramfile->_data.length()) {
+        downloader_cat.info()
+          << _NOTIFY_HTTP_CHANNEL_ID
+          << "Invalid starting position of byte " << _first_byte_delivered
+          << " within Ramfile (which has "
+          << _download_to_ramfile->_data.length() << " bytes)\n";
+        close_download_stream();
+        _status_entry._status_code = SC_download_invalid_range;
+        return false;
+      }
+
+      if (_first_byte_delivered == 0) {
+        _download_to_ramfile->_data = string();
+      }
+      else {
+        _download_to_ramfile->_data =
+          _download_to_ramfile->_data.substr(0, _first_byte_delivered);
+      }
+    }
+    else if (_download_dest == DD_stream) {
+      _download_to_stream->seekp(_first_byte_delivered);
+    }
+  }
+  else {
+    // If _subdocument_resumes is false, we should be sure to reset to the
+    // beginning of the file, regardless of the value of
+    // _first_byte_delivered.
+    if (_download_dest == DD_file || _download_dest == DD_stream) {
+      _download_to_stream->seekp(0);
+    }
+    else if (_download_dest == DD_ram) {
+      _download_to_ramfile->_data = string();
+    }
+  }
+
+  return true;
+}
+
+/**
+ * Interprets the "Content-Range" header in the reply, and fills in
+ * _first_byte_delivered and _last_byte_delivered appropriately if the header
+ * response can be understood.
+ */
+bool HTTPChannel::
+parse_content_range(const std::string &content_range) {
+  // First, get the units indication.
+  size_t p = 0;
+  while (p < content_range.length() && !isspace(content_range[p])) {
+    p++;
+  }
+
+  std::string units = content_range.substr(0, p);
+  while (p < content_range.length() && isspace(content_range[p])) {
+    p++;
+  }
+
+  if (units == "bytes") {
+    const char *c_str = content_range.c_str();
+    char *endptr;
+    if (p < content_range.length() && isdigit(content_range[p])) {
+      long first_byte = strtol(c_str + p, &endptr, 10);
+      p = endptr - c_str;
+      if (p < content_range.length() && content_range[p] == '-') {
+        p++;
+        if (p < content_range.length() && isdigit(content_range[p])) {
+          long last_byte = strtol(c_str + p, &endptr, 10);
+          p = endptr - c_str;
+
+          if (last_byte >= first_byte) {
+            _first_byte_delivered = first_byte;
+            _last_byte_delivered = last_byte;
+            return true;
+          }
+        }
+      }
+    }
+  }
+
+  // Invalid or unhandled response.
+  return false;
+}
+
+/**
+ * Resets the indication of how the document will be downloaded.  This must be
+ * re-specified after each get_document() (or related) call.
+ */
+void HTTPChannel::
+reset_download_to() {
+  close_download_stream();
+  _download_dest = DD_none;
+}
+
+/**
+ * Ensures the file opened for receiving the download has been correctly
+ * closed.
+ */
+void HTTPChannel::
+close_download_stream() {
+  if (_download_to_stream != nullptr) {
+    _download_to_stream->flush();
+    if (_download_dest == DD_file) {
+      VirtualFileSystem::close_write_file(_download_to_stream);
+    }
+  }
+  _download_to_ramfile = nullptr;
+  _download_to_stream = nullptr;
+}
+
+/**
+ * Specifies a Ramfile object to download the resulting document to.  This
+ * should be called immediately after get_document() or begin_get_document()
+ * or related functions.
+ *
+ * In the case of the blocking I/O methods like get_document(), this function
+ * will download the entire document to the Ramfile and return true if it was
+ * successfully downloaded, false otherwise.
+ *
+ * In the case of non-blocking I/O methods like begin_get_document(), this
+ * function simply indicates an intention to download to the indicated
+ * Ramfile.  It returns true if the file can be opened for writing, false
+ * otherwise, but the contents will not be completely downloaded until run()
+ * has returned false.  At this time, it is possible that a communications
+ * error will have left a partial file, so is_download_complete() may be
+ * called to test this.
+ *
+ * If subdocument_resumes is true and the document in question was previously
+ * requested as a subdocument (i.e.  get_subdocument() with a first_byte value
+ * greater than zero), this will automatically seek to the appropriate byte
+ * within the Ramfile for writing the output.  In this case, the Ramfile must
+ * already have at least first_byte bytes in it.
+ */
+bool HTTPChannel::
+download_to_ram(Ramfile *ramfile, bool subdocument_resumes) {
+  State state = get_state();
+  nassertr(state != S_unsent, false);
+  nassertr(ramfile != nullptr, false);
+
+  reset_download_to();
+  ramfile->_pos = 0;
+  _download_dest = DD_ram;
+  _download_to_ramfile = ramfile;
+  _subdocument_resumes = (subdocument_resumes && _first_byte_delivered != 0);
+
+  if (state != S_done && _wanted_nonblocking) {
+    // In nonblocking mode, we just kick off the request.
+    return state != S_opened || run_send();
+  }
+
+  // In normal, blocking mode, go ahead and do the download.
+  if (!open_download_file()) {
+    reset_download_to();
+    return false;
+  }
+
+  if (state != S_done) {
+    while (run()) {
+    }
+  }
+
+  // Copy the entire response text.
+  int bytes_read = EM_ASM_INT({
+    var xhr = window._httpChannels[$0];
+    var state = xhr.readyState;
+    var body = xhr.responseText;
+    var ptr = __extend_string($1, body.length);
+    writeAsciiToMemory(body, ptr, true);
+    return state;
+  }, this, &ramfile->_data);
+
+  _bytes_downloaded = bytes_read;
+
+  close_download_stream();
+
+  return is_valid();
+}
+
+/**
+ * Specifies the name of an ostream to download the resulting document to.
+ * This should be called immediately after get_document() or
+ * begin_get_document() or related functions.
+ *
+ * In the case of the blocking I/O methods like get_document(), this function
+ * will download the entire document to the file and return true if it was
+ * successfully downloaded, false otherwise.
+ *
+ * In the case of non-blocking I/O methods like begin_get_document(), this
+ * function simply indicates an intention to download to the indicated file.
+ * It returns true if the file can be opened for writing, false otherwise, but
+ * the contents will not be completely downloaded until run() has returned
+ * false.  At this time, it is possible that a communications error will have
+ * left a partial file, so is_download_complete() may be called to test this.
+ *
+ * If subdocument_resumes is true and the document in question was previously
+ * requested as a subdocument (i.e.  get_subdocument() with a first_byte value
+ * greater than zero), this will automatically seek to the appropriate byte
+ * within the file for writing the output.  In this case, the file must
+ * already exist and must have at least first_byte bytes in it.  If
+ * subdocument_resumes is false, a subdocument will always be downloaded
+ * beginning at the first byte of the file.
+ */
+bool HTTPChannel::
+download_to_stream(std::ostream *strm, bool subdocument_resumes) {
+  State state = get_state();
+  nassertr(state != S_unsent, false);
+
+  reset_download_to();
+  _download_dest = DD_stream;
+  _download_to_stream = strm;
+  _download_to_stream->clear();
+  _subdocument_resumes = subdocument_resumes;
+
+  if (state != S_done && _wanted_nonblocking) {
+    // In nonblocking mode, we just kick off the request.
+    return state != S_opened || run_send();
+  }
+
+  // In normal, blocking mode, go ahead and do the download.
+  if (!open_download_file()) {
+    reset_download_to();
+    return false;
+  }
+
+  if (state != S_done) {
+    while (run()) {
+    }
+  }
+
+  // Copy the entire response text.
+  char buffer[4096];
+  int bytes_read = EM_ASM_INT({
+    var xhr = window._httpChannels[$0];
+    var state = xhr.readyState;
+    var body = xhr.responseText;
+    var read = 0;
+    while (read < body.length) {
+      var size = Math.min(body.length - read, 4096);
+      for (var dest = $2; dest < $2 + size; ++dest) {
+        HEAP8[(dest>>0)] = body.charCodeAt(read++) & 0xff;
+      }
+      __write_stream($1, $2, size);
+    }
+    return read;
+  }, this, strm, buffer);
+
+  strm->flush();
+  _bytes_downloaded = bytes_read;
+
+  close_download_stream();
+
+  return is_valid();
+}
+
+#endif  // __EMSCRIPTEN__

+ 241 - 0
panda/src/downloader/httpChannel_emscripten.h

@@ -0,0 +1,241 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file httpChannel_emscripten.h
+ * @author rdb
+ * @date 2021-02-10
+ */
+
+#ifndef HTTPCHANNEL_EMSCRIPTEN_H
+#define HTTPCHANNEL_EMSCRIPTEN_H
+
+#include "pandabase.h"
+
+#ifdef __EMSCRIPTEN__
+
+#include "config_downloader.h"
+#include "documentSpec.h"
+#include "httpEnum.h"
+#include "pmap.h"
+#include "pointerTo.h"
+#include "pvector.h"
+#include "ramfile.h"
+#include "typedReferenceCount.h"
+#include "urlSpec.h"
+
+class HTTPClient;
+
+/**
+ * This is a reduced implementation of HTTPChannel used on the web, which uses
+ * XMLHttpRequest instead of OpenSSL.  It offers fewer features.
+ */
+class EXPCL_PANDA_DOWNLOADER HTTPChannel : public TypedReferenceCount {
+private:
+  HTTPChannel(HTTPClient *client);
+
+public:
+  virtual ~HTTPChannel();
+
+PUBLISHED:
+  // get_status_code() will either return an HTTP-style status code >= 100
+  // (e.g.  404), or one of the following values.  In general, these are
+  // ordered from less-successful to more-successful.
+  enum StatusCode {
+    SC_incomplete = 0,
+    SC_internal_error,
+    SC_no_connection,
+    SC_timeout,
+    SC_lost_connection,
+    SC_non_http_response,
+    SC_invalid_http,
+    SC_socks_invalid_version,
+    SC_socks_no_acceptable_login_method,
+    SC_socks_refused,
+    SC_socks_no_connection,
+    SC_ssl_internal_failure,
+    SC_ssl_no_handshake,
+
+    // No one returns this code, but StatusCode values higher than this are
+    // deemed more successful than any generic HTTP response.
+    SC_http_error_watermark,
+
+    SC_ssl_invalid_server_certificate,
+    SC_ssl_self_signed_server_certificate,
+    SC_ssl_unexpected_server,
+
+    // These errors are only generated after a download_to_*() call been
+    // issued.
+    SC_download_open_error,
+    SC_download_write_error,
+    SC_download_invalid_range,
+  };
+
+  INLINE HTTPClient *get_client() const;
+
+  INLINE bool is_valid() const;
+
+  INLINE const URLSpec &get_url() const;
+  INLINE const DocumentSpec &get_document_spec() const;
+  INLINE int get_status_code() const;
+  INLINE std::string get_status_string() const;
+  INLINE const std::string &get_www_realm() const;
+  std::string get_header_value(const std::string &key) const;
+
+  INLINE void set_content_type(std::string content_type);
+  INLINE std::string get_content_type() const;
+
+  INLINE void set_expected_file_size(size_t file_size);
+  std::streamsize get_file_size() const;
+  INLINE bool is_file_size_known() const;
+
+  INLINE size_t get_first_byte_requested() const;
+  INLINE size_t get_last_byte_requested() const;
+  INLINE size_t get_first_byte_delivered() const;
+  INLINE size_t get_last_byte_delivered() const;
+
+  void write_headers(std::ostream &out) const;
+
+  INLINE void reset();
+  INLINE void preserve_status();
+
+  INLINE void clear_extra_headers();
+  INLINE void send_extra_header(const std::string &key, const std::string &value);
+
+  BLOCKING INLINE bool get_document(const DocumentSpec &url);
+  BLOCKING INLINE bool get_subdocument(const DocumentSpec &url,
+                                       size_t first_byte, size_t last_byte);
+  BLOCKING INLINE bool get_header(const DocumentSpec &url);
+  BLOCKING INLINE bool post_form(const DocumentSpec &url, const std::string &body);
+  BLOCKING INLINE bool put_document(const DocumentSpec &url, const std::string &body);
+  BLOCKING INLINE bool delete_document(const DocumentSpec &url);
+  BLOCKING INLINE bool get_options(const DocumentSpec &url);
+
+  INLINE void begin_get_document(const DocumentSpec &url);
+  INLINE void begin_get_subdocument(const DocumentSpec &url,
+                                    size_t first_byte, size_t last_byte);
+  INLINE void begin_get_header(const DocumentSpec &url);
+  INLINE void begin_post_form(const DocumentSpec &url, const std::string &body);
+  bool run();
+
+  BLOCKING bool download_to_ram(Ramfile *ramfile, bool subdocument_resumes = true);
+  BLOCKING bool download_to_stream(std::ostream *strm, bool subdocument_resumes = true);
+
+  INLINE size_t get_bytes_downloaded() const;
+  INLINE size_t get_bytes_requested() const;
+  INLINE bool is_download_complete() const;
+
+private:
+  enum State {
+    S_unsent = 0,
+    S_opened = 1,
+    S_headers_received = 2,
+    S_loading = 3,
+    S_done = 4
+  };
+  State get_state() const;
+
+  bool run_send();
+  bool run_headers_received();
+
+  bool begin_request(HTTPEnum::Method method, const DocumentSpec &url,
+                     const std::string &body, bool nonblocking,
+                     size_t first_byte, size_t last_byte);
+  void reset_for_new_request();
+
+  bool open_download_file();
+
+  bool parse_content_range(const std::string &content_range);
+
+  void reset_download_to();
+  void close_download_stream();
+
+private:
+  class StatusEntry {
+  public:
+    INLINE StatusEntry();
+    int _status_code;
+    std::string _status_string;
+  };
+  typedef pvector<StatusEntry> StatusList;
+
+  HTTPClient *_client;
+  StatusList _status_list;
+  URLSpec _proxy;
+
+  typedef std::pair<std::string, std::string> ExtraHeader;
+  pvector<ExtraHeader> _send_extra_headers;
+
+  bool _nonblocking;
+  bool _wanted_nonblocking;
+
+  DocumentSpec _document_spec;
+  DocumentSpec _request;
+  HTTPEnum::Method _method;
+  std::string request_path;
+  std::string _header;
+  std::string _body;
+  std::string _content_type;
+  size_t _first_byte_requested;
+  size_t _last_byte_requested;
+  size_t _first_byte_delivered;
+  size_t _last_byte_delivered;
+
+  enum DownloadDest {
+    DD_none,
+    DD_file,
+    DD_ram,
+    DD_stream,
+  };
+  DownloadDest _download_dest;
+  bool _subdocument_resumes;
+  //Filename _download_to_filename;
+  Ramfile *_download_to_ramfile;
+  std::ostream *_download_to_stream;
+
+  StatusEntry _status_entry;
+
+  std::string _www_realm;
+  //std::string _www_username;
+  //PT(HTTPAuthorization) _www_auth;
+
+  typedef pmap<std::string, std::string> Headers;
+  Headers _headers;
+
+  size_t _expected_file_size;
+  size_t _file_size;
+  size_t _transfer_file_size;
+  size_t _bytes_downloaded;
+  size_t _bytes_requested;
+  bool _got_expected_file_size;
+  bool _got_file_size;
+  bool _got_transfer_file_size;
+
+public:
+  virtual TypeHandle get_type() const {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    TypedReferenceCount::init_type();
+    register_type(_type_handle, "HTTPChannel",
+                  TypedReferenceCount::get_class_type());
+  }
+
+private:
+  static TypeHandle _type_handle;
+  friend class HTTPClient;
+};
+
+#include "httpChannel_emscripten.I"
+
+#endif  // __EMSCRIPTEN__
+
+#endif

+ 4 - 0
panda/src/downloader/httpClient.h

@@ -214,6 +214,10 @@ private:
 
 
 #include "httpClient.I"
 #include "httpClient.I"
 
 
+#elif defined(__EMSCRIPTEN__)
+
+#include "httpClient_emscripten.h"
+
 #endif  // HAVE_OPENSSL
 #endif  // HAVE_OPENSSL
 
 
 #endif
 #endif

+ 32 - 0
panda/src/downloader/httpClient_emscripten.I

@@ -0,0 +1,32 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file httpClient_emscripten.I
+ * @author rdb
+ * @date 2021-02-10
+ */
+
+/**
+ * Implements HTTPAuthorization::base64_encode().  This is provided here just
+ * as a convenient place to publish it for access by the scripting language;
+ * C++ code should probably use HTTPAuthorization directly.
+ */
+INLINE std::string HTTPClient::
+base64_encode(const std::string &s) {
+  return HTTPAuthorization::base64_encode(s);
+}
+
+/**
+ * Implements HTTPAuthorization::base64_decode().  This is provided here just
+ * as a convenient place to publish it for access by the scripting language;
+ * C++ code should probably use HTTPAuthorization directly.
+ */
+INLINE std::string HTTPClient::
+base64_decode(const std::string &s) {
+  return HTTPAuthorization::base64_decode(s);
+}

+ 426 - 0
panda/src/downloader/httpClient_emscripten.cxx

@@ -0,0 +1,426 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file httpClient_emscripten.cxx
+ * @author rdb
+ * @date 2021-02-10
+ */
+
+#include "httpClient_emscripten.h"
+#include "httpChannel.h"
+#include "config_downloader.h"
+#include "httpBasicAuthorization.h"
+#include "httpDigestAuthorization.h"
+
+#ifdef __EMSCRIPTEN__
+
+#include <emscripten/em_asm.h>
+
+using std::string;
+
+PT(HTTPClient) HTTPClient::_global_ptr;
+
+/**
+ *
+ */
+HTTPClient::
+HTTPClient() {
+  ConfigVariableList http_username
+    ("http-username",
+     PRC_DESC("Adds one or more username/password pairs to all HTTP clients.  The client "
+              "will present this username/password when asked to authenticate a request "
+              "for a particular server and/or realm.  The username is of the form "
+              "server:realm:username:password, where either or both of server and "
+              "realm may be empty, or just realm:username:password or username:password.  "
+              "If the server or realm is empty, they will match anything."));
+
+  {
+    // Also load in the general usernames.
+    int num_unique_values = http_username.get_num_unique_values();
+    for (int i = 0; i < num_unique_values; i++) {
+      string username = http_username.get_unique_value(i);
+      add_http_username(username);
+    }
+  }
+}
+
+/**
+ * Specifies the username:password string corresponding to a particular server
+ * and/or realm, when demanded by the server.  Either or both of the server or
+ * realm may be empty; if so, they match anything.  Also, the server may be
+ * set to the special string `"*proxy"`, which will match any proxy server.
+ *
+ * If the username is set to the empty string, this clears the password for
+ * the particular server/realm pair.
+ */
+void HTTPClient::
+set_username(const string &server, const string &realm, const string &username) {
+  string key = server + ":" + realm;
+  if (username.empty()) {
+    _usernames.erase(key);
+  } else {
+    _usernames[key] = username;
+  }
+}
+
+/**
+ * Returns the username:password string set for this server/realm pair, or
+ * empty string if nothing has been set.  See set_username().
+ */
+string HTTPClient::
+get_username(const string &server, const string &realm) const {
+  string key = server + ":" + realm;
+  Usernames::const_iterator ui;
+  ui = _usernames.find(key);
+  if (ui != _usernames.end()) {
+    return (*ui).second;
+  }
+  return string();
+}
+
+/**
+ * Stores the indicated cookie in the client's list of cookies, as if it had
+ * been received from a server.
+ */
+void HTTPClient::
+set_cookie(const HTTPCookie &cookie) {
+  std::ostringstream stream;
+  stream << cookie;
+  std::string str = stream.str();
+
+  EM_ASM({
+    document.cookie = UTF8ToString($0);
+  }, str.c_str());
+}
+
+/**
+ * Removes the cookie with the matching domain/path/name from the client's
+ * list of cookies.  Returns true if it was removed, false if the cookie was
+ * not matched.
+ */
+bool HTTPClient::
+clear_cookie(const HTTPCookie &cookie) {
+  HTTPCookie expired;
+  expired.set_name(cookie.get_name());
+  expired.set_path(cookie.get_path());
+  expired.set_domain(cookie.get_domain());
+  expired.set_expires(HTTPDate((time_t)0));
+
+  std::ostringstream stream;
+  stream << expired;
+  std::string str = stream.str();
+
+  return (bool)EM_ASM_INT({
+    var set = UTF8ToString($0);
+    var old = document.cookie;
+    document.cookie = set;
+    return (document.cookie !== old);
+  }, str.c_str());
+}
+
+/**
+ * Removes the all stored cookies from the client.
+ */
+void HTTPClient::
+clear_all_cookies() {
+  // NB. This is imperfect, and won't clear cookies with other domains or paths.
+  EM_ASM({
+    var cookies = document.cookie.split(";");
+    for (var i = 0; i < cookies.length; ++i) {
+      var name = cookies[i].split("=", 1)[0];
+      document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+    }
+
+    cookies = document.cookie.split(";");
+    for (var i = 0; i < cookies.length; ++i) {
+      var name = cookies[i].split("=", 1)[0];
+      document.cookie = name + "=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+    }
+  });
+}
+
+/**
+ * Returns true if there is a cookie in the client matching the given cookie's
+ * domain/path/name, false otherwise.
+ */
+//bool HTTPClient::
+//has_cookie(const HTTPCookie &cookie) const {
+//}
+
+/**
+ * Looks up and returns the cookie in the client matching the given cookie's
+ * domain/path/name.  If there is no matching cookie, returns an empty cookie.
+ */
+//HTTPCookie HTTPClient::
+//get_cookie(const HTTPCookie &cookie) const {
+//  return HTTPCookie();
+//}
+
+/**
+ * Outputs the complete list of cookies stored on the client, for all domains,
+ * including the expired cookies (which will normally not be sent back to a
+ * host).
+ */
+void HTTPClient::
+write_cookies(std::ostream &out) const {
+  char *str = (char *)EM_ASM_INT({
+    var str = document.cookie.replace(/; ?/g, "\n");
+    var len = lengthBytesUTF8(str) + 1;
+    var buffer = _malloc(len);
+    stringToUTF8(str, buffer, len);
+    return buffer;
+  });
+
+  out << str << "\n";
+  free(str);
+}
+
+/**
+ * Returns a new HTTPChannel object that may be used for reading multiple
+ * documents using the same connection, for greater network efficiency than
+ * calling HTTPClient::get_document() repeatedly (which would force a new
+ * connection for each document).
+ *
+ * Also, HTTPChannel has some additional, less common interface methods than
+ * the basic interface methods that exist on HTTPClient; if you wish to call
+ * any of these methods you must first obtain an HTTPChannel.
+ */
+PT(HTTPChannel) HTTPClient::
+make_channel(bool persistent_connection) {
+  return new HTTPChannel(this);
+}
+
+/**
+ * Posts form data to a particular URL and retrieves the response.  Returns a
+ * new HTTPChannel object whether the document is successfully read or not;
+ * you can test is_valid() and get_return_code() to determine whether the
+ * document was retrieved.
+ */
+PT(HTTPChannel) HTTPClient::
+post_form(const URLSpec &url, const string &body) {
+  PT(HTTPChannel) doc = new HTTPChannel(this);
+  doc->post_form(url, body);
+  return doc;
+}
+
+/**
+ * Opens the named document for reading.  Returns a new HTTPChannel object
+ * whether the document is successfully read or not; you can test is_valid()
+ * and get_return_code() to determine whether the document was retrieved.
+ */
+PT(HTTPChannel) HTTPClient::
+get_document(const URLSpec &url) {
+  PT(HTTPChannel) doc = new HTTPChannel(this);
+  doc->get_document(url);
+  return doc;
+}
+
+/**
+ * Like get_document(), except only the header associated with the document is
+ * retrieved.  This may be used to test for existence of the document; it
+ * might also return the size of the document (if the server gives us this
+ * information).
+ */
+PT(HTTPChannel) HTTPClient::
+get_header(const URLSpec &url) {
+  PT(HTTPChannel) doc = new HTTPChannel(this);
+  doc->get_header(url);
+  return doc;
+}
+
+/**
+ * Returns the default global HTTPClient.
+ */
+HTTPClient *HTTPClient::
+get_global_ptr() {
+  if (_global_ptr == nullptr) {
+    _global_ptr = new HTTPClient;
+  }
+  return _global_ptr;
+}
+
+/**
+ * Handles a Config definition for http-username as
+ * server:realm:username:password, where either or both of server and realm
+ * may be empty, or just server:username:password or username:password.
+ */
+void HTTPClient::
+add_http_username(const string &http_username) {
+  size_t c1 = http_username.find(':');
+  if (c1 != string::npos) {
+    size_t c2 = http_username.find(':', c1 + 1);
+    if (c2 != string::npos) {
+      size_t c3 = http_username.find(':', c2 + 1);
+      if (c3 != string::npos) {
+        size_t c4 = http_username.find(':', c3 + 1);
+        if (c4 != string::npos) {
+          // Oops, we have five?  Problem.
+          downloader_cat.error()
+            << "Invalid http-username " << http_username << "\n";
+        }
+        else {
+          // Ok, we have four.
+          set_username(http_username.substr(0, c1),
+                       http_username.substr(c1 + 1, c2 - (c1 + 1)),
+                       http_username.substr(c2 + 1));
+        }
+      }
+      else {
+        // We have only three.
+        set_username(string(),
+                     http_username.substr(0, c1),
+                     http_username.substr(c1 + 1));
+      }
+    }
+    else {
+      // We have only two.
+      set_username(string(), string(), http_username);
+    }
+  } else {
+    // We have only one?  Problem.
+    downloader_cat.error()
+      << "Invalid http-username " << http_username << "\n";
+  }
+}
+
+/**
+ * Chooses a suitable username:password string for the given URL and realm.
+ */
+string HTTPClient::
+select_username(const URLSpec &url, const string &realm) const {
+  string username;
+
+  // Look in several places in order to find the matching username.
+
+  // Fist, if there's a username on the URL, that always wins (except when we
+  // are looking for a proxy username).
+  if (url.has_username()) {
+    username = url.get_username();
+  }
+
+  // Otherwise, start looking on the HTTPClient.
+  if (username.empty()) {
+    // Try the specific serverrealm.
+    username = get_username(url.get_server(), realm);
+  }
+  if (username.empty()) {
+    // Then, try the specific serverany realm.
+    username = get_username(url.get_server(), string());
+  }
+  if (username.empty()) {
+    // Then, try any server with this realm.
+    username = get_username(string(), realm);
+  }
+  if (username.empty()) {
+    // Then, take the general password.
+    username = get_username(string(), string());
+  }
+
+  return username;
+}
+
+/**
+ * Chooses a suitable pre-computed authorization for the indicated URL.
+ * Returns NULL if no authorization matches.
+ */
+HTTPAuthorization *HTTPClient::
+select_auth(const URLSpec &url, const string &last_realm) {
+  Domains &domains = _www_domains;
+  std::string canon = HTTPAuthorization::get_canonical_url(url).get_url();
+
+  // Look for the longest domain string that is a prefix of our canonical URL.
+  // We have to make a linear scan through the list.
+  Domains::const_iterator best_di = domains.end();
+  size_t longest_length = 0;
+  Domains::const_iterator di;
+  for (di = domains.begin(); di != domains.end(); ++di) {
+    const string &domain = (*di).first;
+    size_t length = domain.length();
+    if (domain == canon.substr(0, length)) {
+      // This domain string matches.  Is it the longest?
+      if (length > longest_length) {
+        best_di = di;
+        longest_length = length;
+      }
+    }
+  }
+
+  if (best_di != domains.end()) {
+    // Ok, we found a matching domain.  Use it.
+    if (downloader_cat.is_spam()) {
+      downloader_cat.spam()
+        << "Choosing domain " << (*best_di).first << " for " << url << "\n";
+    }
+    const Realms &realms = (*best_di).second._realms;
+    // First, try our last realm.
+    Realms::const_iterator ri;
+    ri = realms.find(last_realm);
+    if (ri != realms.end()) {
+      return (*ri).second;
+    }
+
+    if (!realms.empty()) {
+      // Oh well, just return the first realm.
+      return (*realms.begin()).second;
+    }
+  }
+
+  // No matching domains.
+  return nullptr;
+}
+
+/**
+ * Generates a new authorization entry in response to a 401 or 407 challenge
+ * from the server or proxy.  The new authorization entry is stored for future
+ * connections to the same server (or, more precisely, the same domain, which
+ * may be a subset of the server, or it may include multiple servers).
+ */
+PT(HTTPAuthorization) HTTPClient::
+generate_auth(const URLSpec &url, const string &challenge) {
+  HTTPAuthorization::AuthenticationSchemes schemes;
+  HTTPAuthorization::parse_authentication_schemes(schemes, challenge);
+
+  PT(HTTPAuthorization) auth;
+  HTTPAuthorization::AuthenticationSchemes::iterator si;
+
+#ifdef HAVE_OPENSSL
+  si = schemes.find("digest");
+  if (si != schemes.end()) {
+    auth = new HTTPDigestAuthorization((*si).second, url, false);
+  }
+#endif
+
+  if (auth == nullptr || !auth->is_valid()) {
+    si = schemes.find("basic");
+    if (si != schemes.end()) {
+      auth = new HTTPBasicAuthorization((*si).second, url, false);
+    }
+  }
+
+  if (auth == nullptr || !auth->is_valid()) {
+    downloader_cat.warning()
+      << "Don't know how to use any of the server's available authorization schemes:\n";
+    for (si = schemes.begin(); si != schemes.end(); ++si) {
+      downloader_cat.warning() << (*si).first << "\n";
+    }
+  }
+  else {
+    // Now that we've got an authorization, store it under under each of its
+    // suggested domains for future use.
+    Domains &domains = _www_domains;
+    const vector_string &domain = auth->get_domain();
+    vector_string::const_iterator si;
+    for (si = domain.begin(); si != domain.end(); ++si) {
+      domains[(*si)]._realms[auth->get_realm()] = auth;
+    }
+  }
+
+  return auth;
+}
+
+#endif  // __EMSCRIPTEN__

+ 93 - 0
panda/src/downloader/httpClient_emscripten.h

@@ -0,0 +1,93 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file httpClient_emscripten.h
+ * @author rdb
+ * @date 2021-02-10
+ */
+
+#ifndef HTTPCLIENT_EMSCRIPTEN_H
+#define HTTPCLIENT_EMSCRIPTEN_H
+
+#include "pandabase.h"
+
+#ifdef __EMSCRIPTEN__
+
+#include "urlSpec.h"
+#include "httpAuthorization.h"
+#include "httpEnum.h"
+#include "httpChannel.h"
+#include "httpCookie.h"
+#include "pointerTo.h"
+#include "pvector.h"
+#include "pmap.h"
+#include "referenceCount.h"
+
+class Filename;
+class HTTPChannel;
+
+/**
+ * Reduced version of HTTPClient that is available in Emscripten.  It uses the
+ * browser to make HTTP requests.  As such, there is only a global HTTPClient
+ * pointer, and it is not possible to make individual HTTPClient objects.
+ */
+class EXPCL_PANDA_DOWNLOADER HTTPClient : public ReferenceCount {
+private:
+  HTTPClient();
+
+PUBLISHED:
+  void set_username(const std::string &server, const std::string &realm, const std::string &username);
+  std::string get_username(const std::string &server, const std::string &realm) const;
+
+  void set_cookie(const HTTPCookie &cookie);
+  bool clear_cookie(const HTTPCookie &cookie);
+  void clear_all_cookies();
+  //bool has_cookie(const HTTPCookie &cookie) const;
+  //HTTPCookie get_cookie(const HTTPCookie &cookie) const;
+
+  void write_cookies(std::ostream &out) const;
+
+  PT(HTTPChannel) make_channel(bool persistent_connection);
+  BLOCKING PT(HTTPChannel) post_form(const URLSpec &url, const std::string &body);
+  BLOCKING PT(HTTPChannel) get_document(const URLSpec &url);
+  BLOCKING PT(HTTPChannel) get_header(const URLSpec &url);
+
+  INLINE static std::string base64_encode(const std::string &s);
+  INLINE static std::string base64_decode(const std::string &s);
+
+  static HTTPClient *get_global_ptr();
+
+private:
+  void add_http_username(const std::string &http_username);
+  std::string select_username(const URLSpec &url, const std::string &realm) const;
+
+  HTTPAuthorization *select_auth(const URLSpec &url, const std::string &last_realm);
+  PT(HTTPAuthorization) generate_auth(const URLSpec &url,
+                                      const std::string &challenge);
+
+  typedef pmap<std::string, std::string> Usernames;
+  Usernames _usernames;
+
+  typedef pmap<std::string, PT(HTTPAuthorization)> Realms;
+  class Domain {
+  public:
+    Realms _realms;
+  };
+  typedef pmap<std::string, Domain> Domains;
+  Domains _www_domains;
+
+  static PT(HTTPClient) _global_ptr;
+
+  friend class HTTPChannel;
+};
+
+#include "httpClient_emscripten.I"
+
+#endif  // __EMSCRIPTEN__
+
+#endif

+ 1 - 1
panda/src/downloader/httpCookie.cxx

@@ -13,7 +13,7 @@
 
 
 #include "httpCookie.h"
 #include "httpCookie.h"
 
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 
 #include "httpChannel.h"
 #include "httpChannel.h"
 #include "string_utils.h"
 #include "string_utils.h"

+ 1 - 1
panda/src/downloader/httpCookie.h

@@ -20,7 +20,7 @@
 // this to establish https connections; this is because it uses the OpenSSL
 // this to establish https connections; this is because it uses the OpenSSL
 // library to portably handle all of the socket communications.
 // library to portably handle all of the socket communications.
 
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 
 #include "httpDate.h"
 #include "httpDate.h"
 #include "urlSpec.h"
 #include "urlSpec.h"

+ 1 - 1
panda/src/downloader/httpEnum.cxx

@@ -13,7 +13,7 @@
 
 
 #include "httpEnum.h"
 #include "httpEnum.h"
 
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 
 /**
 /**
  *
  *

+ 3 - 1
panda/src/downloader/httpEnum.h

@@ -20,7 +20,7 @@
 // this to establish https connections; this is because it uses the OpenSSL
 // this to establish https connections; this is because it uses the OpenSSL
 // library to portably handle all of the socket communications.
 // library to portably handle all of the socket communications.
 
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 
 /**
 /**
  * This class is just used as a namespace wrapper for some of the enumerated
  * This class is just used as a namespace wrapper for some of the enumerated
@@ -28,12 +28,14 @@
  */
  */
 class EXPCL_PANDA_DOWNLOADER HTTPEnum {
 class EXPCL_PANDA_DOWNLOADER HTTPEnum {
 PUBLISHED:
 PUBLISHED:
+#ifdef HAVE_OPENSSL
   enum HTTPVersion {
   enum HTTPVersion {
     HV_09,  // HTTP 0.9 or older
     HV_09,  // HTTP 0.9 or older
     HV_10,  // HTTP 1.0
     HV_10,  // HTTP 1.0
     HV_11,  // HTTP 1.1
     HV_11,  // HTTP 1.1
     HV_other,
     HV_other,
   };
   };
+#endif
 
 
   enum Method {
   enum Method {
     M_options,
     M_options,

+ 2 - 0
panda/src/downloader/p3downloader_composite2.cxx

@@ -1,7 +1,9 @@
 #include "httpAuthorization.cxx"
 #include "httpAuthorization.cxx"
 #include "httpBasicAuthorization.cxx"
 #include "httpBasicAuthorization.cxx"
 #include "httpChannel.cxx"
 #include "httpChannel.cxx"
+#include "httpChannel_emscripten.cxx"
 #include "httpClient.cxx"
 #include "httpClient.cxx"
+#include "httpClient_emscripten.cxx"
 #include "httpCookie.cxx"
 #include "httpCookie.cxx"
 #include "httpDate.cxx"
 #include "httpDate.cxx"
 #include "httpDigestAuthorization.cxx"
 #include "httpDigestAuthorization.cxx"

+ 24 - 1
panda/src/downloader/virtualFileHTTP.cxx

@@ -19,7 +19,7 @@
 
 
 #include <iterator>
 #include <iterator>
 
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 
 using std::istream;
 using std::istream;
 using std::ostream;
 using std::ostream;
@@ -140,9 +140,32 @@ open_read_file(bool auto_unwrap) const {
     return nullptr;
     return nullptr;
   }
   }
 
 
+  strstream->seekg(0);
+
   return return_file(strstream, auto_unwrap);
   return return_file(strstream, auto_unwrap);
 }
 }
 
 
+/**
+ * Fills up the indicated string with the contents of the file, if it is a
+ * regular file.  Returns true on success, false otherwise.
+ */
+bool VirtualFileHTTP::
+read_file(string &result, bool auto_unwrap) const {
+  result = string();
+
+  if (_status_only) {
+    return false;
+  }
+
+  Ramfile ramfile;
+  if (!_channel->download_to_ram(&ramfile, false)) {
+    return false;
+  }
+
+  result = std::move(ramfile._data);
+  return true;
+}
+
 /**
 /**
  * Fills up the indicated pvector with the contents of the file, if it is a
  * Fills up the indicated pvector with the contents of the file, if it is a
  * regular file.  Returns true on success, false otherwise.
  * regular file.  Returns true on success, false otherwise.

+ 2 - 1
panda/src/downloader/virtualFileHTTP.h

@@ -20,7 +20,7 @@
 #include "httpChannel.h"
 #include "httpChannel.h"
 #include "urlSpec.h"
 #include "urlSpec.h"
 
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 
 class VirtualFileMountHTTP;
 class VirtualFileMountHTTP;
 
 
@@ -51,6 +51,7 @@ public:
   virtual std::streamsize get_file_size() const;
   virtual std::streamsize get_file_size() const;
   virtual time_t get_timestamp() const;
   virtual time_t get_timestamp() const;
 
 
+  virtual bool read_file(std::string &result, bool auto_unwrap) const;
   virtual bool read_file(vector_uchar &result, bool auto_unwrap) const;
   virtual bool read_file(vector_uchar &result, bool auto_unwrap) const;
 
 
 private:
 private:

+ 1 - 1
panda/src/downloader/virtualFileMountHTTP.cxx

@@ -15,7 +15,7 @@
 #include "virtualFileHTTP.h"
 #include "virtualFileHTTP.h"
 #include "virtualFileSystem.h"
 #include "virtualFileSystem.h"
 
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 
 using std::string;
 using std::string;
 
 

+ 1 - 1
panda/src/downloader/virtualFileMountHTTP.h

@@ -16,7 +16,7 @@
 
 
 #include "pandabase.h"
 #include "pandabase.h"
 
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 
 #include "virtualFileMount.h"
 #include "virtualFileMount.h"
 #include "httpClient.h"
 #include "httpClient.h"

+ 1 - 1
panda/src/express/virtualFile.h

@@ -84,7 +84,7 @@ public:
   INLINE bool write_file(const std::string &data, bool auto_wrap);
   INLINE bool write_file(const std::string &data, bool auto_wrap);
 
 
   INLINE void set_original_filename(const Filename &filename);
   INLINE void set_original_filename(const Filename &filename);
-  bool read_file(std::string &result, bool auto_unwrap) const;
+  virtual bool read_file(std::string &result, bool auto_unwrap) const;
   virtual bool read_file(vector_uchar &result, bool auto_unwrap) const;
   virtual bool read_file(vector_uchar &result, bool auto_unwrap) const;
   virtual bool write_file(const unsigned char *data, size_t data_size, bool auto_wrap);
   virtual bool write_file(const unsigned char *data, size_t data_size, bool auto_wrap);
 
 

+ 46 - 3
panda/src/express/virtualFileSystem.cxx

@@ -28,6 +28,10 @@
 #include "executionEnvironment.h"
 #include "executionEnvironment.h"
 #include "pset.h"
 #include "pset.h"
 
 
+#ifdef __EMSCRIPTEN__
+#include "virtualFileMountHTTP.h"
+#endif
+
 using std::iostream;
 using std::iostream;
 using std::istream;
 using std::istream;
 using std::ostream;
 using std::ostream;
@@ -853,10 +857,49 @@ get_global_ptr() {
     _global_ptr = new VirtualFileSystem;
     _global_ptr = new VirtualFileSystem;
 
 
     // Set up the default mounts.  First, there is always the root mount.
     // Set up the default mounts.  First, there is always the root mount.
-    _global_ptr->mount("/", "/", 0);
+#ifdef __EMSCRIPTEN__
+    // Unless we're running in node.js, we don't have a filesystem, and instead
+    // mount the current server root as our filesystem root.
+    bool is_node = (bool)EM_ASM_INT(return (typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string'));
+    if (!is_node) {
+      _global_ptr->mount(new VirtualFileMountHTTP(URLSpec("/")), "/", MF_read_only);
+
+      // And get the "current working directory".
+      char cwd[4096];
+      bool have_memfs = (bool)EM_ASM_INT({
+        var path = location.pathname;
+        stringToUTF8(path.substring(0, path.lastIndexOf('/')), $0, 4096);
+
+        if (FS && FS.root) {
+          /* Emscripten creates these by default, but we don't want them. */
+          var contents = FS.root.contents;
+          delete contents.dev;
+          delete contents.home;
+          delete contents.proc;
+          delete contents.tmp;
+          return true;
+        }
+        else {
+          return false;
+        }
+      }, cwd);
+
+      _global_ptr->_cwd = cwd;
 
 
-    // And our initial cwd comes from the environment.
-    _global_ptr->chdir(ExecutionEnvironment::get_cwd());
+      // If we built with the Emscripten VFS enabled, mount it on top of the
+      // current directory, so that emscripten's preload system will work.
+      if (have_memfs) {
+        _global_ptr->mount("/", _global_ptr->_cwd, MF_read_only);
+      }
+    }
+    else
+#endif
+    {
+      _global_ptr->mount("/", "/", 0);
+
+      // And our initial cwd comes from the environment.
+      _global_ptr->chdir(ExecutionEnvironment::get_cwd());
+    }
 
 
     // Then, we add whatever mounts are listed in the Configrc file.
     // Then, we add whatever mounts are listed in the Configrc file.
     ConfigVariableList mounts
     ConfigVariableList mounts