Files
integration/luprex/core/cpp/http.cpp

1336 lines
40 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 <cstdint>
using string_view = std::string_view;
bool is_supported_protocol(string_view protocol) {
return (protocol == "HTTP/1.0") || (protocol == "HTTP/1.1");
}
bool is_supported_method(string_view method) {
return ((method == "GET") || (method == "HEAD") || (method == "POST"));
}
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 &params, StreamBuffer *sb) {
sb->write_bytes(url_encode_path(path));
bool first_param = true;
for (const auto &pair : params) {
sb->write_char(first_param ? '?' : '&');
sb->write_bytes(url_encode_param(pair.first));
sb->write_char('=');
sb->write_bytes(url_encode_param(pair.second));
first_param = false;
}
}
static void send_host_and_port(std::string_view host, int port, StreamBuffer *sb) {
sb->write_bytes(host);
if (port != 0) {
sb->write_char(':');
sb->ostream() << port;
}
}
// In a properly-formed url, the hostname and path are url encoded.
// This parser expects an encoded URL.
struct ParsedURL {
public:
bool valid;
eng::string proto;
eng::string host;
int port;
eng::string path;
UrlParameters params;
public:
void clear() {
valid = false;
proto.clear();
host.clear();
port = 0;
path.clear();
params.clear();
}
eng::string str() {
StreamBuffer sb;
sb.write_bytes(proto);
sb.write_bytes("://");
send_host_and_port(host, port, &sb);
send_encoded_path(path, params, &sb);
return eng::string(sb.view());
}
ParsedURL(std::string_view url) {
clear();
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;
}
};
HttpClientRequest::HttpClientRequest() {
verify_certificate_ = false;
port_ = 0;
request_id_ = 0;
place_id_ = 0;
thread_id_ = 0;
}
void HttpClientRequest::fail(string_view s) {
if (error_.empty()) {
error_ = s;
}
}
eng::string HttpClientRequest::target() const {
assert(check().empty());
eng::ostringstream oss;
oss << (verify_certificate_ ? "cert" : "nocert");
oss << ':' << host_ << ':' << port_;
return oss.str();
}
void HttpClientRequest::set_verify_certificate(bool flag) {
verify_certificate_ = flag;
}
void HttpClientRequest::set_method(const eng::string &s) {
eng::string method = util::ascii_toupper(s);
if (!is_supported_method(method)) {
fail(util::ss("HTTP method not implemented: ", method, "."));
return;
}
if ((!method_.empty()) && (method_ != method)) {
fail(util::ss("HTTP method specified twice: ", method_, " and ", method));
return;
}
method_ = method;
}
void HttpClientRequest::set_host(const eng::string &s) {
eng::string host = util::ascii_tolower(s);
if (host.empty()) {
fail(util::ss("HTTP hostname cannot be empty string."));
return;
}
// This is not quite strict, but it's close. I believe
// the DNS lookup will fail for invalid hostnames anyway.
for (char c : host) {
if ((c != '-') && (c != '.') && (!sv::ascii_isalnum(c))) {
fail(util::ss("HTTP hostnames can only contain letters, digits, and hyphen: ", host));
return;
}
}
if (!host_.empty()) {
fail(util::ss("HTTP hostname specified twice: ", host_, " and ", host));
return;
}
host_ = host;
}
void HttpClientRequest::set_port(int port) {
if ((port < 1) || (port > 65535)) {
fail(util::ss("HTTP port must be between 1 and 65535: ", port));
return;
}
if (port_ != 0) {
fail(util::ss("HTTP port specified twice: ", port_, " and ", port));
return;
}
port_ = port;
}
void HttpClientRequest::set_path(string_view path) {
if (!sv::has_prefix(path, "/")) {
fail(util::ss("HTTP path must start with slash"));
return;
}
if (!path_.empty()) {
fail(util::ss("HTTP path specified twice: ", path_, " and ", path));
return;
}
path_ = path;
}
void HttpClientRequest::set_param(const eng::string &key, const eng::string &val) {
if (params_.find(key) != params_.end()) {
fail(util::ss("HTTP url parameter specified twice: ", key));
return;
}
if (key.empty()) {
fail(util::ss("HTTP parameter key cannot be empty"));
return;
}
params_[key] = val;
}
void HttpClientRequest::set_url(string_view url) {
ParsedURL parsed_url(url);
if (!parsed_url.valid) {
fail(util::ss("syntactically invalid URL: ", url));
return;
}
if (parsed_url.proto != "https") {
fail(util::ss("unsupported protocol: ", parsed_url.proto));
return;
}
set_host(parsed_url.host);
if (parsed_url.port) set_port(parsed_url.port);
set_path(parsed_url.path);
for (const auto &pair : parsed_url.params) {
set_param(pair.first, pair.second);
}
}
void HttpClientRequest::set_mime_type(const eng::string &mime_type) {
if (!valid_mime_type(mime_type)) {
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));
return;
}
mime_type_ = mime_type;
}
void HttpClientRequest::set_content(const eng::string &content) {
if (!content_.empty()) {
fail(util::ss("Content specified twice"));
return;
}
content_ = content;
}
void HttpClientRequest::set_verify_certificate(LuaStack &LS, LuaSlot val) {
if (!LS.isboolean(val)) {
fail(util::ss("HTTP verify_certificate must be a boolean"));
return;
}
set_verify_certificate(LS.ckboolean(val));
}
void HttpClientRequest::set_method(LuaStack &LS, LuaSlot val) {
if (!LS.isstring(val)) {
fail(util::ss("HTTP method must be a string"));
return;
}
set_method(LS.ckstring(val));
}
void HttpClientRequest::set_host(LuaStack &LS, LuaSlot val) {
if (!LS.isstring(val)) {
fail(util::ss("HTTP host must be a string"));
return;
}
set_host(LS.ckstring(val));
}
void HttpClientRequest::set_port(LuaStack &LS, LuaSlot val) {
if (!LS.isint(val)) {
fail(util::ss("HTTP port must be an int"));
return;
}
set_port(LS.ckint(val));
}
void HttpClientRequest::set_path(LuaStack &LS, LuaSlot val) {
if (!LS.isstring(val)) {
fail(util::ss("HTTP path must be a string"));
return;
}
set_path(LS.ckstring(val));
}
void HttpClientRequest::set_param(LuaStack &LS, LuaSlot key, LuaSlot val) {
if (!LS.isstring(key)) {
fail(util::ss("HTTP url parameter key must be a string"));
return;
}
if (!LS.isstring(val)) {
fail(util::ss("HTTP url parameter val must be a string"));
return;
}
set_param(LS.ckstring(key), LS.ckstring(val));
}
void HttpClientRequest::set_params(LuaStack &LS0, LuaSlot tab) {
if (!LS0.istable(tab)) {
fail(util::ss("HTTP params must be a table"));
return;
}
LuaVar key, val;
LuaStack LS(LS0.state(), key, val);
LS.set(key, LuaNil);
while (LS.next(tab, key, val)) {
set_param(LS, key, val);
}
}
void HttpClientRequest::set_url(LuaStack &LS, LuaSlot val) {
if (!LS.isstring(val)) {
fail(util::ss("HTTP url must be a string"));
return;
}
set_url(LS.ckstring(val));
}
void HttpClientRequest::set_mime_type(LuaStack &LS, LuaSlot val) {
if (!LS.isstring(val)) {
fail(util::ss("HTTP 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)) {
fail(util::ss("HTTP content must be a string"));
return;
}
set_content(LS.ckstring(val));
}
void HttpClientRequest::set_defaults() {
if (method_.empty()) {
method_ = "GET";
}
if (port_ == 0) {
port_ = 443;
}
}
void HttpClientRequest::set_config(LuaStack &LS0, LuaSlot tab) {
LuaVar key, val;
LuaStack LS(LS0.state(), key, val);
LS.set(key, LuaNil);
while (LS.next(tab, key, val)) {
eng::string kstr;
if (LS.isstring(key)) kstr = LS.ckstring(key);
if (kstr == "method") {
set_method(LS, val);
} else if (kstr == "host") {
set_host(LS, val);
} else if (kstr == "port") {
set_port(LS, val);
} else if (kstr == "path") {
set_path(LS, val);
} else if (kstr == "params") {
set_params(LS, val);
} else if (kstr == "url") {
set_url(LS, val);
} else if (kstr == "verifycertificate") {
set_verify_certificate(LS, val);
} else if (kstr == "mimetype") {
set_mime_type(LS, val);
} else if (kstr == "content") {
set_content(LS, val);
} else if (kstr == "") {
fail(util::ss("HTTP config parameter names must be strings."));
} else {
fail(util::ss("HTTP unrecognized config parameter: ", kstr));
}
}
}
eng::string HttpClientRequest::check() const {
if (!error_.empty()) {
return error_;
}
if (method_.empty()) {
return "HTTP method has not been set";
}
if (host_.empty()) {
return "HTTP host has not been set";
}
if (port_ == 0) {
return "HTTP port has not been set";
}
if (path_.empty()) {
return "HTTP url has not been set";
}
if (method_ == "POST") {
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";
}
}
} else {
if ((!mime_type_.empty()) || (!content_.empty())) {
return "HTTP mime type and content are only 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_bool(verify_certificate_);
sb->write_string(method_);
sb->write_string(host_);
sb->write_int32(port_);
sb->write_string(path_);
sb->write_int32(params_.size());
for (const auto &pair : params_) {
sb->write_string(pair.first);
sb->write_string(pair.second);
}
}
void HttpClientRequest::deserialize(StreamBuffer *sb) {
request_id_ = sb->read_int64();
place_id_ = sb->read_int64();
thread_id_ = sb->read_int64();
error_ = sb->read_string();
verify_certificate_ = sb->read_bool();
method_ = sb->read_string();
host_ = sb->read_string();
port_ = sb->read_int32();
path_ = sb->read_string();
int32_t nparams = sb->read_int32();
params_.clear();
for (int i = 0; i < nparams; i++) {
eng::string k = sb->read_string();
eng::string v = sb->read_string();
params_[k] = v;
}
}
eng::string HttpClientRequest::DebugString() {
StreamBuffer sb;
send_internal(&sb, true);
return eng::string(sb.view());
}
void HttpClientRequestMap::serialize(StreamBuffer *sb) const {
sb->write_int32(size());
for (const auto &pair : *this) {
pair.second.serialize(sb);
}
}
void HttpClientRequestMap::deserialize(StreamBuffer *sb) {
int32_t count = sb->read_int32();
clear();
HttpClientRequest req;
for (int i = 0; i < count; i++) {
req.deserialize(sb);
(*this)[req.request_id()] = req;
}
}
HttpParser::HttpParser() {
status_ = 0;
mime_type_ = "";
content_length_ = -1;
comm_length_ = 0;
}
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("malformed request: ", detail));
} else {
fail(500, util::ss("malformed 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 (!is_supported_method(method)) {
fail(405, util::ss("Method Not Allowed: ", method));
return false;
}
if (!is_supported_protocol(protocol)) {
syntax(util::ss("unsupported protocol: ", protocol));
return false;
}
// 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;
}
// Break down the status line.
//
string_view protoversion = sv::read_to_space(status);
if (!is_supported_protocol(protoversion)) {
syntax(util::ss("unsupported protocol: ", protoversion));
return false;
}
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_ = util::ss("error code ", 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) {
// 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/plain";
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_parsed(LuaStack &LS0, LuaSlot tab) const {
LuaVar ptab;
LuaStack LS(LS0.state(), ptab);
LS.newtable(tab);
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 (!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_);
}
}
void HttpParser::parser_generate_debug_string(std::ostream &oss) const {
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;
}
}
void HttpParser::clear_content_on_error() {
if ((status_ < 200) || (status_ > 299)) {
mime_type_.clear();
charset_.clear();
content_.clear();
}
}
HttpClientResponse::HttpClientResponse() {
request_id_ = 0;
is_request_ = false;
}
eng::string HttpClientResponse::DebugString() const {
eng::ostringstream oss;
oss << "HttpClientResponse:" << std::endl;
if (request_id_ != 0) {
oss << " request_id: " << request_id_ << std::endl;
}
parser_generate_debug_string(oss);
return oss.str();
}
void HttpClientResponse::parse(std::string_view view, bool closed) {
std::string_view original_view = view;
// 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.
set_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;
}
// If there's an error code, throw out the content.
clear_content_on_error();
}
void HttpClientResponse::store(LuaStack &LS, LuaSlot tab) const {
store_parsed(LS, tab);
}
void HttpClientResponse::store_fail(LuaStack &LS, LuaSlot tab, int status_code, std::string_view error) {
HttpClientResponse response;
response.fail(status_code, error);
response.store(LS, tab);
}
HttpServerRequest::HttpServerRequest() {
is_request_ = true;
}
void HttpServerRequest::parse(std::string_view view, bool closed) {
std::string_view original_view = view;
// 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 (method_ == "POST") {
if (!parse_content(view, closed)) {
return;
}
}
// Calculate the comm length.
set_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;
// If there's an error code, throw out the content.
clear_content_on_error();
}
void HttpServerRequest::store(LuaStack &LS, LuaSlot tab) const {
store_parsed(LS, tab);
}
eng::string HttpServerRequest::DebugString() const {
eng::ostringstream oss;
oss << "HttpServerRequest:" << std::endl;
parser_generate_debug_string(oss);
return oss.str();
}
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)"
"| url (ie, 'https://host:port/path.html?a=b&c=d')"
"|"
"|You can specify url components separately (host, port, path,"
"|and params), or you can specify the entire url as a unit. "
"|If you specify components, they must not be url-encoded. "
"|If you specify the url as a whole, it must already be url-encoded."
"|"
"|You can omit the port, in which case it defaults to the"
"|standard https port. You can omit verifycertificate, in which"
"|case it defaults to true. You can omit the method if the"
"|method is implied by the function you called (eg, 'http.get')."
"|"
"|Note that unencrypted http is not supported - we only allow https."
"|However, you can talk to a server that has a dummy certificate"
"|by specifying verifycertificate=false."
"|"
"|This routine, http.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);
HttpClientRequest req;
req.set_config(LS, tab);
req.set_defaults();
eng::string error = req.check();
if (!error.empty()) {
luaL_error(L, "%s", error.c_str());
return 0;
}
LS.set(str, req.DebugString());
return LS.result();
}
LuaDefine(http_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);
HttpClientResponse resp;
resp.parse(LS.ckstring(text), true);
resp.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)"
"| 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 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);
HttpServerRequest req;
req.parse(LS.ckstring(text), true);
req.store(LS, tab);
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();
}