1927 lines
58 KiB
C++
1927 lines
58 KiB
C++
//
|
|
// 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 <cstdint>
|
|
|
|
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(LuaStack &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(LuaStack &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(LuaStack &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(LuaStack &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(LuaStack &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(LuaStack &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(LuaStack &LS0, LuaSlot tab) {
|
|
if (!LS0.istable(tab)) {
|
|
check_fail(util::ss("params must be a table"));
|
|
return;
|
|
}
|
|
LuaVar key, val;
|
|
LuaStack LS(LS0.state(), key, val);
|
|
LS.set(key, LuaNil);
|
|
while (LS.next(tab, key, val)) {
|
|
set_param(LS, key, val);
|
|
}
|
|
}
|
|
|
|
void HttpClientRequest::set_url(LuaStack &LS, LuaSlot val) {
|
|
if (!LS.isstring(val)) {
|
|
check_fail(util::ss("url must be a string"));
|
|
return;
|
|
}
|
|
set_url(LS.ckstring(val));
|
|
}
|
|
|
|
void HttpClientRequest::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 HttpClientRequest::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 HttpClientRequest::set_jsonvalue(LuaStack &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;
|
|
LuaStack 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(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_jsonvalue(LuaStack &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;
|
|
LuaStack 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<string_view> 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(LuaStack &LS0, LuaSlot tab) const {
|
|
LuaVar ptab, djson;
|
|
LuaStack 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(LuaStack &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;
|
|
LuaStack LS(L, url, fixed);
|
|
ParsedURL parsed(LS.ckstring(url));
|
|
if (!parsed.valid) {
|
|
luaL_error(L, "invalid URL, not fixable");
|
|
return LS.result();
|
|
}
|
|
LS.set(fixed, parsed.str());
|
|
return LS.result();
|
|
}
|
|
|
|
|
|
LuaDefine(http_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;
|
|
LuaStack 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;
|
|
LuaStack 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;
|
|
LuaStack 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;
|
|
LuaStack 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;
|
|
LuaStack 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;
|
|
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;
|
|
}
|