// // 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 "json.hpp" #include using string_view = std::string_view; struct { int code; const char *name; } status_codes[] = { // Commonly-used codes duplicated at front of list. { 200, "OK" }, { 201, "Created" }, { 404, "Not Found" }, { 400, "Bad Request" }, { 500, "Internal Server Error" }, // All valid codes, including the ones above. { 101, "Switching Protocols" }, { 102, "Processing" }, { 103, "Early Hints" }, { 200, "OK" }, { 201, "Created" }, { 202, "Accepted" }, { 203, "Non-Authoritative Information" }, { 204, "No Content" }, { 205, "Reset Content" }, { 206, "Partial Content" }, { 207, "Multi-Status" }, { 208, "Already Reported" }, { 300, "Multiple Choices" }, { 301, "Moved Permanently" }, { 302, "Found" }, { 303, "See Other" }, { 304, "Not Modified" }, { 305, "Use Proxy" }, { 306, "Switch Proxy" }, { 307, "Temporary Redirect" }, { 308, "Permanent Redirect" }, { 400, "Bad Request" }, { 401, "Unauthorized" }, { 402, "Payment Required" }, { 403, "Forbidden" }, { 404, "Not Found" }, { 405, "Method Not Allowed" }, { 406, "Not Acceptable" }, { 407, "Proxy Authentication Required" }, { 408, "Request Timeout" }, { 409, "Conflict" }, { 410, "Gone" }, { 411, "Length Required" }, { 412, "Precondition Failed" }, { 413, "Payload Too Large" }, { 414, "URI Too Long" }, { 415, "Unsupported Media Type" }, { 416, "Range Not Satisfiable" }, { 417, "Expectation Failed" }, { 418, "I'm a teapot" }, { 421, "Misdirected Request" }, { 422, "Unprocessable Entity" }, { 423, "Locked" }, { 424, "Failed Dependency" }, { 425, "Too Early" }, { 426, "Upgrade Required" }, { 428, "Precondition Required" }, { 429, "Too Many Requests" }, { 431, "Request Header Fields Too Large" }, { 451, "Unavailable For Legal Reasons" }, { 500, "Internal Server Error" }, { 501, "Not Implemented" }, { 502, "Bad Gateway" }, { 503, "Service Unavailable" }, { 504, "Gateway Timeout" }, { 505, "HTTP Version Not Supported" }, { 506, "Variant Also Negotiates" }, { 507, "Insufficient Storage" }, { 508, "Loop Detected" }, { 510, "Not Extended" }, { 511, "Network Authentication Required" }, { 0, "" }, }; static const char *status_code_to_string(int code) { for (int i = 0; status_codes[i].code != 0; i++) { if (status_codes[i].code == code) { return status_codes[i].name; } } switch (code / 100) { case 1: return "Unknown Informational Status"; case 2: return "Unknown Successful Status"; case 3: return "Unknown Redirect"; case 4: return "Unknown Client Error"; case 5: return "Unknown Server Error"; default: return "Unknown Error"; } } static int status_code_from_string(std::string_view s) { for (int i = 0; status_codes[i].code != 0; i++) { if (sv::case_insensitive_eq(status_codes[i].name, s)) { return status_codes[i].code; } } return 0; } // Our client always sends HTTP/1.1 queries, but it // accepts HTTP/1.0 responses. Our server accepts both protocols. static bool is_supported_protocol(string_view protocol) { return (protocol == "HTTP/1.0") || (protocol == "HTTP/1.1"); } // Our client can make GET, HEAD, and POST requests. static bool is_supported_client_method(string_view method) { return ((method == "GET") || (method == "HEAD") || (method == "POST")); } // Our server is only allowed to return certain status codes. static bool is_supported_server_status(int status) { if (status == 200) return true; if ((status >= 400) && (status <= 599)) return true; return false; } bool contains_newline(std::string_view s) { if (s.find('\n') != std::string_view::npos) return true; return false; } bool words_separated_by_dashes(string_view v) { while (true) { string_view word = sv::read_ascii_identifier(v); if (word.empty()) return false; if (v.empty()) return true; char c = v.front(); if (c != '-') return false; v.remove_prefix(1); } } // This doesn't check whether the mime type is actually // registered, obviously. It only checks that it's in // the desired notation. bool valid_mime_type(string_view method) { string_view part1 = sv::read_ascii_identifier(method); if (part1.empty()) return false; if (sv::zfront(method) != '/') return false; method.remove_prefix(1); while (true) { string_view word = sv::read_ascii_identifier(method); if (word.empty()) return false; if (method.empty()) return true; char c = method.front(); if ((c != '-') && (c != '.') && (c != '+')) return false; method.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; } } static void send_protocol(bool http11, StreamBuffer *sb) { if (http11) { sb->write_bytes("HTTP/1.1"); } else { sb->write_bytes("HTTP/1.0"); } } static void send_content_length_header(bool http11, int size, StreamBuffer *sb) { if (http11) { sb->write_bytes("Content-length: "); sb->ostream() << size; sb->write_bytes("\r\n"); } } static void send_content_type_header(bool http11, const eng::string &mime, StreamBuffer *sb) { sb->write_bytes("Content-type: "); sb->write_bytes(mime); if (http11) { if (sv::has_prefix(mime, "text/")) { sb->write_bytes(" ; charset=utf-8"); } } sb->write_bytes("\r\n"); } static void send_cache_control_header(bool http11, int max_age, StreamBuffer *sb) { if (http11) { if (max_age == 0) { sb->write_bytes("Cache-control: no-cache\r\n"); } else { sb->ostream() << "Cache-control: max-age=" << max_age << "\r\n"; } } else { sb->write_bytes("Pragma: no-cache\r\n"); } } static void send_connection_header(bool http11, bool keep_alive, StreamBuffer *sb) { if (http11) { if (keep_alive) { sb->write_bytes("Connection: keep-alive\r\n"); } else { sb->write_bytes("Connection: close\r\n"); } } } static void send_error_response(bool http11, int code, eng::string extrainfo, StreamBuffer *sb) { send_protocol(http11, sb); eng::string errstr = status_code_to_string(code); sb->ostream() << " " << code << " " << errstr; if ((!extrainfo.empty()) && (!contains_newline(extrainfo))) { sb->write_bytes(": "); sb->write_bytes(extrainfo); } sb->write_bytes("\r\n"); sb->write_bytes("Content-Type: text/plain\r\n"); sb->write_bytes("\r\n"); sb->write_bytes(errstr); if (!extrainfo.empty()) { sb->write_bytes(": "); sb->write_bytes(extrainfo); } } // HTTP 1.0: An entity body is included with a request message only when the request method calls for one. // The presence of an entity body in a request is signaled by the inclusion of a Content-Length header field // in the request message headers. HTTP/1.0 requests containing an entity body must include a valid // Content-Length header field. // // HTTP 1.1: The presence of a message-body in a request is signaled by the inclusion of a Content-Length or // Transfer-Encoding header field in the request's message-headers // static bool request_contains_content(int content_length, std::string_view transfer_encoding) { return (content_length >= 0) || (!transfer_encoding.empty()); } // HTTP 1.0: For response messages, whether or not an entity body is included with a message is dependent on // both the request method and the response code. All responses to the HEAD request method must not include a // body, even though the presence of entity header fields may lead one to believe they do. All 1xx (informational), // 204 (no content), and 304 (not modified) responses must not include a body. All other responses must include an // entity body or a Content-Length header field defined with a value of zero (0). // // HTTP 1.1: For response messages, whether or not a message-body is included with a message is dependent on both // the request method and the response status code (section 6.1.1). All responses to the HEAD request method MUST // NOT include a message-body, even though the presence of entity- header fields might lead one to believe they // do. All 1xx (informational), 204 (no content), and 304 (not modified) responses MUST NOT include a message-body. // All other responses do include a message-body, although it MAY be of zero length. // static bool response_contains_content(std::string_view method, int status) { if (method == "HEAD") return false; if ((status >= 100) && (status <= 199)) return false; if (status == 204) return false; if (status == 304) return false; return true; } // 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(); if (!sv::has_prefix(url, "/")) { 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; } } else { // Stick in some defaults for unspecified fields. host = "host"; proto = "https"; } // 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; } }; void HttpClientRequest::check_fail(string_view s) { if (check_fail_.empty()) { check_fail_ = s; } } 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()); } // 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("\r\n"); // Send the host header. sb->write_bytes("Host: "); send_host_and_port(host_, port_, sb); sb->write_bytes("\r\n"); // 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("\r\n"); // Add a user-agent header. Not sure why. sb->write_bytes("User-agent: Mozilla 5.0 (luprex)"); sb->write_bytes("\r\n"); // 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("\r\n"); } // Send the content length and the content type. if (content_assigned_) { send_content_length_header(true, content_.size(), sb); send_content_type_header(true, mime_type_, sb); } // Send the extra linebreak. sb->write_bytes("\r\n"); // Send the content. if (content_assigned_) { sb->write_bytes(content_); } } HttpClientRequest::HttpClientRequest() { verify_certificate_ = false; port_ = 0; content_assigned_ = false; request_id_ = 0; place_id_ = 0; thread_id_ = 0; } 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 (!is_supported_client_method(method)) { check_fail(util::ss("method not implemented: ", method, ".")); return; } if ((!method_.empty()) && (method_ != method)) { check_fail(util::ss("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()) { check_fail(util::ss("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))) { check_fail(util::ss("hostnames can only contain letters, digits, and hyphen: ", host)); return; } } if (!host_.empty()) { check_fail(util::ss("hostname specified twice: ", host_, " and ", host)); return; } host_ = host; } void HttpClientRequest::set_port(int port) { if ((port < 1) || (port > 65535)) { check_fail(util::ss("port must be between 1 and 65535: ", port)); return; } if (port_ != 0) { check_fail(util::ss("port specified twice: ", port_, " and ", port)); return; } port_ = port; } void HttpClientRequest::set_path(string_view path) { if (!sv::has_prefix(path, "/")) { check_fail(util::ss("path must start with slash")); return; } if (!path_.empty()) { check_fail(util::ss("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()) { check_fail(util::ss("url parameter specified twice: ", key)); return; } if (key.empty()) { check_fail(util::ss("parameter key cannot be empty")); return; } params_[key] = val; } void HttpClientRequest::set_url(string_view url) { ParsedURL parsed_url(url); if (!parsed_url.valid) { check_fail(util::ss("syntactically invalid URL: ", url)); return; } if (parsed_url.proto != "https") { check_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_mime_type(const eng::string &mime_type) { if (!valid_mime_type(mime_type)) { check_fail(util::ss("Not a valid mime type: ", mime_type)); return; } if (!mime_type_.empty()) { check_fail(util::ss("Mime type specified twice: ", mime_type_, " and ", mime_type)); return; } mime_type_ = mime_type; } void HttpClientRequest::set_content(const eng::string &content) { if (!content_.empty()) { check_fail(util::ss("Content specified twice")); return; } if (content_assigned_) { check_fail("Content specified twice."); return; } content_ = content; content_assigned_ = true; } void HttpClientRequest::set_verify_certificate(LuaCoreStack &LS, LuaSlot val) { if (!LS.isboolean(val)) { check_fail(util::ss("verifycertificate must be a boolean")); return; } set_verify_certificate(LS.ckboolean(val)); } void HttpClientRequest::set_method(LuaCoreStack &LS, LuaSlot val) { if (!LS.isstring(val)) { check_fail(util::ss("method must be a string")); return; } set_method(LS.ckstring(val)); } void HttpClientRequest::set_host(LuaCoreStack &LS, LuaSlot val) { if (!LS.isstring(val)) { check_fail(util::ss("host must be a string")); return; } set_host(LS.ckstring(val)); } void HttpClientRequest::set_port(LuaCoreStack &LS, LuaSlot val) { if (!LS.isint(val)) { check_fail(util::ss("port must be an int")); return; } set_port(LS.ckint(val)); } void HttpClientRequest::set_path(LuaCoreStack &LS, LuaSlot val) { if (!LS.isstring(val)) { check_fail(util::ss("path must be a string")); return; } set_path(LS.ckstring(val)); } void HttpClientRequest::set_param(LuaCoreStack &LS, LuaSlot key, LuaSlot val) { if (!LS.isstring(key)) { check_fail(util::ss("url parameter key must be a string")); return; } if (!LS.isstring(val)) { check_fail(util::ss("url parameter val must be a string")); return; } set_param(LS.ckstring(key), LS.ckstring(val)); } void HttpClientRequest::set_params(LuaCoreStack &LS0, LuaSlot tab) { if (!LS0.istable(tab)) { check_fail(util::ss("params must be a table")); return; } LuaVar key, val; LuaOldStack LS(LS0.state(), key, val); LS.set(key, LuaNil); while (LS.next(tab, key, val)) { set_param(LS, key, val); } } void HttpClientRequest::set_url(LuaCoreStack &LS, LuaSlot val) { if (!LS.isstring(val)) { check_fail(util::ss("url must be a string")); return; } set_url(LS.ckstring(val)); } void HttpClientRequest::set_mime_type(LuaCoreStack &LS, LuaSlot val) { if (!LS.isstring(val)) { check_fail(util::ss("mime type must be a string")); return; } set_mime_type(LS.ckstring(val)); } void HttpClientRequest::set_content(LuaCoreStack &LS, LuaSlot val) { if (!LS.isstring(val)) { check_fail(util::ss("content must be a string")); return; } set_content(LS.ckstring(val)); } void HttpClientRequest::set_jsonvalue(LuaCoreStack &LS, LuaSlot val) { eng::string out; eng::string err = json::encode(LS, val, out, false, HttpParser::MAX_CONTENT_LENGTH); if (!err.empty()) { check_fail(util::ss("json encode failure: ", err)); return; } set_content(out); set_mime_type("application/json"); } void HttpClientRequest::set_defaults() { if (method_.empty()) { method_ = "GET"; } if (port_ == 0) { port_ = 443; } } void HttpClientRequest::configure(LuaKeywordParser &kp) { LuaVar val; LuaOldStack LS(kp.state(), val); if (kp.parse(val, "method")) { set_method(LS, val); } if (kp.parse(val, "host")) { set_host(LS, val); } if (kp.parse(val, "port")) { set_port(LS, val); } if (kp.parse(val, "path")) { set_path(LS, val); } if (kp.parse(val, "params")) { set_params(LS, val); } if (kp.parse(val, "url")) { set_url(LS, val); } if (kp.parse(val, "verifycertificate")) { set_verify_certificate(LS, val); } if (kp.parse(val, "mimetype")) { set_mime_type(LS, val); } if (kp.parse(val, "content")) { set_content(LS, val); } if (kp.parse(val, "html")) { set_content(LS, val); set_mime_type("text/html"); } if (kp.parse(val, "text")) { set_content(LS, val); set_mime_type("text/plain"); } if (kp.parse(val, "json")) { set_content(LS, val); set_mime_type("application/json"); } if (kp.parse(val, "bytes")) { set_content(LS, val); set_mime_type("application/octet-stream"); } if (kp.parse(val, "jsonvalue")) { set_jsonvalue(LS, val); } } eng::string HttpClientRequest::target() const { assert(check().empty()); eng::ostringstream oss; oss << (verify_certificate_ ? "cert" : "nocert"); oss << ':' << host_ << ':' << port_; return oss.str(); } eng::string HttpClientRequest::check() const { if (!check_fail_.empty()) { return check_fail_; } if (method_.empty()) { return "method has not been set"; } if (host_.empty()) { return "host has not been set"; } if (port_ == 0) { return "port has not been set"; } if (path_.empty()) { return "url has not been set"; } if (method_ == "POST") { if (!content_assigned_) { return "content not set for POST request"; } if (mime_type_.empty()) { return "mime type not set for POST request"; } } return ""; } void HttpClientRequest::serialize(StreamBuffer *sb) const { sb->write_int64(request_id_); sb->write_int64(place_id_); sb->write_int64(thread_id_); sb->write_string(check_fail_); 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(); check_fail_ = 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::debug_string() { StreamBuffer sb; send_internal(&sb, true); return eng::string(sb.view()); } void HttpServerResponse::check_fail(string_view s) { if (check_fail_.empty()) { check_fail_ = s; } } void HttpServerResponse::send_internal(StreamBuffer *sb, bool debug_string) const { eng::string error = check(); if (!error.empty()) { send_error_response(http11_, 500, error, sb); return; } // Determine if we're going to be sending content. bool contains_content = response_contains_content("GET", status_); // The status message is the human-readable status code. eng::string statusmsg = status_code_to_string(status_); // Annotate error messages. const eng::string *content = &content_; if (errstatus() && contains_content && (mime_type_ == "text/plain") && !contains_newline(content_)) { if (sv::has_prefix(content_, statusmsg)) { statusmsg = content_; } else if (!content_.empty()) { statusmsg += ": "; statusmsg += content_; } content = &statusmsg; } // Send the status line. send_protocol(http11_, sb); sb->ostream() << " " << status_ << " " << statusmsg << "\r\n"; // Send the connection header. if (!errstatus()) { send_connection_header(http11_, keep_alive_, sb); } // Send the headers that make sense if we're sending content. if (contains_content) { if (!errstatus()) { send_cache_control_header(http11_, max_age_, sb); } send_content_length_header(http11_, content->size(), sb); send_content_type_header(http11_, mime_type_, sb); } // Send the extra linebreak. sb->write_bytes("\r\n"); // Send the content. if (contains_content) { sb->write_bytes(*content); } } HttpServerResponse::HttpServerResponse() { status_ = 0; max_age_ = 0; keep_alive_ = false; content_assigned_ = false; http11_ = false; } void HttpServerResponse::set_status(int status) { if (!is_supported_server_status(status)) { check_fail(util::ss("status code not supported yet: ", status)); return; } if (status_ != 0) { check_fail(util::ss("status specified twice: ", status_, " and ", status)); return; } status_ = status; } void HttpServerResponse::set_max_age(int max_age) { if (max_age < 0) { check_fail(util::ss("max age must be a positive integer: ", max_age)); return; } if (max_age_ != 0) { check_fail(util::ss("max age specified twice: ", max_age_, " and ", max_age)); return; } max_age_ = max_age; } void HttpServerResponse::set_mime_type(const eng::string &mime_type) { if (!valid_mime_type(mime_type)) { check_fail(util::ss("mime type not syntactically valid: ", mime_type)); return; } if (!mime_type_.empty()) { check_fail(util::ss("mime type specified twice: ", mime_type_, " and ", mime_type)); return; } mime_type_ = mime_type; } void HttpServerResponse::set_content(const eng::string &content) { if (content_assigned_) { check_fail(util::ss("content specified twice")); return; } content_ = content; content_assigned_ = true; } void HttpServerResponse::set_status(LuaCoreStack &LS, LuaSlot val) { int status = 0; if (LS.isstring(val)) { eng::string s = LS.ckstring(val); status = status_code_from_string(s); if (status == 0) { check_fail(util::ss("unrecognized status code: ", s)); return; } } else if (LS.isint(val)) { status = LS.ckint(val); } else { check_fail(util::ss("status must be an integer")); return; } set_status(status); } void HttpServerResponse::set_max_age(LuaCoreStack &LS, LuaSlot val) { if (!LS.isint(val)) { check_fail(util::ss("max-age must be an int")); return; } set_max_age(LS.ckint(val)); } void HttpServerResponse::set_mime_type(LuaCoreStack &LS, LuaSlot val) { if (!LS.isstring(val)) { check_fail(util::ss("mime type must be a string")); return; } set_mime_type(LS.ckstring(val)); } void HttpServerResponse::set_content(LuaCoreStack &LS, LuaSlot val) { if (!LS.isstring(val)) { check_fail(util::ss("content must be a string")); return; } set_content(LS.ckstring(val)); } void HttpServerResponse::set_jsonvalue(LuaCoreStack &LS, LuaSlot val) { eng::string out; eng::string err = json::encode(LS, val, out, false, HttpParser::MAX_CONTENT_LENGTH); if (!err.empty()) { check_fail(util::ss("json encode failure: ", err)); return; } set_content(out); set_mime_type("application/json"); } void HttpServerResponse::configure(LuaKeywordParser &kp) { LuaVar val; LuaOldStack LS(kp.state(), val); if (kp.parse(val, "status")) { set_status(LS, val); } if (kp.parse(val, "maxage")) { set_max_age(LS, val); } if (kp.parse(val, "mimetype")) { set_mime_type(LS, val); } if (kp.parse(val, "content")) { set_content(LS, val); } if (kp.parse(val, "html")) { set_content(LS, val); set_mime_type("text/html"); } if (kp.parse(val, "text")) { set_content(LS, val); set_mime_type("text/plain"); } if (kp.parse(val, "json")) { set_content(LS, val); set_mime_type("application/json"); } if (kp.parse(val, "bytes")) { set_content(LS, val); set_mime_type("application/octet-stream"); } if (kp.parse(val, "jsonvalue")) { set_jsonvalue(LS, val); } } void HttpServerResponse::fail(int status, const eng::string &errmessage) { status_ = status; content_ = errmessage; content_assigned_ = true; mime_type_ = "text/plain"; max_age_ = 0; } void HttpServerResponse::set_defaults() { // If you specified content, and didn't specify // a status code, then assume it's a success. if ((status_ == 0) && (content_assigned_)) { status_ = 200; } // If the response status indicates that we're sending // content, and there's no content, generate blank content. if (response_contains_content("GET", status_) && (!content_assigned_) && (mime_type_.empty())) { content_assigned_ = true; content_ = ""; mime_type_ = "text/plain"; } } eng::string HttpServerResponse::check() const { if (!check_fail_.empty()) { return check_fail_; } // There needs to be a status code. if (status_ == 0) { return "status code not specified"; } // If you assigned a mime type and didn't specify // content, that's bad. if ((!content_assigned_) && (!mime_type_.empty())) { return "mime type specified without content"; } // If you assigned content, but didn't assign // a mime type, that's bad. if (content_assigned_ && (mime_type_.empty())) { return "content specified without mime type"; } return ""; } eng::string HttpServerResponse::debug_string() { StreamBuffer sb; send_internal(&sb, true); return eng::string(sb.view()); } HttpParser::HttpParser() { request_id_ = 0; is_request_ = false; status_ = 0; mime_type_ = ""; content_length_ = -1; comm_length_ = 0; http11_ = false; } eng::string HttpParser::first_path_component(std::string_view defval) const { std::string_view v = path_; assert(sv::zfront(v) == '/'); v.remove_prefix(1); std::string_view first = sv::read_to_sep(v, '/'); if (first.empty()) { return eng::string(defval); } else { return eng::string(first); } } void HttpParser::fail(int code, std::string_view message) { status_ = code; error_ = message; mime_type_ = ""; charset_ = ""; content_ = ""; } void HttpParser::syntax(std::string_view detail) { if (is_request_) { fail(400, util::ss("Bad Request: ", detail)); } else { fail(500, util::ss("Bad Response: ", detail)); } } void HttpParser::incomplete(bool closed) { if (closed) { syntax("response truncated"); } else { fail(0, "response not yet fully received"); } } void HttpParser::oversized() { fail(413, util::ss("Payload Too Large: Limit=", MAX_CONTENT_LENGTH)); } bool HttpParser::parse_request_line(std::string_view &view, bool closed) { // Extract the request line. // string_view request = sv::trim(sv::read_to_line(view)); if (sv::isnull(view)) { incomplete(closed); return false; } // Break down the request line. // eng::string method = util::ascii_toupper(sv::read_to_space(request)); string_view path = sv::read_to_space(request); eng::string protocol = util::ascii_toupper(sv::read_to_space(request)); if ((!request.empty()) || (protocol.empty())) { syntax("invalid request line"); return false; } if (method != "GET") { fail(405, util::ss("Method Not Allowed: ", method)); return false; } if (!is_supported_protocol(protocol)) { fail(505, util::ss("HTTP Version Not Supported: ", protocol)); return false; } http11_ = (protocol == "HTTP/1.1"); // Parse the url. // ParsedURL url(path); if (!url.valid) { syntax(util::ss("invalid URL path: ", path)); return false; } method_ = method; path_ = url.path; params_ = url.params; return true; } bool HttpParser::parse_status_line(std::string_view &view, bool closed) { // Extract the status line. // string_view status = sv::trim(sv::read_to_line(view)); if (sv::isnull(view)) { incomplete(closed); return false; } // Get the protocol version from the response line. // string_view protoversion = sv::read_to_space(status); if (!is_supported_protocol(protoversion)) { syntax(util::ss("unsupported protocol: ", protoversion)); return false; } http11_ = (protoversion == "HTTP/1.1"); // Get the status code from the response line. // string_view scode = sv::read_to_space(status); int64_t code = sv::to_int64(scode, 0); if ((code < 100) || (code > 599)) { syntax(util::ss("invalid response code: ", scode)); return false; } status_ = code; // Responses outside the range 200-299 are errors, // and therefore must store a nonempty error message. // if ((code < 200) || (code > 299)) { if (status.empty()) { error_ = status_code_to_string(code); } else { error_ = status; } } return true; } void HttpParser::parse_content_encoding(string_view value) { content_encoding_ = util::ascii_tolower(value); } void HttpParser::parse_content_length(string_view value) { int64_t code = sv::to_int64(value); if ((code < 0) || (code > INT_MAX)) { syntax(util::ss("unparseable content-length: ", value)); } content_length_ = code; } void HttpParser::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()) { syntax(util::ss("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 HttpParser::parse_location(string_view value) { location_ = url_decode(value); } void HttpParser::parse_transfer_encoding(string_view value) { transfer_encoding_ = util::ascii_tolower(value); } void HttpParser::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(416, util::ss("Range Not Satisfiable: unsupported header: ", header)); } } bool HttpParser::parse_headers(std::string_view &view, bool closed) { // Parse the headers. while (true) { string_view header = sv::read_to_line(view); if (sv::isnull(view)) { incomplete(closed); return false; } if (header.empty()) { return true; } eng::string command = util::ascii_tolower(sv::trim(sv::read_to_sep(header, ':'))); if (sv::isnull(header)) { syntax(util::ss("no colon in header line: ", command)); return false; } if (!words_separated_by_dashes(command)) { syntax(util::ss("invalid header: ", command)); return false; } parse_header(command, sv::trim(header)); } } bool HttpParser::parse_content_basic(std::string_view &view, bool closed) { if (content_length_ >= 0) { if (content_length_ > MAX_CONTENT_LENGTH) { oversized(); 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) { oversized(); return false; } if (!closed) { incomplete(closed); return false; } content_ = sv::read_nbytes(view, view.size()); } return true; } bool HttpParser::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) { syntax("unparseable chunk header"); return false; } if (chunk_size > MAX_CONTENT_LENGTH) { oversized(); return false; } if (chunk_size == 0) break; total_size += chunk_size; if (total_size > MAX_CONTENT_LENGTH) { oversized(); 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()) { syntax("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; } bool HttpParser::parse_content(std::string_view &view, bool closed) { // If there's no body, just return true. if (is_request_) { if (!request_contains_content(content_length_, transfer_encoding_)) { return true; } } else { if (!response_contains_content(method_, status_)) { return true; } } // Parse the content. if (transfer_encoding_ == "") { if (!parse_content_basic(view, closed)) { return false; } } else if (transfer_encoding_ == "chunked") { if (!parse_content_chunked(view, closed)) { return false; } } else { syntax(util::ss("unsupported transfer-encoding: ", transfer_encoding_)); return false; } // Uncompress the content. if ((content_encoding_ == "") || (content_encoding_ == "identity")) { } else { syntax(util::ss("content-encoding not supported: ", content_encoding_)); return true; } // If the sender didn't specify content-type, make a guess based on the content. if (mime_type_.empty()) { if (sv::valid_utf8(content_)) { mime_type_ = "text/html"; charset_ = "utf-8"; } else { mime_type_ = "application/octet-stream"; charset_ = ""; } } // Switch the charset to utf-8, if it's text. if (sv::has_prefix(mime_type_, "text/")) { if (charset_.empty() || (charset_ == "ascii") || (charset_ == "utf-8")) { // we're already good. } else { // We can't convert charsets yet. syntax(util::ss("charset not supported: ", charset_)); return true; } } else { // Not text. No need to specify charset. charset_.clear(); } return true; } void HttpParser::store(LuaCoreStack &LS0, LuaSlot tab) const { LuaVar ptab, djson; LuaOldStack LS(LS0.state(), ptab, djson); LS.newtable(tab); if (!is_request_) { LS.rawset(tab, "status", status_); } if (!error_.empty()) { LS.rawset(tab, "error", error_); } if (!location_.empty()) { LS.rawset(tab, "location", location_); } if (!mime_type_.empty() || !content_.empty()) { LS.rawset(tab, "mimetype", mime_type_); LS.rawset(tab, "content", content_); if (mime_type_ == "application/json") { json::decode(LS, djson, content_); LS.rawset(tab, "jsonvalue", djson); } } // We currently don't store the method, because // our server only supports GET. Even if we // do support more methods in the future, are // unlikely to want to handle this from inside lua. // // if (!method_.empty()) { // LS.rawset(tab, "method", method_); // } if (!path_.empty()) { LS.rawset(tab, "path", path_); LS.newtable(ptab); LS.rawset(tab, "params", ptab); for (const auto &pair : params_) { LS.rawset(ptab, pair.first, pair.second); } } // 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 (comm_length_ != 0) { LS.rawset(tab, "dbg_commlength", comm_length_); } } eng::string HttpParser::debug_string() const { eng::ostringstream oss; if (request_id_ != 0) { oss << " request_id: " << request_id_ << std::endl; } oss << " status_code: " << status_ << std::endl; oss << " error: " << error_ << std::endl; if (content_length_ >= 0) { oss << " content_length: " << content_length_ << std::endl; } if (!transfer_encoding_.empty()) { oss << " transfer_encoding: " << transfer_encoding_ << std::endl; } if (!location_.empty()) { oss << " location: " << location_ << std::endl; } if (!mime_type_.empty()) { oss << " mime_type: " << mime_type_ << std::endl; } if (!charset_.empty()) { oss << " charset: " << charset_ << std::endl; } if (!content_.empty()) { oss << " content: " << content_ << std::endl; } if (!method_.empty()) { oss << " method: " << method_ << std::endl; } if (!path_.empty()) { oss << " path: " << path_ << std::endl; } for (const auto &pair : params_) { oss << " param: " << pair.first << "=" << pair.second << std::endl; } if (comm_length_ > 0) { oss << " comm_length: " << comm_length_ << std::endl; } return oss.str(); } void HttpParser::parse_response(std::string_view view, bool closed, std::string_view method) { std::string_view original_view = view; is_request_ = false; method_ = method; // Parse the status line. if (!parse_status_line(view, closed)) { return; } // Parse the headers. if (!parse_headers(view, closed)) { return; } // Process the content. if (!parse_content(view, closed)) { return; } // Calculate the response length. comm_length_ = original_view.size() - view.size(); // If it's not a redirect, ignore location. if ((status_ < 300) || (status_ > 399)) { location_.clear(); } // If it's multipart, reject it. if (sv::has_prefix(mime_type_, "multipart/")) { syntax("multipart messages not supported"); return; } } void HttpParser::parse_request(std::string_view view, bool closed) { std::string_view original_view = view; is_request_ = true; // Parse the request line. if (!parse_request_line(view, closed)) { return; } // Parse the headers. if (!parse_headers(view, closed)) { return; } // Process the content, if any. if (!parse_content(view, closed)) { return; } // Calculate the comm length. comm_length_ = original_view.size() - view.size(); // Always ignore location. location_.clear(); // If it's multipart, reject it. if (sv::has_prefix(mime_type_, "multipart/")) { syntax("multipart messages not supported"); return; } // If we've made it this far, and there's no // status code, set it to 200 OK. if (status_ == 0) status_ = 200; } void HttpParser::store_fail(LuaCoreStack &LS, LuaSlot tab, int status_code, std::string_view error) { HttpParser parser; parser.fail(status_code, error); parser.store(LS, tab); } 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; } } LuaDefine(http_fixurl, "url", "validate URL and repair minor flaws in the URL syntax") { LuaArg url; LuaRet fixed; LuaOldStack 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_clientrequest, "request", "|Takes an HTTP client 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)" "| content (the body to send to the server, if any)" "| mimetype (mime type of the body, if any)" "|" "|The table can also contain this field, which sets" "|host, port, path, and params in a single directive." "|" "| url (ie, 'https://host:port/path.html?a=b&c=d')" "|" "|The table can also contain these fields, which set" "|both the content and the mimetype in a single directive:" "|" "| html - content as a string (text/html)" "| text - content as a string (text/plain)" "| json - content as a string (application/json)" "| bytes - content as a string (application/octet-stream)" "|" "|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.clientrequest, returns a debug string for the " "|request. The debug string looks like the actual http headers" "|that would be sent.") { LuaArg tab; LuaRet str; LuaOldStack LS(L, tab, str); LuaKeywordParser kp(LS, tab); HttpClientRequest req; req.configure(kp); kp.final_check_throw(); req.set_defaults(); eng::string error = req.check(); if (!error.empty()) { luaL_error(L, "%s", error.c_str()); return 0; } LS.set(str, req.debug_string()); return LS.result(); } LuaDefine(http_clientresponse, "response", "|Returns an HTTP client response in the form of a lua table." "|The table will contain these important fields:" "|" "| status - 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_commlength - Total bytes in the communication." "|" "|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" "| 413 (payload too large) - we refuse to download something so big" "| 425 (can't resume) - reloaded a save game with a pending request" "|" "|Error messages that are generated locally consist of " "|the standard message (eg, 'bad request') followed by more " "|detailed information." "|" "|This routine, http.clientresponse, generates a response by parsing" "|an actual HTTP response string. This is for debugging only.") { LuaArg text; LuaRet tab; LuaOldStack LS(L, text, tab); HttpParser parser; parser.parse_response(LS.ckstring(text), true, "GET"); parser.store(LS, tab); return LS.result(); } LuaDefine(http_serverrequest, "request", "|Returns an HTTP server request in the form of a lua table." "|The table will contain these important fields:" "|" "| status - 3-digit HTTP response code." "| error - an error message, or nil if no error." "| method - GET, HEAD, or POST" "| path - the url-decoded path, eg, '/index.html'" "| params - a table of url-decoded URL parameters" "| content - the content, as a string (POST only)" "| mimetype - the mime type of the content (POST only)" "|" "|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_commlength - Total bytes in the communication." "|" "|None of the dbg fields is needed to understand the request." "|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." "|" "|When the engine is functioning as a webserver, bad requests " "|are never passed to lua. Therefore, a request that is passed " "|to lua will always contain status=200 and error=nil. However, " "|when debugging server requests using http.serverrequest, " "|it is possible to see certain other errors:" "|" "| 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" "| 413 (payload too large) - we refuse to download something so big" "| 425 (can't resume) - reloaded a save game with a pending request" "|" "|Error messages that are generated locally consist of " "|the standard message (eg, 'bad request') followed by more " "|detailed information." "|" "|This routine, http.serverrequest, generates a request by parsing" "|an actual HTTP request string. This is for debugging only.") { LuaArg text; LuaRet tab; LuaOldStack LS(L, text, tab); HttpParser parser; parser.parse_request(LS.ckstring(text), true); parser.store(LS, tab); return LS.result(); } LuaDefine(http_serverresponse, "response", "|Takes an HTTP server response in the form of a lua table." "|The table may contain these fields:" "|" "| status - status result" "| content - content as a string" "| mimetype - mime type of the content" "| maxage - enables client-side caching" "|" "|The table can also contain these fields, which set" "|both the content and the mimetype in a single directive:" "|" "| html - content as a string (text/html)" "| text - content as a string (text/plain)" "| json - content as a string (application/json)" "| bytes - content as a string (application/octet-stream)" "|" "|To send a successful response to a GET or HEAD request," "|you must supply content and mimetype (you can supply both" "|in a single field, if desired). We recommend omitting the" "|status code." "|" "|To send a successful response to a POST request, you only" "|need to supply status=201 or status='Created'." "|" "|To send an error response, you must supply a status" "|code indicating the error. The status must be one of the" "|standard HTTP status codes, expressed either as a 3-digit" "|number (eg, 404), or a string (eg, 'Not Found')." "|" "|Not every HTTP status code is currently supported:" "|" "| * Success codes 200 and 201 are supported" "| * Other success codes (202-299) are not supported" "| * Informational codes (100-199) are not supported" "| * Redirects (300-399) are not supported" "| * All error codes (400-599) are supported" "|" "|By default, this server asks the client not to cache" "|anything. If you specify maxage, however, then you" "|are giving the client permission to cache." "|" "|If the lua server throws an error, or returns an" "|invalid response, the error will be reported through" "|the browser, as a 500 Internal Server Error." "|" "|This routine, http.serverresponse, returns a debug string for the " "|response. The debug string looks like the actual http response" "|that would be sent.") { LuaArg tab; LuaRet str; LuaOldStack LS(L, tab, str); LuaKeywordParser kp(LS, tab); HttpServerResponse resp; resp.configure(kp); kp.final_check_throw(); resp.set_defaults(); LS.set(str, resp.debug_string()); return LS.result(); } LuaDefine(http_validmime, "(mt)", "") { LuaArg str; LuaRet ok; LuaOldStack LS(L, str, ok); LS.set(ok, valid_mime_type(LS.ckstring(str))); return LS.result(); } LuaDefine(http_statusstring, "(statuscode)", "Convert a 3-digit status code to a string") { LuaArg code; LuaRet str; LuaOldStack LS(L, code, str); int icode = LS.ckint(code); LS.set(str, status_code_to_string(icode)); return LS.result(); } LuaDefine(http_statuscode, "(statusstring)", "Convert a string to a 3-digit status code") { LuaArg str; LuaRet code; LuaOldStack LS(L, code, str); eng::string sstr = LS.ckstring(str); LS.set(code, status_code_from_string(sstr)); int iresult = LS.result(); return iresult; }