// // Things to worry about: // Expect: 100-Continue #include "http.hpp" #include "wrap-sstream.hpp" #include "wrap-string.hpp" #include "util.hpp" #include "luastack.hpp" #include using string_view = std::string_view; bool words_separated_by_dashes(string_view v) { while (true) { if (!sv::ascii_isalpha(sv::zfront(v))) return false; v.remove_prefix(1); while (sv::ascii_isalnum(sv::zfront(v))) v.remove_prefix(1); if (v.empty()) return true; if (sv::zfront(v) != '-') return false; v.remove_prefix(1); } } // Technically, this is a true, correct URL encode routine. static eng::string url_encode_param(string_view value) { eng::ostringstream result; const char *hexdigits = "0123456789ABCDEF"; for (int i = 0; i < int(value.size()); i++) { char c = value[i]; if (sv::ascii_isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { result << c; } else if (c == ' ') { result << '+'; } else { result << '%' << hexdigits[c>>4] << hexdigits[c&15]; } } return result.str(); } // This URL encode routine leaves slashes intact. That's not // technically correct, but it's really what you want for paths. static eng::string url_encode_path(string_view value) { eng::ostringstream result; const char *hexdigits = "0123456789ABCDEF"; for (int i = 0; i < int(value.size()); i++) { char c = value[i]; if (sv::ascii_isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || c == '/') { result << c; } else if (c == ' ') { result << '+'; } else { result << '%' << hexdigits[c>>4] << hexdigits[c&15]; } } return result.str(); } static eng::string url_decode(string_view eurl) { eng::ostringstream result; int i = 0; int len = eurl.size(); while (i < len) { char c = eurl[i]; if (c == '+') { result << ' '; i += 1; } else if ((c == '%') && (i + 2 < len)) { std::string_view code = eurl.substr(i + 1, 2); uint64_t value = sv::to_hex64(code); if (value > 255) { result << '?'; } else { result << char(value); } i += 3; } else { result << c; i += 1; } } return result.str(); } static void send_encoded_path(std::string_view path, const UrlParameters ¶ms, StreamBuffer *sb) { sb->write_bytes(url_encode_path(path)); bool first_param = true; for (const auto &pair : params) { sb->write_char(first_param ? '?' : '&'); sb->write_bytes(url_encode_param(pair.first)); sb->write_char('='); sb->write_bytes(url_encode_param(pair.second)); first_param = false; } } static void send_host_and_port(std::string_view host, int port, StreamBuffer *sb) { sb->write_bytes(host); if (port != 0) { sb->write_char(':'); sb->ostream() << port; } } // In a properly-formed url, the hostname and path are url encoded. // This parser expects an encoded URL. struct ParsedURL { public: bool valid; eng::string proto; eng::string host; int port; eng::string path; UrlParameters params; public: void clear() { valid = false; proto.clear(); host.clear(); port = 0; path.clear(); params.clear(); } eng::string str() { StreamBuffer sb; sb.write_bytes(proto); sb.write_bytes("://"); send_host_and_port(host, port, &sb); send_encoded_path(path, params, &sb); return eng::string(sb.view()); } ParsedURL(std::string_view url) { clear(); proto = util::ascii_tolower(sv::read_to_sep(url, ':')); if (!sv::has_prefix(url, "//")) { clear(); return; } url.remove_prefix(2); if (!words_separated_by_dashes(proto)) { clear(); return; } // Extract the host and port as a single string. string_view turl = url; string_view hostport = sv::read_to_sep(turl, '/'); url.remove_prefix(hostport.size()); // Split the host and port from each other and parse them. host = util::ascii_tolower(sv::read_to_sep(hostport, ':')); if (host.empty()) { clear(); return; } if (!hostport.empty()) { int64_t iport = sv::to_int64(hostport); if ((iport < 1) || (iport > 65535)) { clear(); return; } port = iport; } // Split off the path. path = url_decode(sv::read_to_sep(url, '?')); if (path.empty()) { path = "/"; } // Process url parameters. while (!sv::isnull(url)) { std::string_view keyval = sv::read_to_sep(url, '&'); if (keyval.empty()) { clear(); return; } std::string_view key = sv::read_to_sep(keyval, '='); if (key.empty()) { clear(); return; } if (sv::isnull(keyval)) { clear(); return; } eng::string dkey = url_decode(key); eng::string dval = url_decode(keyval); params[dkey] = dval; } // If we made it here, we have a valid URL valid = true; } }; HttpClientRequest::HttpClientRequest() { verify_certificate_ = false; port_ = 0; request_id_ = 0; place_id_ = 0; thread_id_ = 0; } void HttpClientRequest::fail(string_view s) { if (error_.empty()) { error_ = s; } } eng::string HttpClientRequest::target() const { assert(check().empty()); eng::ostringstream oss; oss << (verify_certificate_ ? "cert" : "nocert"); oss << ':' << host_ << ':' << port_; return oss.str(); } void HttpClientRequest::set_verify_certificate(bool flag) { verify_certificate_ = flag; } void HttpClientRequest::set_method(const eng::string &s) { eng::string method = util::ascii_toupper(s); if ((method != "GET") && (method != "HEAD")) { fail(util::ss("HTTP method not implemented: ", method, ".", "Currently, only HEAD and GET are implemented.")); return; } if ((!method_.empty()) && (method_ != method)) { fail(util::ss("HTTP method specified twice: ", method_, " and ", method)); return; } method_ = method; } void HttpClientRequest::set_host(const eng::string &s) { eng::string host = util::ascii_tolower(s); if (host.empty()) { fail(util::ss("HTTP hostname cannot be empty string.")); return; } // This is not quite strict, but it's close. I believe // the DNS lookup will fail for invalid hostnames anyway. for (char c : host) { if ((c != '-') && (c != '.') && (!sv::ascii_isalnum(c))) { fail(util::ss("HTTP hostnames can only contain letters, digits, and hyphen: ", host)); return; } } if (!host_.empty()) { fail(util::ss("HTTP hostname specified twice: ", host_, " and ", host)); return; } host_ = host; } void HttpClientRequest::set_port(int port) { if ((port < 1) || (port > 65535)) { fail(util::ss("HTTP port must be between 1 and 65535: ", port)); return; } if (port_ != 0) { fail(util::ss("HTTP port specified twice: ", port_, " and ", port)); return; } port_ = port; } void HttpClientRequest::set_path(string_view path) { if (!sv::has_prefix(path, "/")) { fail(util::ss("HTTP path must start with slash")); return; } if (!path_.empty()) { fail(util::ss("HTTP path specified twice: ", path_, " and ", path)); return; } path_ = path; } void HttpClientRequest::set_param(const eng::string &key, const eng::string &val) { if (params_.find(key) != params_.end()) { fail(util::ss("HTTP url parameter specified twice: ", key)); return; } if (key.empty()) { fail(util::ss("HTTP parameter key cannot be empty")); return; } params_[key] = val; } void HttpClientRequest::set_url(string_view url) { ParsedURL parsed_url(url); if (!parsed_url.valid) { fail(util::ss("syntactically invalid URL: ", url)); return; } if (parsed_url.proto != "https") { fail(util::ss("unsupported protocol: ", parsed_url.proto)); return; } set_host(parsed_url.host); if (parsed_url.port) set_port(parsed_url.port); set_path(parsed_url.path); for (const auto &pair : parsed_url.params) { set_param(pair.first, pair.second); } } void HttpClientRequest::set_verify_certificate(LuaStack &LS, LuaSlot val) { if (!LS.isboolean(val)) { fail(util::ss("HTTP verify_certificate must be a boolean")); return; } set_verify_certificate(LS.ckboolean(val)); } void HttpClientRequest::set_method(LuaStack &LS, LuaSlot val) { if (!LS.isstring(val)) { fail(util::ss("HTTP method must be a string")); return; } set_method(LS.ckstring(val)); } void HttpClientRequest::set_host(LuaStack &LS, LuaSlot val) { if (!LS.isstring(val)) { fail(util::ss("HTTP host must be a string")); return; } set_host(LS.ckstring(val)); } void HttpClientRequest::set_port(LuaStack &LS, LuaSlot val) { if (!LS.isint(val)) { fail(util::ss("HTTP port must be an int")); return; } set_port(LS.ckint(val)); } void HttpClientRequest::set_path(LuaStack &LS, LuaSlot val) { if (!LS.isstring(val)) { fail(util::ss("HTTP path must be a string")); return; } set_path(LS.ckstring(val)); } void HttpClientRequest::set_param(LuaStack &LS, LuaSlot key, LuaSlot val) { if (!LS.isstring(key)) { fail(util::ss("HTTP url parameter key must be a string")); return; } if (!LS.isstring(val)) { fail(util::ss("HTTP url parameter val must be a string")); return; } set_param(LS.ckstring(key), LS.ckstring(val)); } void HttpClientRequest::set_params(LuaStack &LS0, LuaSlot tab) { if (!LS0.istable(tab)) { fail(util::ss("HTTP params must be a table")); return; } LuaVar key, val; LuaStack LS(LS0.state(), key, val); LS.set(key, LuaNil); while (LS.next(tab, key, val)) { set_param(LS, key, val); } } void HttpClientRequest::set_url(LuaStack &LS, LuaSlot val) { if (!LS.isstring(val)) { fail(util::ss("HTTP url must be a string")); return; } set_url(LS.ckstring(val)); } void HttpClientRequest::set_defaults() { if (method_.empty()) { method_ = "GET"; } if (port_ == 0) { port_ = 443; } } void HttpClientRequest::set_config(LuaStack &LS0, LuaSlot tab) { LuaVar key, val; LuaStack LS(LS0.state(), key, val); LS.set(key, LuaNil); while (LS.next(tab, key, val)) { eng::string kstr; if (LS.isstring(key)) kstr = LS.ckstring(key); if (kstr == "method") { set_method(LS, val); } else if (kstr == "host") { set_host(LS, val); } else if (kstr == "port") { set_port(LS, val); } else if (kstr == "path") { set_path(LS, val); } else if (kstr == "params") { set_params(LS, val); } else if (kstr == "url") { set_url(LS, val); } else if (kstr == "verifycertificate") { set_verify_certificate(LS, val); } else if (kstr == "") { fail(util::ss("HTTP config parameter names must be strings.")); } else { fail(util::ss("HTTP unrecognized config parameter: ", kstr)); } } } eng::string HttpClientRequest::check() const { if (!error_.empty()) { return error_; } if (method_.empty()) { return "HTTP method has not been set"; } if (host_.empty()) { return "HTTP host has not been set"; } if (port_ == 0) { return "HTTP port has not been set"; } if (path_.empty()) { return "HTTP url has not been set"; } return ""; } void HttpClientRequest::send_internal(StreamBuffer *sb, bool debug_string) const { // If there's an error in the request, handle it. In debug string mode, // we just put the error into the output. In production mode, we assert // fail. eng::string error = check(); if (debug_string) { if (!error.empty()) { sb->write_bytes(error); return; } } else { assert(error.empty()); } // Choose a linebreak. eng::string linebreak = (debug_string) ? "\n" : "\r\n"; // Send the command. sb->write_bytes(method_); sb->write_char(' '); send_encoded_path(path_, params_, sb); sb->write_bytes(" HTTP/1.1"); sb->write_bytes(linebreak); // Send the host header. sb->write_bytes("Host: "); send_host_and_port(host_, port_, sb); sb->write_bytes(linebreak); // The empty accept-encoding header notifies the // server that we don't support gzip, deflate, or // other content compression. sb->write_bytes("Accept-encoding:"); sb->write_bytes(linebreak); // Add a user-agent header. Not sure why. sb->write_bytes("User-agent: Mozilla 5.0 (luprex)"); sb->write_bytes(linebreak); // Add the requester IDs (debug string only) if (debug_string && ((request_id_ != 0) || (place_id_ != 0) || (thread_id_ != 0))) { sb->write_bytes("X-Requester-Ids: "); sb->ostream() << "rid=" << request_id_ << "; pid=" << place_id_ << "; tid=" << thread_id_; sb->write_bytes(linebreak); } // Send the extra linebreak. if (!debug_string) { sb->write_bytes(linebreak); } } void HttpClientRequest::serialize(StreamBuffer *sb) const { sb->write_int64(request_id_); sb->write_int64(place_id_); sb->write_int64(thread_id_); sb->write_string(error_); sb->write_bool(verify_certificate_); sb->write_string(method_); sb->write_string(host_); sb->write_int32(port_); sb->write_string(path_); sb->write_int32(params_.size()); for (const auto &pair : params_) { sb->write_string(pair.first); sb->write_string(pair.second); } } void HttpClientRequest::deserialize(StreamBuffer *sb) { request_id_ = sb->read_int64(); place_id_ = sb->read_int64(); thread_id_ = sb->read_int64(); error_ = sb->read_string(); verify_certificate_ = sb->read_bool(); method_ = sb->read_string(); host_ = sb->read_string(); port_ = sb->read_int32(); path_ = sb->read_string(); int32_t nparams = sb->read_int32(); params_.clear(); for (int i = 0; i < nparams; i++) { eng::string k = sb->read_string(); eng::string v = sb->read_string(); params_[k] = v; } } eng::string HttpClientRequest::DebugString() { StreamBuffer sb; send_internal(&sb, true); return eng::string(sb.view()); } void HttpClientRequestMap::serialize(StreamBuffer *sb) const { sb->write_int32(size()); for (const auto &pair : *this) { pair.second.serialize(sb); } } void HttpClientRequestMap::deserialize(StreamBuffer *sb) { int32_t count = sb->read_int32(); clear(); HttpClientRequest req; for (int i = 0; i < count; i++) { req.deserialize(sb); (*this)[req.request_id()] = req; } } HttpClientResponse::HttpClientResponse() { request_id_ = 0; status_code_ = 0; response_length_ = 0; mime_type_ = ""; content_length_ = -1; } eng::string HttpClientResponse::DebugString() const { eng::ostringstream oss; oss << "HttpClientResponse:" << std::endl; oss << " status_code: " << status_code_ << std::endl; oss << " error: " << error_ << std::endl; oss << " content_length: " << content_length_ << std::endl; oss << " transfer_encoding: " << transfer_encoding_ << std::endl; oss << " location: " << location_ << std::endl; oss << " mime_type: " << mime_type_ << std::endl; oss << " charset: " << charset_ << std::endl; oss << " content: " << content_ << std::endl; oss << " response_length: " << response_length_ << std::endl; return oss.str(); } void HttpClientResponse::fail(int code, string_view message) { status_code_ = code; error_ = message; mime_type_ = ""; charset_ = ""; content_ = ""; } void HttpClientResponse::incomplete(bool closed) { if (closed) { fail(500, "internal server error: response truncated"); } else { fail(0, "response not yet fully received"); } } void HttpClientResponse::parse_content_encoding(string_view value) { content_encoding_ = util::ascii_tolower(value); } void HttpClientResponse::parse_content_length(string_view value) { int64_t code = sv::to_int64(value); if ((code < 0) || (code > INT_MAX)) { fail(500, util::ss("internal server error: unparseable content-length: ", value)); } content_length_ = code; } void HttpClientResponse::parse_content_type(string_view value) { eng::string ctype = util::ascii_tolower(value); string_view ctview(ctype); mime_type_ = sv::trim(sv::read_to_sep(ctview, ';')); if (mime_type_.empty()) { fail(500, util::ss("internal server error: unparseable content-type: ", value)); return; } while (true) { string_view feature = sv::trim(sv::read_to_sep(ctview, ';')); if (feature.empty()) { return; } string_view ftype = sv::trim(sv::read_to_sep(feature, '=')); if (ftype == "charset") { charset_ = sv::trim(feature); } } } void HttpClientResponse::parse_location(string_view value) { location_ = url_decode(value); } void HttpClientResponse::parse_transfer_encoding(string_view value) { transfer_encoding_ = util::ascii_tolower(value); } void HttpClientResponse::parse_header(string_view header, string_view value) { if (header == "content-encoding") { parse_content_encoding(value); } else if (header == "content-length") { parse_content_length(value); } else if (header == "content-type") { parse_content_type(value); } else if (header == "location") { parse_location(value); } else if (header == "transfer-encoding") { parse_transfer_encoding(value); } else if (header == "content-range") { fail(406, util::ss("not acceptable: unsupported response header: ", header)); } } bool HttpClientResponse::parse_content_basic(std::string_view &view, bool closed) { if (content_length_ >= 0) { if (content_length_ > MAX_CONTENT_LENGTH) { fail(413, util::ss("payload too large: luprex limit=", MAX_CONTENT_LENGTH)); return false; } if (int(view.size()) < content_length_) { incomplete(closed); return false; } content_ = sv::read_nbytes(view, content_length_); } else { if (int64_t(view.size()) > MAX_CONTENT_LENGTH) { fail(413, util::ss("payload too large: luprex limit=", MAX_CONTENT_LENGTH)); return false; } if (!closed) { incomplete(closed); return false; } content_ = sv::read_nbytes(view, view.size()); } return true; } bool HttpClientResponse::parse_content_chunked(std::string_view &view, bool closed) { int64_t total_size = 0; std::vector chunks; while (true) { std::string_view chunk_header = sv::trim(sv::read_to_line(view)); if (sv::isnull(view)) { incomplete(closed); return false; } int64_t chunk_size = sv::to_hex64(chunk_header, -1); if (chunk_size < 0) { fail(500, "internal server error: unparseable chunk header"); return false; } if (chunk_size > MAX_CONTENT_LENGTH) { fail(413, util::ss("payload too large: luprex limit=", MAX_CONTENT_LENGTH)); return false; } if (chunk_size == 0) break; total_size += chunk_size; if (total_size > MAX_CONTENT_LENGTH) { fail(413, util::ss("payload too large: luprex limit=", MAX_CONTENT_LENGTH)); return false; } std::string_view chunk = sv::read_nbytes(view, chunk_size); if (int64_t(chunk.size()) != chunk_size) { incomplete(closed); return false; } std::string_view newline = sv::read_to_line(view); if (!newline.empty()) { fail(500, "internal server error: corrupted chunk encoding"); return false; } if (sv::isnull(view)) { incomplete(closed); return false; } chunks.push_back(chunk); } content_.resize(total_size); size_t offset = 0; for (string_view chunk : chunks) { content_.replace(offset, chunk.size(), chunk); offset += chunk.size(); } return true; } void HttpClientResponse::parse(const StreamBuffer *sb, bool closed) { // We're not going to modify the StreamBuffer at all. // Instead, we work entirely on a view. string_view view = sb->view(); // Get the status line. string_view status = sv::trim(sv::read_to_line(view)); if (sv::isnull(view)) { incomplete(closed); return; } // Parse the status line. string_view protoversion = sv::read_to_space(status); if (!sv::has_prefix(protoversion, "HTTP/")) { fail(500, util::ss("internal server error: status line appears corrupt")); return; } string_view scode = sv::read_to_space(status); int64_t code = sv::to_int64(scode, 0); if ((code < 100) || (code > 599)) { fail(500, util::ss("internal server error: invalid response code: ", scode)); return; } status_code_ = code; // Responses outside the range 200-299 are errors, // and therefore must store an error message. if ((code < 200) || (code > 299)) { if (status.empty()) { error_ = util::ss("error code ", code); } else { error_ = status; } } // Parse the headers. while (true) { string_view header = sv::read_to_line(view); if (sv::isnull(view)) { incomplete(closed); return; } if (header.empty()) { break; } eng::string command = util::ascii_tolower(sv::trim(sv::read_to_sep(header, ':'))); if (sv::isnull(header)) { fail(500, util::ss("internal server error: no colon in header line: ", command)); return; } if (!words_separated_by_dashes(command)) { fail(500, util::ss("internal server error: invalid header: ", command)); return; } parse_header(command, sv::trim(header)); } // Process the content using the transfer encoding. if (transfer_encoding_ == "") { if (!parse_content_basic(view, closed)) return; } else if (transfer_encoding_ == "chunked") { if (!parse_content_chunked(view, closed)) return; } else { fail(500, util::ss("unsupported transfer-encoding: ", transfer_encoding_)); return; } // Calculate the response length. response_length_ = sb->fill() - view.size(); // If it's not a redirect, disallow 'location'. if ((status_code_ < 300) || (status_code_ > 399)) { if (!location_.empty()) { fail(500, util::ss("internal server error: location specified, but result code not 300-399: ", code)); return; } } // If the server didn't specify content-type, make a guess. if (mime_type_.empty()) { if (sv::valid_utf8(content_)) { mime_type_ = "text/plain"; charset_ = "utf-8"; } else { mime_type_ = "application/octet-stream"; charset_ = ""; } } // If it's multipart, reject it. if (sv::has_prefix(mime_type_, "multipart/")) { fail(406, "not acceptable: multipart messages not supported"); return; } // If it's text, demand a reasonable charset. Otherwise, // ignore the charset. if (sv::has_prefix(mime_type_, "text/")) { if (charset_.empty() || (charset_ == "ascii")) { charset_ = "utf-8"; } if (charset_ != "utf-8") { fail(406, util::ss("not acceptable: charset not supported: ", charset_)); return; } } else { charset_.clear(); } // Uncompress the content. if ((content_encoding_ == "") || (content_encoding_ == "identity")) { } else { fail(406, util::ss("not acceptable: content-encoding not supported: ", content_encoding_)); return; } // If there's an error code, throw out the content. if ((status_code_ < 200) || (status_code_ > 299)) { mime_type_.clear(); charset_.clear(); content_.clear(); } } void HttpClientResponse::store(LuaStack &LS0, LuaSlot tab) const { LuaStack LS(LS0.state()); LS.newtable(tab); LS.rawset(tab, "responsecode", status_code_); if (!error_.empty()) { LS.rawset(tab, "error", error_); } if (!location_.empty()) { LS.rawset(tab, "location", location_); } if (!mime_type_.empty()) { LS.rawset(tab, "mimetype", mime_type_); LS.rawset(tab, "content", content_); } // Debugging fields. Do not use for lua programming. if (content_length_ >= 0) { LS.rawset(tab, "dbg_contentlength", content_length_); } if (!transfer_encoding_.empty()) { LS.rawset(tab, "dbg_transferencoding", transfer_encoding_); } if (!charset_.empty()) { LS.rawset(tab, "dbg_charset", charset_); } if (response_length_ != 0) { LS.rawset(tab, "dbg_responselength", response_length_); } } void HttpClientResponse::store_fail(LuaStack &LS, LuaSlot tab, int status_code, std::string_view error) { HttpClientResponse response; response.fail(status_code, error); response.store(LS, tab); } LuaDefine(http_fixurl, "url", "validate URL and repair minor flaws in the URL syntax") { LuaArg url; LuaRet fixed; LuaStack LS(L, url, fixed); ParsedURL parsed(LS.ckstring(url)); if (!parsed.valid) { luaL_error(L, "invalid URL, not fixable"); return LS.result(); } LS.set(fixed, parsed.str()); return LS.result(); } LuaDefine(http_request, "request", "|Takes an HTTP request in the form of a lua table." "|The table may contain these fields:" "|" "| method (ie, 'GET', 'POST', etc)" "| host (ie, 'google.com')" "| port (default: 443)" "| path (ie, '/index.html')" "| params (a table of url parameters)" "| verifycertificate (default: true)" "| url (ie, 'https://host:port/path.html?a=b&c=d')" "|" "|You can specify url components separately (host, port, path," "|and params), or you can specify the entire url as a unit. " "|If you specify components, they must not be url-encoded. " "|If you specify the url as a whole, it must already be url-encoded." "|" "|You can omit the port, in which case it defaults to the" "|standard https port. You can omit verifycertificate, in which" "|case it defaults to true. You can omit the method if the" "|method is implied by the function you called (eg, 'http.get')." "|" "|Note that unencrypted http is not supported - we only allow https." "|However, you can talk to a server that has a dummy certificate" "|by specifying verifycertificate=false." "|" "|This routine, http.request, returns a debug string for the " "|request. The debug string looks like the actual http headers" "|that would be sent.") { LuaArg tab; LuaRet str; LuaStack LS(L, tab, str); HttpClientRequest req; req.set_config(LS, tab); req.set_defaults(); eng::string error = req.check(); if (!error.empty()) { luaL_error(L, "%s", error.c_str()); return 0; } LS.set(str, req.DebugString()); return LS.result(); } LuaDefine(http_response, "response", "|Returns an HTTP response in the form of a lua table." "|The table will contain these important fields:" "|" "| responsecode - 3-digit HTTP response code." "| error - an error message, or nil if no error." "| content - on success, the content, as a string." "| mimetype - on success, the mime type of the content." "| location - for HTTP redirects, the target url." "|" "|If the mimetype is a text mimetype, then the content" "|is automatically converted to utf-8." "|" "|The table may also contain these debugging-only fields." "|" "| dbg_transferencoding - If there was a Transfer-Encoding header." "| dbg_contentlength - If there was a Content-length header." "| dbg_charset - Original character set for text mime types." "| dbg_responselength - Total bytes in the response." "|" "|None of the dbg fields is needed to understand the response." "|For example, consider dbg_charset. When text content is" "|passed to lua, the content is automatically converted to utf-8." "|So dbg_charset only tells you what the character set used" "|to be, before it was converted to utf-8." "|" "|If an http routine generates an error, that error will be" "|expressed as a status code. These locally-generated status" "|codes can be:" "|" "| 400 (bad request) - the request was malformed" "| 503 (service unavailable) - dns fail, connect fail, or ssl fail" "| 500 (internal server error)- the response contains invalid HTTP" "| 406 (not acceptable) - the response used a feature we don't support yet" "| 413 (payload too large) - we refuse to download something so big" "| 425 (can't resume) - reloaded a save game with a pending request" "|" "|In this case, the error message will be the stock" "|error message, followed by more information." "|" "|This routine, http.response, generates a response by parsing" "|an actual HTTP response string. This is for debugging only.") { LuaArg text; LuaRet tab; LuaStack LS(L, text, tab); HttpClientResponse resp; StreamBuffer sb; sb.write_bytes(LS.ckstring(text)); resp.parse(&sb, true); resp.store(LS, tab); return LS.result(); }