From ca271b8db118c41e95ea6e5216ed31ead055094e Mon Sep 17 00:00:00 2001 From: jyelon Date: Fri, 20 May 2022 17:12:58 -0400 Subject: [PATCH] HTTP server functionality is in there. --- luprex/core/cpp/http.cpp | 863 +++++++++++++++++++++++------ luprex/core/cpp/http.hpp | 156 +++++- luprex/core/cpp/lpxserver.cpp | 4 +- luprex/core/cpp/luastack.hpp | 3 +- luprex/core/cpp/util.cpp | 12 + luprex/core/cpp/util.hpp | 4 + luprex/core/cpp/world-accessor.cpp | 4 +- luprex/core/cpp/world-core.cpp | 85 +++ luprex/core/cpp/world.hpp | 9 + 9 files changed, 932 insertions(+), 208 deletions(-) diff --git a/luprex/core/cpp/http.cpp b/luprex/core/cpp/http.cpp index 01c6fdd9..8e803e16 100644 --- a/luprex/core/cpp/http.cpp +++ b/luprex/core/cpp/http.cpp @@ -14,14 +14,132 @@ using string_view = std::string_view; -bool is_supported_protocol(string_view protocol) { +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"); } -bool is_supported_method(string_view method) { +// 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); @@ -134,9 +252,75 @@ static void send_host_and_port(std::string_view host, int port, StreamBuffer *sb } } +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); + } +} + // In a properly-formed url, the hostname and path are url encoded. // This parser expects an encoded URL. - struct ParsedURL { public: bool valid; @@ -218,26 +402,77 @@ public: } }; -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; +void HttpClientRequest::check_fail(string_view s) { + if (check_fail_.empty()) { + check_fail_ = 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::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"); + } + + // If it's a post request, send the content length and the content type. + if (method_ == "POST") { + 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"); + + // If it's a post request, send the content. + if (method_ == "POST") { + 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) { @@ -246,12 +481,12 @@ void HttpClientRequest::set_verify_certificate(bool flag) { void HttpClientRequest::set_method(const eng::string &s) { eng::string method = util::ascii_toupper(s); - if (!is_supported_method(method)) { - fail(util::ss("HTTP method not implemented: ", method, ".")); + if (!is_supported_client_method(method)) { + check_fail(util::ss("method not implemented: ", method, ".")); return; } if ((!method_.empty()) && (method_ != method)) { - fail(util::ss("HTTP method specified twice: ", method_, " and ", method)); + check_fail(util::ss("method specified twice: ", method_, " and ", method)); return; } method_ = method; @@ -260,19 +495,19 @@ void HttpClientRequest::set_method(const eng::string &s) { 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.")); + 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))) { - fail(util::ss("HTTP hostnames can only contain letters, digits, and hyphen: ", host)); + check_fail(util::ss("hostnames can only contain letters, digits, and hyphen: ", host)); return; } } if (!host_.empty()) { - fail(util::ss("HTTP hostname specified twice: ", host_, " and ", host)); + check_fail(util::ss("hostname specified twice: ", host_, " and ", host)); return; } host_ = host; @@ -280,11 +515,11 @@ void HttpClientRequest::set_host(const eng::string &s) { void HttpClientRequest::set_port(int port) { if ((port < 1) || (port > 65535)) { - fail(util::ss("HTTP port must be between 1 and 65535: ", port)); + check_fail(util::ss("port must be between 1 and 65535: ", port)); return; } if (port_ != 0) { - fail(util::ss("HTTP port specified twice: ", port_, " and ", port)); + check_fail(util::ss("port specified twice: ", port_, " and ", port)); return; } port_ = port; @@ -292,11 +527,11 @@ void HttpClientRequest::set_port(int port) { void HttpClientRequest::set_path(string_view path) { if (!sv::has_prefix(path, "/")) { - fail(util::ss("HTTP path must start with slash")); + check_fail(util::ss("path must start with slash")); return; } if (!path_.empty()) { - fail(util::ss("HTTP path specified twice: ", path_, " and ", path)); + check_fail(util::ss("path specified twice: ", path_, " and ", path)); return; } path_ = path; @@ -304,11 +539,11 @@ void HttpClientRequest::set_path(string_view 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)); + check_fail(util::ss("url parameter specified twice: ", key)); return; } if (key.empty()) { - fail(util::ss("HTTP parameter key cannot be empty")); + check_fail(util::ss("parameter key cannot be empty")); return; } params_[key] = val; @@ -317,11 +552,11 @@ void HttpClientRequest::set_param(const eng::string &key, const eng::string &val void HttpClientRequest::set_url(string_view url) { ParsedURL parsed_url(url); if (!parsed_url.valid) { - fail(util::ss("syntactically invalid URL: ", url)); + check_fail(util::ss("syntactically invalid URL: ", url)); return; } if (parsed_url.proto != "https") { - fail(util::ss("unsupported protocol: ", parsed_url.proto)); + check_fail(util::ss("unsupported protocol: ", parsed_url.proto)); return; } set_host(parsed_url.host); @@ -334,11 +569,11 @@ void HttpClientRequest::set_url(string_view url) { void HttpClientRequest::set_mime_type(const eng::string &mime_type) { if (!valid_mime_type(mime_type)) { - fail(util::ss("Not a valid mime type: ", mime_type)); + check_fail(util::ss("Not a valid mime type: ", mime_type)); return; } if (!mime_type_.empty()) { - fail(util::ss("Mime type specified twice: ", mime_type_, " and ", mime_type)); + check_fail(util::ss("Mime type specified twice: ", mime_type_, " and ", mime_type)); return; } mime_type_ = mime_type; @@ -346,15 +581,20 @@ void HttpClientRequest::set_mime_type(const eng::string &mime_type) { void HttpClientRequest::set_content(const eng::string &content) { if (!content_.empty()) { - fail(util::ss("Content specified twice")); + 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(LuaStack &LS, LuaSlot val) { if (!LS.isboolean(val)) { - fail(util::ss("HTTP verify_certificate must be a boolean")); + check_fail(util::ss("verifycertificate must be a boolean")); return; } set_verify_certificate(LS.ckboolean(val)); @@ -362,7 +602,7 @@ void HttpClientRequest::set_verify_certificate(LuaStack &LS, LuaSlot val) { void HttpClientRequest::set_method(LuaStack &LS, LuaSlot val) { if (!LS.isstring(val)) { - fail(util::ss("HTTP method must be a string")); + check_fail(util::ss("method must be a string")); return; } set_method(LS.ckstring(val)); @@ -370,7 +610,7 @@ void HttpClientRequest::set_method(LuaStack &LS, LuaSlot val) { void HttpClientRequest::set_host(LuaStack &LS, LuaSlot val) { if (!LS.isstring(val)) { - fail(util::ss("HTTP host must be a string")); + check_fail(util::ss("host must be a string")); return; } set_host(LS.ckstring(val)); @@ -378,7 +618,7 @@ void HttpClientRequest::set_host(LuaStack &LS, LuaSlot val) { void HttpClientRequest::set_port(LuaStack &LS, LuaSlot val) { if (!LS.isint(val)) { - fail(util::ss("HTTP port must be an int")); + check_fail(util::ss("port must be an int")); return; } set_port(LS.ckint(val)); @@ -386,7 +626,7 @@ void HttpClientRequest::set_port(LuaStack &LS, LuaSlot val) { void HttpClientRequest::set_path(LuaStack &LS, LuaSlot val) { if (!LS.isstring(val)) { - fail(util::ss("HTTP path must be a string")); + check_fail(util::ss("path must be a string")); return; } set_path(LS.ckstring(val)); @@ -394,11 +634,11 @@ void HttpClientRequest::set_path(LuaStack &LS, LuaSlot 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")); + check_fail(util::ss("url parameter key must be a string")); return; } if (!LS.isstring(val)) { - fail(util::ss("HTTP url parameter val must be a string")); + check_fail(util::ss("url parameter val must be a string")); return; } set_param(LS.ckstring(key), LS.ckstring(val)); @@ -406,7 +646,7 @@ void HttpClientRequest::set_param(LuaStack &LS, LuaSlot key, LuaSlot val) { void HttpClientRequest::set_params(LuaStack &LS0, LuaSlot tab) { if (!LS0.istable(tab)) { - fail(util::ss("HTTP params must be a table")); + check_fail(util::ss("params must be a table")); return; } LuaVar key, val; @@ -419,7 +659,7 @@ void HttpClientRequest::set_params(LuaStack &LS0, LuaSlot tab) { void HttpClientRequest::set_url(LuaStack &LS, LuaSlot val) { if (!LS.isstring(val)) { - fail(util::ss("HTTP url must be a string")); + check_fail(util::ss("url must be a string")); return; } set_url(LS.ckstring(val)); @@ -427,7 +667,7 @@ void HttpClientRequest::set_url(LuaStack &LS, LuaSlot val) { void HttpClientRequest::set_mime_type(LuaStack &LS, LuaSlot val) { if (!LS.isstring(val)) { - fail(util::ss("HTTP mime type must be a string")); + check_fail(util::ss("mime type must be a string")); return; } set_mime_type(LS.ckstring(val)); @@ -435,7 +675,7 @@ void HttpClientRequest::set_mime_type(LuaStack &LS, LuaSlot val) { void HttpClientRequest::set_content(LuaStack &LS, LuaSlot val) { if (!LS.isstring(val)) { - fail(util::ss("HTTP content must be a string")); + check_fail(util::ss("content must be a string")); return; } set_content(LS.ckstring(val)); @@ -476,119 +716,61 @@ void HttpClientRequest::set_config(LuaStack &LS0, LuaSlot tab) { } else if (kstr == "content") { set_content(LS, val); } else if (kstr == "") { - fail(util::ss("HTTP config parameter names must be strings.")); + check_fail(util::ss("configuration parameter names must be strings.")); } else { - fail(util::ss("HTTP unrecognized config parameter: ", kstr)); + check_fail(util::ss("unrecognized request configuration parameter: ", kstr)); } } } +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 (!error_.empty()) { - return error_; + if (!check_fail_.empty()) { + return check_fail_; } if (method_.empty()) { - return "HTTP method has not been set"; + return "method has not been set"; } if (host_.empty()) { - return "HTTP host has not been set"; + return "host has not been set"; } if (port_ == 0) { - return "HTTP port has not been set"; + return "port has not been set"; } if (path_.empty()) { - return "HTTP url has not been set"; + return "url has not been set"; } if (method_ == "POST") { + if (!content_assigned_) { + return "content not set for POST request"; + } if (mime_type_.empty()) { - if (content_.empty()) { - return "HTTP mime type and content not set for POST request"; - } else { - return "HTTP mime type has not been set for POST request"; - } + return "mime type not set for POST request"; } } else { - if ((!mime_type_.empty()) || (!content_.empty())) { - return "HTTP mime type and content are only for POST requests"; + if (content_assigned_) { + return "content can only be specified for POST requests"; + } + if (!mime_type_.empty()) { + return "mime type can only be specified for POST requests"; } } 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 = "\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); - } - - // If it's a post request, send the content length and the content type. - if (method_ == "POST") { - sb->write_bytes("Content-length: "); - sb->ostream() << content_.size(); - sb->write_bytes(linebreak); - sb->write_bytes("Content-type: "); - sb->write_bytes(mime_type_); - if (sv::has_prefix(mime_type_, "text/")) { - sb->write_bytes(" ; charset=utf-8"); - } - sb->write_bytes(linebreak); - } - - // Send the extra linebreak. - sb->write_bytes(linebreak); - - // If it's a post request, send the content. - if (method_ == "POST") { - sb->write_bytes(content_); - } -} - 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_string(check_fail_); sb->write_bool(verify_certificate_); sb->write_string(method_); sb->write_string(host_); @@ -605,7 +787,7 @@ void HttpClientRequest::deserialize(StreamBuffer *sb) { request_id_ = sb->read_int64(); place_id_ = sb->read_int64(); thread_id_ = sb->read_int64(); - error_ = sb->read_string(); + check_fail_ = sb->read_string(); verify_certificate_ = sb->read_bool(); method_ = sb->read_string(); host_ = sb->read_string(); @@ -620,27 +802,251 @@ void HttpClientRequest::deserialize(StreamBuffer *sb) { } } -eng::string HttpClientRequest::DebugString() { +eng::string HttpClientRequest::debug_string() { 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 HttpServerResponse::check_fail(string_view s) { + if (check_fail_.empty()) { + check_fail_ = s; } } -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; + +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; } + + // 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() && (mime_type_ == "text/plain") && + !content_.empty() && !contains_newline(content_)) { + if (sv::has_prefix(content_, statusmsg)) { + statusmsg = content_; + } else { + 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 (content_assigned_) { + 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 (content_assigned_) { + 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(LuaStack &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(LuaStack &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(LuaStack &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(LuaStack &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_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 == "status") { + set_status(LS, val); + } else if (kstr == "maxage") { + set_max_age(LS, val); + } else if (kstr == "mimetype") { + set_mime_type(LS, val); + } else if (kstr == "content") { + set_content(LS, val); + } else if (kstr == "html") { + set_content(LS, val); + set_mime_type("text/html"); + } else if (kstr == "text") { + set_content(LS, val); + set_mime_type("text/plain"); + } else if (kstr == "json") { + set_content(LS, val); + set_mime_type("application/json"); + } else if (kstr == "bytes") { + set_content(LS, val); + set_mime_type("application/octet-stream"); + } else if (kstr == "") { + check_fail(util::ss("response configuration parameters must be strings.")); + } else { + check_fail(util::ss("unrecognized response configuration parameter: ", kstr)); + } + } +} + +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 you're sending an error message along with + // an error status code, then assume the error + // message is text/plain. + if ((status_ >= 400) && (status_ <= 599)) { + if (content_assigned_ && (mime_type_.empty())) { + 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 specify content, you have to specify mime + // type, and vice versa. Also, we need both for a valid response. + if ((status_ == 200) || (content_assigned_) || (!mime_type_.empty())) { + if (!content_assigned_) { + return "content not specified"; + } + if (mime_type_.empty()) { + return "mime type not specified"; + } + } + return ""; +} + +eng::string HttpServerResponse::debug_string() { + StreamBuffer sb; + send_internal(&sb, true); + return eng::string(sb.view()); } HttpParser::HttpParser() { @@ -650,6 +1056,19 @@ HttpParser::HttpParser() { 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) { @@ -662,9 +1081,9 @@ void HttpParser::fail(int code, std::string_view message) { void HttpParser::syntax(std::string_view detail) { if (is_request_) { - fail(400, util::ss("malformed request: ", detail)); + fail(400, util::ss("Bad Request: ", detail)); } else { - fail(500, util::ss("malformed response: ", detail)); + fail(500, util::ss("Bad Response: ", detail)); } } @@ -677,7 +1096,7 @@ void HttpParser::incomplete(bool closed) { } void HttpParser::oversized() { - fail(413, util::ss("payload too large: Limit=", MAX_CONTENT_LENGTH)); + fail(413, util::ss("Payload Too Large: Limit=", MAX_CONTENT_LENGTH)); } bool HttpParser::parse_request_line(std::string_view &view, bool closed) { @@ -698,20 +1117,21 @@ bool HttpParser::parse_request_line(std::string_view &view, bool closed) { syntax("invalid request line"); return false; } - if (!is_supported_method(method)) { + if (method != "GET") { fail(405, util::ss("Method Not Allowed: ", method)); return false; } if (!is_supported_protocol(protocol)) { - syntax(util::ss("unsupported 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)); + syntax(util::ss("invalid URL path: ", path)); return false; } @@ -730,13 +1150,17 @@ bool HttpParser::parse_status_line(std::string_view &view, bool closed) { return false; } - // Break down the status line. + // 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)) { @@ -750,7 +1174,7 @@ bool HttpParser::parse_status_line(std::string_view &view, bool closed) { // if ((code < 200) || (code > 299)) { if (status.empty()) { - error_ = util::ss("error code ", code); + error_ = status_code_to_string(code); } else { error_ = status; } @@ -810,7 +1234,7 @@ void HttpParser::parse_header(string_view header, string_view 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)); + fail(416, util::ss("Range Not Satisfiable: unsupported header: ", header)); } } @@ -937,7 +1361,7 @@ bool HttpParser::parse_content(std::string_view &view, bool closed) { // 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/plain"; + mime_type_ = "text/html"; charset_ = "utf-8"; } else { mime_type_ = "application/octet-stream"; @@ -966,7 +1390,9 @@ void HttpParser::store(LuaStack &LS0, LuaSlot tab) const { LuaStack LS(LS0.state(), ptab); LS.newtable(tab); - LS.rawset(tab, "status", status_); + if (!is_request_) { + LS.rawset(tab, "status", status_); + } if (!error_.empty()) { LS.rawset(tab, "error", error_); } @@ -977,9 +1403,14 @@ void HttpParser::store(LuaStack &LS0, LuaSlot tab) const { LS.rawset(tab, "mimetype", mime_type_); LS.rawset(tab, "content", content_); } - if (!method_.empty()) { - LS.rawset(tab, "method", method_); - } + // 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); @@ -1044,14 +1475,6 @@ eng::string HttpParser::debug_string() const { return oss.str(); } -void HttpParser::clear_content_on_error() { - if ((status_ < 200) || (status_ > 299)) { - mime_type_.clear(); - charset_.clear(); - content_.clear(); - } -} - void HttpParser::parse_response(std::string_view view, bool closed) { std::string_view original_view = view; is_request_ = false; @@ -1084,9 +1507,6 @@ void HttpParser::parse_response(std::string_view view, bool closed) { syntax("multipart messages not supported"); return; } - - // If there's an error code, throw out the content. - clear_content_on_error(); } void HttpParser::parse_request(std::string_view view, bool closed) { @@ -1125,9 +1545,6 @@ void HttpParser::parse_request(std::string_view view, bool closed) { // If we've made it this far, and there's no // status code, set it to 200 OK. if (status_ == 0) status_ = 200; - - // If there's an error code, throw out the content. - clear_content_on_error(); } void HttpParser::store_fail(LuaStack &LS, LuaSlot tab, int status_code, std::string_view error) { @@ -1136,6 +1553,23 @@ void HttpParser::store_fail(LuaStack &LS, LuaSlot tab, int status_code, std::str 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; @@ -1190,7 +1624,7 @@ LuaDefine(http_clientrequest, "request", luaL_error(L, "%s", error.c_str()); return 0; } - LS.set(str, req.DebugString()); + LS.set(str, req.debug_string()); return LS.result(); } @@ -1300,6 +1734,66 @@ LuaDefine(http_serverrequest, "request", 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; + LuaStack LS(L, tab, str); + HttpServerResponse resp; + resp.set_config(LS, tab); + resp.set_defaults(); + LS.set(str, resp.debug_string()); + return LS.result(); +} + + LuaDefine(http_validmime, "(mt)", "") { LuaArg str; LuaRet ok; @@ -1307,3 +1801,22 @@ LuaDefine(http_validmime, "(mt)", "") { 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; + LuaStack 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; + LuaStack LS(L, code, str); + eng::string sstr = LS.ckstring(str); + LS.set(code, status_code_from_string(sstr)); + int iresult = LS.result(); + return iresult; +} \ No newline at end of file diff --git a/luprex/core/cpp/http.hpp b/luprex/core/cpp/http.hpp index dc46e5a9..628ad771 100644 --- a/luprex/core/cpp/http.hpp +++ b/luprex/core/cpp/http.hpp @@ -31,9 +31,9 @@ private: int64_t place_id_; int64_t thread_id_; - // If the request contains an error, the error - // message is stored here. - eng::string error_; + // When we detect that we generated an invalid + // outgoing request, the error is stored here. + eng::string check_fail_; // If true, verify the server's certificate. // True is the default. @@ -59,9 +59,10 @@ private: // The content as a string, only for POST requests. eng::string content_; + bool content_assigned_; private: - void fail(std::string_view error); + void check_fail(std::string_view error); void send_internal(StreamBuffer *target, bool debug_string) const; public: @@ -69,17 +70,6 @@ public: // All of the fields have empty values. HttpClientRequest(); - // Get request-related fields. - // - const eng::string &error() const { return error_; } - bool verify_certificate() const { return verify_certificate_; } - const eng::string &method() const { return method_; } - const eng::string &host() const { return host_; } - int port() const { return port_; } - const eng::string &path() const { return path_; } - const eng::string &mime_type() const { return mime_type_; } - const eng::string &content() const { return content_; } - // Populate an request-related fields one piece at a time. // If you pass an invalid value, or if the field is // already set, the routine will generate an error message @@ -116,7 +106,10 @@ public: void set_config(LuaStack &LS0, LuaSlot tab); // Get or Set the request IDs. - // This class does just stores these. + // + // This class does not use these fields, it just stores + // them as a convenience. + // int64_t request_id() const { return request_id_; } int64_t place_id() const { return place_id_; } int64_t thread_id() const { return thread_id_; } @@ -125,25 +118,123 @@ public: void set_thread_id(int64_t thread_id) { thread_id_ = thread_id; } // Get the network target, eg, "cert:host:port" + // eng::string target() const; // Verify that the request is error free and that // defaults have been set. + // eng::string check() const; // Put the request into the stream, assuming HTTP/1.1 + // void send(StreamBuffer *target) const { send_internal(target, false); } // Serialize and deserialize. + // void serialize(StreamBuffer *sb) const; void deserialize(StreamBuffer *sb); // Get the request as a debug string. - eng::string DebugString(); + // + eng::string debug_string(); +}; + +class HttpServerResponse { +private: + // When we detect that our own response is + // invalid, the error is stored here. + // + eng::string check_fail_; + + // The status code to send back to the client + // as part of the response status line. + // + int status_; + + // Maximum age for the cache-control directive. + // + int max_age_; + + // Mime type of the content. + // + eng::string mime_type_; + + // Content to send to the client. + // + eng::string content_; + bool content_assigned_; + + // If the keep-alive flag is set, then a connection: keep-alive + // header is sent. Otherwise, a connection: close is sent. + // The keep-alive flag cannot be controlled from Lua. It is + // designed to be handled automatically by the server code. + // + bool keep_alive_; + + // The protocol is taken from the request. + // + bool http11_; + +private: + void check_fail(std::string_view error); + void send_internal(StreamBuffer *target, bool debug_string) const; + +public: + // Construct an empty response. + // All of the fields have empty values. + // + HttpServerResponse(); + + void set_status(int status); + void set_max_age(int max_age); + void set_mime_type(const eng::string &mime_type); + void set_content(const eng::string &content); + + void set_status(LuaStack &LS, LuaSlot val); + void set_max_age(LuaStack &LS, LuaSlot val); + void set_mime_type(LuaStack &LS, LuaSlot val); + void set_content(LuaStack &LS, LuaSlot val); + + // Set default values. + // + void set_defaults(); + + // Populate request-related fields from a Lua table. + // + void set_config(LuaStack &LS0, LuaSlot tab); + + // Set the keep_alive flag. + // + void set_keep_alive(bool k) { keep_alive_ = k; }; + + // Set the protocol. + // + void set_http11(bool k) { http11_= k; } + + // Store a fail response. + // + void fail(int status, const eng::string &message); + + // Verify that the response is error free. + // + eng::string check() const; + + // Get the status of the response. + // + int status() const { return status_; } + bool errstatus() const { return (status_ >= 400)&&(status_ <= 599); } + + // Put the response into the stream, assuming HTTP/1.1 + // + void send(StreamBuffer *target) const { send_internal(target, false); } + + // Get the response as a debug string. + // + eng::string debug_string(); }; // HttpParser is used for parsing both requests and responses. -// Stores a status code, an error message, headers, and content. // class HttpParser : public eng::nevernew { protected: @@ -181,7 +272,10 @@ protected: // Charset of the content before the content was translated to utf-8. eng::string charset_; - // The method, GET, HEAD, or POST. (only when parsing requests) + // The protocol: true for HTTP/1.1, false for HTTP/1.0 + bool http11_; + + // The method: always "GET". (only when parsing requests) eng::string method_; // The URL path, not url-encoded. (only when parsing requests) @@ -216,10 +310,6 @@ protected: // void oversized(); - // If the status is not in the range 200-299 (OK), - // then discard the content. - void clear_content_on_error(); - // Parse a response status line, such as "HTTP/1.1 200 OK" // returns false if we couldn't parse the whole line. // @@ -269,9 +359,23 @@ public: // HttpParser(); - // Get the parsed status. + // Get the parsed status or error message. // int status() const { return status_; } + bool errstatus() const { return (status_ >= 400) && (status_ <= 599); } + eng::string error() const { return error_; } + + // Return true if the communication was complete. + // + bool complete() const { return status_ != 0; } + + // Get the first component of the path. + // If the path is empty, return defval. + eng::string first_path_component(std::string_view defval) const; + + // Get the parsed protocol. + // + bool http11() const { return http11_; } // Store the parsed fields into a lua table. // @@ -301,10 +405,6 @@ public: int64_t request_id() const { return request_id_; } void set_request_id(int64_t v) { request_id_ = v; } - // Return true if the communication was complete. - // - bool complete() const { return status_ != 0; } - // Return a debug string. // eng::string debug_string() const; diff --git a/luprex/core/cpp/lpxserver.cpp b/luprex/core/cpp/lpxserver.cpp index 4d555f80..20162c9b 100644 --- a/luprex/core/cpp/lpxserver.cpp +++ b/luprex/core/cpp/lpxserver.cpp @@ -198,7 +198,6 @@ public: HttpChannel htchan; htchan.channel_ = chan; http_server_channels_.push_back(htchan); - stdostream() << "Http Server got new client " << chan->chid() << std::endl; } } @@ -258,7 +257,8 @@ public: htchan.parsed_bytes_ = chan->in()->fill(); if (parser.complete()) { StreamBuffer *sb = chan->out(); - sb->ostream() << "HTTP/1.1 200 OK\n\n"; + HttpServerResponse resp = master_->http_serve(parser); + resp.send(sb); htchan.channel_ = nullptr; } } diff --git a/luprex/core/cpp/luastack.hpp b/luprex/core/cpp/luastack.hpp index 4dbd4f94..2d427984 100644 --- a/luprex/core/cpp/luastack.hpp +++ b/luprex/core/cpp/luastack.hpp @@ -246,9 +246,8 @@ private: template void count_slots(LuaArg &v, SS & ... stackslots) { - count_slots(stackslots...); + count_slots(stackslots...); } - template void count_slots(LuaVar &v, SS & ... stackslots) { diff --git a/luprex/core/cpp/util.cpp b/luprex/core/cpp/util.cpp index 808da8b9..dd3c2ecd 100644 --- a/luprex/core/cpp/util.cpp +++ b/luprex/core/cpp/util.cpp @@ -24,6 +24,18 @@ namespace sv { +bool case_insensitive_eq(string_view s1, string_view s2) { + if (s1.size() != s2.size()) return false; + for (int i = 0; i < int(s1.size()); i++) { + char c1 = s1[i]; + char c2 = s2[i]; + if (ascii_isupper(c1)) c1 += 'a'-'A'; + if (ascii_isupper(c2)) c2 += 'a'-'A'; + if (c1 != c2) return false; + } + return true; +} + bool valid_int64(string_view value) { int64_t result; const char *last = value.data() + value.size(); diff --git a/luprex/core/cpp/util.hpp b/luprex/core/cpp/util.hpp index bbf57a51..45bbcf17 100644 --- a/luprex/core/cpp/util.hpp +++ b/luprex/core/cpp/util.hpp @@ -50,6 +50,10 @@ inline bool ascii_isspace(char c) { return (c==' ')||(c=='\t')||(c=='\r')||(c==' // inline bool isnull(string_view v) { return v.data() == nullptr; } +// Return true if the two strings are equal, ignoring case. +// +bool case_insensitive_eq(std::string_view s1, std::string_view s2); + // Check if numbers can be parsed as int64/double bool valid_double(string_view v); bool valid_int64(string_view v); diff --git a/luprex/core/cpp/world-accessor.cpp b/luprex/core/cpp/world-accessor.cpp index a0f1d31a..e3d7b7cf 100644 --- a/luprex/core/cpp/world-accessor.cpp +++ b/luprex/core/cpp/world-accessor.cpp @@ -666,11 +666,13 @@ LuaDefine(http_get, "request", HttpClientRequest req; // Parse the request and make sure it's valid. + // If not, immediately pass a '400 bad request' back to lua. + req.set_method("GET"); req.set_config(LS, request); req.set_defaults(); eng::string error = req.check(); if (!error.empty()) { - HttpParser::store_fail(LS, response, 400, util::ss("bad request: ", error)); + HttpParser::store_fail(LS, response, 400, util::ss("Bad Request: ", error)); return LS.result(); } diff --git a/luprex/core/cpp/world-core.cpp b/luprex/core/cpp/world-core.cpp index 0018f6a3..186a68a0 100644 --- a/luprex/core/cpp/world-core.cpp +++ b/luprex/core/cpp/world-core.cpp @@ -458,6 +458,91 @@ void World::abort_all_http_requests(int status_code, std::string_view error) { } } +HttpServerResponse World::http_serve(const HttpParser &request) { + assert(stack_is_clear()); + HttpServerResponse response; + + // We're only supposed to be passed complete requests. + assert(request.complete()); + + // If the request is HTTP/1.1, then the response should be HTTP/1.1 + response.set_http11(request.http11()); + + // If the incoming request has already been detected to be + // invalid by the HTTP parser, then just send the error + // message back to the client without involving lua at all. + if (request.errstatus()) { + response.fail(request.status(), request.error()); + return response; + } + + // Get the name of the desired function. + std::string_view fn = request.first_path_component("index"); + if (!sv::is_lua_id(fn)) { + response.fail(404, util::ss("not a function name: ", fn)); + return response; + } + + lua_State *L = state(); + LuaVar www, func, reqtab; + LuaStack LS(L, www, func, reqtab); + + // Get the www class. If there's no such class, + // return a 503 Service Unavailable to the client. + eng::string err = LS.getclass(www, "www"); + if (!err.empty()) { + response.fail(503, "class www doesn't exist"); + LS.result(); + return response; + } + + // Get the closure. If there's no such closure, + // return a 404 Not Found to the client. + LS.rawget(func, www, fn); + if (!LS.isfunction(func)) { + response.fail(404, util::ss("no such function: www.", fn)); + LS.result(); + return response; + } + + // Store the request into a lua table. + request.store(LS, reqtab); + + // Call the function. + int oldtop = lua_gettop(L); + lua_pushvalue(L, func.index()); + lua_pushvalue(L, reqtab.index()); + Gui::store_global_pointer(L, nullptr); + open_lthread_state(0, 0, 0, false, false); + eng::string msg = traceback_pcall(L, 1, LUA_MULTRET); + close_lthread_state(); + + // If the call threw an error, return + // a 500 Internal Server Error to the client. + if (!msg.empty()) { + response.fail(500, msg); + LS.result(); + return response; + } + + // If the call didn't return a single table, return + // a 500 Internal Server Error to the client. + int newtop = lua_gettop(L); + if ((newtop != oldtop + 1) || (!lua_istable(L, newtop))) { + response.fail(500, util::ss("lua function www.", fn, " didn't return a table")); + LS.result(); + return response; + } + + // Try to convert the table into a response. + // If this results in an error, we don't have to do + // anything special, set_config will store the error. + response.set_config(LS, LuaSpecial(newtop)); + response.set_defaults(); + LS.result(); + return response; +} + void World::run_unittests() { assert(stack_is_clear()); source_db_.run_unittests(); diff --git a/luprex/core/cpp/world.hpp b/luprex/core/cpp/world.hpp index 1b416812..61c2f82b 100644 --- a/luprex/core/cpp/world.hpp +++ b/luprex/core/cpp/world.hpp @@ -206,14 +206,23 @@ public: void update_source(const util::LuaSourceVec &source); // Supply an HTTP response to an outstanding HTTP request. + // void http_response(const HttpParser &response); void http_responses(const HttpParserVec &responses); // Abort all HTTP requests. This is typically used after // reloading a world from a save-game. The http requests that // were in progress are long-since dead. + // void abort_all_http_requests(int status_code, std::string_view error); + // Serve an HTTP query coming in from outside. + // + // Note: the lua code for the http_serve runs in a nonblocking + // context. It must produce a result instantly. + // + HttpServerResponse http_serve(const HttpParser &request); + // Run all unit tests. // void run_unittests();