#include "http.hpp" #include "wrap-sstream.hpp" #include "wrap-string.hpp" #include "luastack.hpp" #include static void url_encode(const eng::string &value, StreamBuffer *sb) { const char *hexdigits = "0123456789ABCDEF"; for (int i = 0; i < int(value.size()); i++) { char c = value[i]; if (util::ascii_isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || (c == '/')) { sb->write_char(c); } else if (c == ' ') { sb->write_char('+'); } else { sb->write_char('%'); sb->write_char(hexdigits[c>>4]); sb->write_char(hexdigits[c&15]); } } } class ErrorStringStream : public eng::ostringstream { private: eng::string *target_; public: ErrorStringStream(eng::string *target) : target_(target) {} ~ErrorStringStream() { if (target_->empty()) { (*target_) = str(); } } }; HttpRequest::HttpRequest() { verify_certificate_ = true; port_ = 0; } void HttpRequest::set_verify_certificate(bool flag) { verify_certificate_ = flag; } eng::string HttpRequest::target() const { assert(check().empty()); eng::ostringstream oss; oss << (verify_certificate_ ? "cert" : "nocert"); oss << ':' << host_ << ':' << port_; return oss.str(); } void HttpRequest::set_method(const eng::string &s) { eng::string method = util::ascii_toupper(s); if ((method != "GET") && (method != "HEAD")) { ErrorStringStream error(&error_); error << "HTTPS method not implemented: " << method; error << ". Currently, only HEAD and GET are implemented."; return; } if ((!method_.empty()) && (method_ != method)) { ErrorStringStream error(&error_); error << "HTTPS method specified twice: " << method_ << " and " << method; return; } method_ = method; } void HttpRequest::set_host(const eng::string &s) { eng::string host = util::ascii_tolower(s); if (host.empty()) { ErrorStringStream error(&error_); error << "HTTPS 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 != '.') && (!util::ascii_isalnum(c))) { ErrorStringStream error(&error_); error << "HTTPS hostnames can only contain letters, digits, and hyphen: " << host; return; } } if (!host_.empty()) { ErrorStringStream error(&error_); error << "HTTPS hostname specified twice: " << host_ << " and " << host; return; } host_ = host; } void HttpRequest::set_port(int port) { if ((port < 1) || (port > 65535)) { ErrorStringStream error(&error_); error << "HTTP port must be between 1 and 65535: " << port; return; } if (port_ != 0) { ErrorStringStream error(&error_); error << "HTTPS port specified twice: " << port_ << " and " << port; return; } port_ = port; } void HttpRequest::set_url(const eng::string &url) { if (util::has_prefix(url, "https://")) { ErrorStringStream error(&error_); error << "set_url(full_url) not implemented yet."; return; } else if (util::has_prefix(url, "/")) { if (!path_.empty()) { ErrorStringStream error(&error_); error << "HTTP path specified twice: " << path_ << " and " << url; return; } path_ = url; } else { ErrorStringStream error(&error_); error << "HTTP url must start with https://, or with /"; return; } } void HttpRequest::set_param(const eng::string &key, const eng::string &val) { if (params_.find(key) != params_.end()) { ErrorStringStream error(&error_); error << "HTTP url parameter specified twice: " << key; return; } params_[key] = val; } void HttpRequest::set_verify_certificate(LuaStack &LS, LuaSlot val) { if (!LS.isboolean(val)) { ErrorStringStream error(&error_); error << "HTTP verify_certificate must be a boolean"; return; } set_verify_certificate(LS.ckboolean(val)); } void HttpRequest::set_method(LuaStack &LS, LuaSlot val) { if (!LS.isstring(val)) { ErrorStringStream error(&error_); error << "HTTP method must be a string"; return; } set_method(LS.ckstring(val)); } void HttpRequest::set_host(LuaStack &LS, LuaSlot val) { if (!LS.isstring(val)) { ErrorStringStream error(&error_); error << "HTTP host must be a string"; return; } set_host(LS.ckstring(val)); } void HttpRequest::set_port(LuaStack &LS, LuaSlot val) { if (!LS.isint(val)) { ErrorStringStream error(&error_); error << "HTTP port must be an int"; return; } set_port(LS.ckint(val)); } void HttpRequest::set_url(LuaStack &LS, LuaSlot val) { if (!LS.isstring(val)) { ErrorStringStream error(&error_); error << "HTTP url must be a string"; return; } set_url(LS.ckstring(val)); } void HttpRequest::set_param(LuaStack &LS, LuaSlot key, LuaSlot val) { if (!LS.isstring(key)) { ErrorStringStream error(&error_); error << "HTTP url parameter key must be a string"; return; } if (!LS.isstring(val)) { ErrorStringStream error(&error_); error << "HTTP url parameter val must be a string"; return; } set_param(LS.ckstring(key), LS.ckstring(val)); } void HttpRequest::set_params(LuaStack &LS0, LuaSlot tab) { if (!LS0.istable(tab)) { ErrorStringStream error(&error_); error << "HTTP params must be a table"; return; } LuaVar key, val; LuaStack LS(LS0.state(), key, val); LS.set(key, LuaNil); while (LS.next(tab, key, val)) { set_param(LS, key, val); } } void HttpRequest::set_defaults() { if (method_.empty()) { method_ = "GET"; } if (port_ == 0) { port_ = 443; } } void HttpRequest::set_config(LuaStack &LS0, LuaSlot tab) { LuaVar key, val; LuaStack LS(LS0.state(), key, val); LS.set(key, LuaNil); while (LS.next(tab, key, val)) { eng::string kstr; if (LS.isstring(key)) kstr = LS.ckstring(key); if (kstr == "method") { set_method(LS, val); } else if (kstr == "host") { set_host(LS, val); } else if (kstr == "port") { set_port(LS, val); } else if (kstr == "url") { set_url(LS, val); } else if (kstr == "params") { set_params(LS, val); } else if (kstr == "verifycertificate") { set_verify_certificate(LS, val); } else if (kstr == "") { ErrorStringStream error(&error_); error << "HTTP config parameter names must be strings."; } else { ErrorStringStream error(&error_); error << "HTTP unrecognized config parameter: " << kstr; } } } eng::string HttpRequest::check() const { if (!error_.empty()) { return error_; } if (method_.empty()) { return "HTTP method has not been set"; } if (host_.empty()) { return "HTTP host has not been set"; } if (port_ == 0) { return "HTTP port has not been set"; } if (path_.empty()) { return "HTTP url has not been set"; } return ""; } void HttpRequest::send_internal(StreamBuffer *sb, bool debug_string) const { // If there's an error in the request, handle it. In debug string mode, // we just put the error into the output. In production mode, we assert // fail. eng::string error = check(); if (debug_string) { if (!error.empty()) { sb->write_bytes(error); return; } } else { assert(error.empty()); } // Choose a linebreak. eng::string linebreak = (debug_string) ? "\n" : "\r\n"; // Send the command. sb->write_bytes(method_); sb->write_char(' '); url_encode(path_, sb); bool first_param = true; for (const auto &pair : params_) { sb->write_char(first_param ? '?' : '&'); url_encode(pair.first, sb); sb->write_char('='); url_encode(pair.second, sb); first_param = false; } sb->write_bytes(" HTTP/1.1"); sb->write_bytes(linebreak); // Send the host header. sb->write_bytes("Host: "); sb->write_bytes(host_); sb->write_char(':'); sb->ostream() << port_; 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); // Send the extra linebreak. if (!debug_string) { sb->write_bytes(linebreak); } } eng::string HttpRequest::DebugString() { StreamBuffer sb; send_internal(&sb, true); return eng::string(sb.view()); } HttpResponse::HttpResponse() { response_code_ = 0; response_length_ = 0; mime_type_ = "application/empty"; } void HttpResponse::fail(int response_code, const eng::string &error) { response_code_ = response_code; error_ = error; response_length_ = 0; mime_type_ = "application/empty"; content_ = ""; } static std::string_view readline(std::string_view &v) { std::string_view result = util::sv_split_one(v, '\n'); return util::sv_rtrim(result, '\r'); } void HttpResponse::parse(const StreamBuffer *sb) { // We're not going to modify the StreamBuffer at all. // Instead, we work entirely on a view. std::string_view view = sb->view(); // Special case this. if (view.empty()) { fail(500, "HTTP server response completely empty"); return; } // Parse the status line. std::string_view status = readline(view); if (status.empty()) { fail(500, "HTTP status-line not present in response"); return; } //std::string_view status_code = util::sv_split_one(status, ' '); } LuaDefine(http_request, "reqtab", "|Given an HTTP request in the form of a table, returns the same " "|request as a string, to assist with debugging." "|" "|The table can contain:" "|" "| method (ie, GET, HEAD, POST, etc)" "| host (ie, 'google.com')" "| port (default: 443)" "| url (ie, '/index.html')" "| params (a table of url parameters)" "| verifycertificate (default: true)" "|" "|The url can start with 'https://', or with '/'. If it starts" "|with 'https://', then the URL includes the host and port, which" "|then must not be specified separately." "|" "|Note that plain 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 module will automatically url encode everything for you." "|Therefore, you shouldn't url encode anything, otherwise," "|you'll end up double-encoding." "|" "|You cannot include url parameters as part of the url. If you try," "|then your ?, &, and = characters will get url encoded, which will" "|cause them to not function. To use url parameters, you must" "|use the separate params table." "|") { LuaArg tab; LuaRet str; LuaStack LS(L, tab, str); HttpRequest req; req.set_config(LS, tab); req.set_defaults(); eng::string error = req.check(); if (!error.empty()) { luaL_error(L, "%s", error.c_str()); return 0; } LS.set(str, req.DebugString()); return LS.result(); }