Machinery for blocking http requests in place, except actual networking
This commit is contained in:
@@ -142,6 +142,9 @@ namespace eng {
|
||||
|
||||
// eng::nevernew. A class containing private operator new and
|
||||
// operator delete, making it impossible to 'new' the class.
|
||||
// This means the class must be embedded as a field in some other
|
||||
// class, and it gets allocated when its enclosing object gets
|
||||
// allocated.
|
||||
namespace eng {
|
||||
class nevernew {
|
||||
private:
|
||||
|
||||
@@ -189,6 +189,9 @@ public:
|
||||
HttpOutRequest::HttpOutRequest() {
|
||||
verify_certificate_ = true;
|
||||
port_ = 0;
|
||||
request_id_ = 0;
|
||||
place_id_ = 0;
|
||||
thread_id_ = 0;
|
||||
}
|
||||
|
||||
void HttpOutRequest::fail(string_view s) {
|
||||
@@ -395,8 +398,6 @@ void HttpOutRequest::set_config(LuaStack &LS0, LuaSlot tab) {
|
||||
set_port(LS, val);
|
||||
} else if (kstr == "path") {
|
||||
set_path(LS, val);
|
||||
} else if (kstr == "encodedpath") {
|
||||
set_path(LS, val);
|
||||
} else if (kstr == "params") {
|
||||
set_params(LS, val);
|
||||
} else if (kstr == "url") {
|
||||
@@ -430,6 +431,7 @@ eng::string HttpOutRequest::check() const {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
void HttpOutRequest::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
|
||||
@@ -469,18 +471,78 @@ void HttpOutRequest::send_internal(StreamBuffer *sb, bool debug_string) const {
|
||||
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);
|
||||
}
|
||||
|
||||
// Send the extra linebreak.
|
||||
if (!debug_string) {
|
||||
sb->write_bytes(linebreak);
|
||||
}
|
||||
}
|
||||
|
||||
void HttpOutRequest::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 HttpOutRequest::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 HttpOutRequest::DebugString() {
|
||||
StreamBuffer sb;
|
||||
send_internal(&sb, true);
|
||||
return eng::string(sb.view());
|
||||
}
|
||||
|
||||
void HttpOutRequestMap::serialize(StreamBuffer *sb) const {
|
||||
sb->write_int32(size());
|
||||
for (const auto &pair : *this) {
|
||||
pair.second.serialize(sb);
|
||||
}
|
||||
}
|
||||
|
||||
void HttpOutRequestMap::deserialize(StreamBuffer *sb) {
|
||||
int32_t count = sb->read_int32();
|
||||
clear();
|
||||
HttpOutRequest req;
|
||||
for (int i = 0; i < count; i++) {
|
||||
req.deserialize(sb);
|
||||
(*this)[req.request_id()] = req;
|
||||
}
|
||||
}
|
||||
|
||||
HttpInResponse::HttpInResponse() {
|
||||
status_code_ = 0;
|
||||
response_length_ = 0;
|
||||
@@ -513,7 +575,7 @@ void HttpInResponse::fail(int code, string_view message) {
|
||||
|
||||
void HttpInResponse::incomplete(bool closed) {
|
||||
if (closed) {
|
||||
fail(500, "response truncated");
|
||||
fail(500, "internal server error: response truncated");
|
||||
} else {
|
||||
fail(0, "response not yet fully received");
|
||||
}
|
||||
@@ -526,7 +588,7 @@ void HttpInResponse::parse_content_encoding(string_view value) {
|
||||
void HttpInResponse::parse_content_length(string_view value) {
|
||||
int64_t code = sv::to_int64(value);
|
||||
if ((code < 0) || (code > INT_MAX)) {
|
||||
fail(500, util::ss("unparseable content-length: ", value));
|
||||
fail(500, util::ss("internal server error: unparseable content-length: ", value));
|
||||
}
|
||||
content_length_ = code;
|
||||
}
|
||||
@@ -536,7 +598,7 @@ void HttpInResponse::parse_content_type(string_view value) {
|
||||
string_view ctview(ctype);
|
||||
mime_type_ = sv::trim(sv::read_to_sep(ctview, ';'));
|
||||
if (mime_type_.empty()) {
|
||||
fail(500, util::ss("unparseable content-type: ", value));
|
||||
fail(500, util::ss("internal server error: unparseable content-type: ", value));
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
@@ -571,14 +633,14 @@ void HttpInResponse::parse_header(string_view header, string_view value) {
|
||||
} else if (header == "transfer-encoding") {
|
||||
parse_transfer_encoding(value);
|
||||
} else if (header == "content-range") {
|
||||
fail(500, util::ss("unsupported response header: ", header));
|
||||
fail(406, util::ss("not acceptable: unsupported response header: ", header));
|
||||
}
|
||||
}
|
||||
|
||||
bool HttpInResponse::parse_content_basic(std::string_view &view, bool closed) {
|
||||
if (content_length_ >= 0) {
|
||||
if (content_length_ > MAX_CONTENT_LENGTH) {
|
||||
fail(500, "content too long");
|
||||
fail(413, util::ss("payload too large: luprex limit=", MAX_CONTENT_LENGTH));
|
||||
return false;
|
||||
}
|
||||
if (int(view.size()) < content_length_) {
|
||||
@@ -588,7 +650,7 @@ bool HttpInResponse::parse_content_basic(std::string_view &view, bool closed) {
|
||||
content_ = sv::read_nbytes(view, content_length_);
|
||||
} else {
|
||||
if (int64_t(view.size()) > MAX_CONTENT_LENGTH) {
|
||||
fail(500, "content too long");
|
||||
fail(413, util::ss("payload too large: luprex limit=", MAX_CONTENT_LENGTH));
|
||||
return false;
|
||||
}
|
||||
if (!closed) {
|
||||
@@ -611,17 +673,17 @@ bool HttpInResponse::parse_content_chunked(std::string_view &view, bool closed)
|
||||
}
|
||||
int64_t chunk_size = sv::to_hex64(chunk_header, -1);
|
||||
if (chunk_size < 0) {
|
||||
fail(500, "unparseable chunk header");
|
||||
fail(500, "internal server error: unparseable chunk header");
|
||||
return false;
|
||||
}
|
||||
if (chunk_size > MAX_CONTENT_LENGTH) {
|
||||
fail(500, "content too long");
|
||||
fail(413, util::ss("payload too large: luprex limit=", MAX_CONTENT_LENGTH));
|
||||
return false;
|
||||
}
|
||||
if (chunk_size == 0) break;
|
||||
total_size += chunk_size;
|
||||
if (total_size > MAX_CONTENT_LENGTH) {
|
||||
fail(500, "content too long");
|
||||
fail(413, util::ss("payload too large: luprex limit=", MAX_CONTENT_LENGTH));
|
||||
return false;
|
||||
}
|
||||
std::string_view chunk = sv::read_nbytes(view, chunk_size);
|
||||
@@ -631,7 +693,7 @@ bool HttpInResponse::parse_content_chunked(std::string_view &view, bool closed)
|
||||
}
|
||||
std::string_view newline = sv::read_to_line(view);
|
||||
if (!newline.empty()) {
|
||||
fail(500, "corrupted chunk encoding");
|
||||
fail(500, "internal server error: corrupted chunk encoding");
|
||||
return false;
|
||||
}
|
||||
if (sv::isnull(view)) {
|
||||
@@ -665,7 +727,7 @@ void HttpInResponse::parse(const StreamBuffer *sb, bool closed) {
|
||||
string_view scode = sv::read_to_space(status);
|
||||
int64_t code = sv::to_int64(scode, 0);
|
||||
if ((code < 100) || (code > 599)) {
|
||||
fail(500, util::ss("protocol error: invalid response code: ", scode));
|
||||
fail(500, util::ss("internal server error: invalid response code: ", scode));
|
||||
}
|
||||
status_code_ = code;
|
||||
|
||||
@@ -690,11 +752,11 @@ void HttpInResponse::parse(const StreamBuffer *sb, bool closed) {
|
||||
}
|
||||
eng::string command = util::ascii_tolower(sv::trim(sv::read_to_sep(header, ':')));
|
||||
if (sv::isnull(header)) {
|
||||
fail(500, util::ss("protocol error: no colon in header line: ", command));
|
||||
fail(500, util::ss("internal server error: no colon in header line: ", command));
|
||||
return;
|
||||
}
|
||||
if (!words_separated_by_dashes(command)) {
|
||||
fail(500, util::ss("protocol error: invalid header: ", command));
|
||||
fail(500, util::ss("internal server error: invalid header: ", command));
|
||||
return;
|
||||
}
|
||||
parse_header(command, sv::trim(header));
|
||||
@@ -716,7 +778,7 @@ void HttpInResponse::parse(const StreamBuffer *sb, bool closed) {
|
||||
// If it's not a redirect, disallow 'location'.
|
||||
if ((status_code_ < 300) || (status_code_ > 399)) {
|
||||
if (!location_.empty()) {
|
||||
fail(500, util::ss("redirect specified, but result code not 300-399: ", code));
|
||||
fail(500, util::ss("internal server error: location specified, but result code not 300-399: ", code));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -734,18 +796,18 @@ void HttpInResponse::parse(const StreamBuffer *sb, bool closed) {
|
||||
|
||||
// If it's multipart, reject it.
|
||||
if (sv::has_prefix(mime_type_, "multipart/")) {
|
||||
fail(500, "multipart messages not implemented");
|
||||
fail(406, "not acceptable: multipart messages not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's text, demand a reasonable charset. Otherwise,
|
||||
// ignore the charset.
|
||||
if (sv::has_prefix(mime_type_, "text/")) {
|
||||
if (charset_.empty()) {
|
||||
if (charset_.empty() || (charset_ == "ascii")) {
|
||||
charset_ = "utf-8";
|
||||
}
|
||||
if (charset_ != "utf-8") {
|
||||
fail(500, util::ss("charset not supported: ", charset_));
|
||||
fail(406, util::ss("not acceptable: charset not supported: ", charset_));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -755,7 +817,7 @@ void HttpInResponse::parse(const StreamBuffer *sb, bool closed) {
|
||||
// Uncompress the content.
|
||||
if ((content_encoding_ == "") || (content_encoding_ == "identity")) {
|
||||
} else {
|
||||
fail(500, util::ss("content-encoding not supported: ", content_encoding_));
|
||||
fail(406, util::ss("not acceptable: content-encoding not supported: ", content_encoding_));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -767,7 +829,7 @@ void HttpInResponse::parse(const StreamBuffer *sb, bool closed) {
|
||||
}
|
||||
}
|
||||
|
||||
void HttpInResponse::store(LuaStack &LS0, LuaSlot tab) {
|
||||
void HttpInResponse::store(LuaStack &LS0, LuaSlot tab) const {
|
||||
LuaStack LS(LS0.state());
|
||||
|
||||
LS.newtable(tab);
|
||||
@@ -784,10 +846,24 @@ void HttpInResponse::store(LuaStack &LS0, LuaSlot tab) {
|
||||
}
|
||||
|
||||
// Debugging fields. Do not use for lua programming.
|
||||
LS.rawset(tab, "dbg-content-length", content_length_);
|
||||
LS.rawset(tab, "dbg-transfer-encoding", transfer_encoding_);
|
||||
LS.rawset(tab, "dbg-charset", charset_);
|
||||
LS.rawset(tab, "dbg-response-length", response_length_);
|
||||
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 (response_length_ != 0) {
|
||||
LS.rawset(tab, "dbg_responselength", response_length_);
|
||||
}
|
||||
}
|
||||
|
||||
void HttpInResponse::store_fail(LuaStack &LS, LuaSlot tab, int status_code, std::string_view error) {
|
||||
HttpInResponse response;
|
||||
response.fail(status_code, error);
|
||||
response.store(LS, tab);
|
||||
}
|
||||
|
||||
LuaDefine(http_fixurl, "url", "validate URL and repair minor flaws in the URL syntax") {
|
||||
@@ -804,36 +880,35 @@ LuaDefine(http_fixurl, "url", "validate URL and repair minor flaws in the URL sy
|
||||
}
|
||||
|
||||
|
||||
LuaDefine(http_request, "reqtab",
|
||||
"|Given an HTTP request in the form of a table, returns the same "
|
||||
"|request as a string, to assist with debugging."
|
||||
LuaDefine(http_request, "request",
|
||||
"|Takes an HTTP request in the form of a lua table."
|
||||
"|The table may contain these fields:"
|
||||
"|"
|
||||
"|The table can contain:"
|
||||
"|"
|
||||
"| method (ie, GET, HEAD, POST, etc)"
|
||||
"| method (ie, 'GET', 'POST', etc)"
|
||||
"| host (ie, 'google.com')"
|
||||
"| port (default: 443)"
|
||||
"| url (ie, '/index.html')"
|
||||
"| path (ie, '/index.html')"
|
||||
"| params (a table of url parameters)"
|
||||
"| verifycertificate (default: true)"
|
||||
"| url (ie, 'https://host:port/path.html?a=b&c=d')"
|
||||
"|"
|
||||
"|The url can start with 'https://', or with '/'. If it starts"
|
||||
"|with 'https://', then the URL includes the host and port, which"
|
||||
"|then must not be specified separately."
|
||||
"|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."
|
||||
"|"
|
||||
"|Note that plain HTTP is not supported - we only allow HTTPS."
|
||||
"|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 module will automatically url encode everything for you."
|
||||
"|Therefore, you shouldn't url encode anything, otherwise,"
|
||||
"|you'll end up double-encoding."
|
||||
"|"
|
||||
"|You cannot include url parameters as part of the url. If you try,"
|
||||
"|then your ?, &, and = characters will get url encoded, which will"
|
||||
"|cause them to not function. To use url parameters, you must"
|
||||
"|use the separate params table."
|
||||
"|") {
|
||||
"|This routine, http.request, 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);
|
||||
@@ -849,7 +924,48 @@ LuaDefine(http_request, "reqtab",
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(http_response, "text", "") {
|
||||
LuaDefine(http_response, "response",
|
||||
"|Returns an HTTP response in the form of a lua table."
|
||||
"|The table will contain these important fields:"
|
||||
"|"
|
||||
"| responsecode - 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_responselength - Total bytes in the response."
|
||||
"|"
|
||||
"|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"
|
||||
"| 406 (not acceptable) - the response used a feature we don't support yet"
|
||||
"| 413 (payload too large) - we refuse to download something so big"
|
||||
"| 425 (can't resume) - reloaded a save game with a pending request"
|
||||
"|"
|
||||
"|In this case, the error message will be the stock"
|
||||
"|error message, followed by more information."
|
||||
"|"
|
||||
"|This routine, http.response, generates a response by parsing"
|
||||
"|an actual HTTP response string. This is for debugging only.") {
|
||||
LuaArg text;
|
||||
LuaRet tab;
|
||||
LuaStack LS(L, text, tab);
|
||||
|
||||
@@ -24,6 +24,11 @@ using UrlParameters = eng::map<eng::string, eng::string>;
|
||||
|
||||
class HttpOutRequest : public eng::nevernew {
|
||||
private:
|
||||
// Request IDs.
|
||||
int64_t request_id_;
|
||||
int64_t place_id_;
|
||||
int64_t thread_id_;
|
||||
|
||||
// If the request contains an error, the error
|
||||
// message is stored here.
|
||||
eng::string error_;
|
||||
@@ -32,38 +37,36 @@ private:
|
||||
// True is the default.
|
||||
bool verify_certificate_;
|
||||
|
||||
// Method: GET, HEAD, POST, etc.
|
||||
// Method: GET, HEAD, POST, etc. Must be all-caps.
|
||||
eng::string method_;
|
||||
|
||||
// The hostname. This is used both for DNS lookup,
|
||||
// and to create an HTTP Host header.
|
||||
// The hostname. Not yet url-encoded.
|
||||
eng::string host_;
|
||||
|
||||
// Port number.
|
||||
int port_;
|
||||
|
||||
// You may specify either path or encoded_path.
|
||||
// The path is not url-encoded, and must not include URL parameters.
|
||||
// The path. Not yet url-encoded. Can not include URL parameters.
|
||||
eng::string path_;
|
||||
|
||||
// If params is nonempty, then we will add URL parameters
|
||||
// to the URL. The contents of the params field should not be
|
||||
// urlencoded, the urlencoding is done automatically when the
|
||||
// request is sent. If you specify encoded_path, then the
|
||||
// params must be empty, because the encoded path already contains
|
||||
// the params.
|
||||
// URL parameters to append to the path. Not yet url-encoded.
|
||||
UrlParameters params_;
|
||||
|
||||
private:
|
||||
void fail(std::string_view error);
|
||||
|
||||
void send_internal(StreamBuffer *target, bool debug_string) const;
|
||||
|
||||
public:
|
||||
// Construct an empty HTTP request.
|
||||
// All of the fields have empty values.
|
||||
HttpOutRequest();
|
||||
|
||||
// Get fields.
|
||||
// Get the request IDs.
|
||||
int64_t request_id() const { return request_id_; }
|
||||
int64_t place_id() const { return place_id_; }
|
||||
int64_t thread_id() const { return thread_id_; }
|
||||
|
||||
// Get request-related fields.
|
||||
const eng::string &error() const { return error_; }
|
||||
bool verify_certificate() const { return verify_certificate_; }
|
||||
const eng::string &method() const { return method_; }
|
||||
@@ -74,6 +77,11 @@ public:
|
||||
// Get the network target, eg, "cert:host:port"
|
||||
eng::string target() const;
|
||||
|
||||
// Set the request IDs.
|
||||
void set_request_id(int64_t request_id) { request_id_ = request_id; }
|
||||
void set_place_id(int64_t place_id) { place_id_ = place_id; }
|
||||
void set_thread_id(int64_t thread_id) { thread_id_ = thread_id; }
|
||||
|
||||
// Populate an HTTP request a piece at a time.
|
||||
// If you pass an invalid value, or if the field is
|
||||
// already set, the routine will generate an error message
|
||||
@@ -113,10 +121,20 @@ public:
|
||||
// Put the request into the stream, assuming HTTP/1.1
|
||||
void send(StreamBuffer *target) const { send_internal(target, false); }
|
||||
|
||||
// Serialize and deserialize.
|
||||
void serialize(StreamBuffer *sb) const;
|
||||
void deserialize(StreamBuffer *sb);
|
||||
|
||||
// Get the request as a debug string.
|
||||
eng::string DebugString();
|
||||
};
|
||||
|
||||
class HttpOutRequestMap : public eng::map<int64_t, HttpOutRequest> {
|
||||
public:
|
||||
void serialize(StreamBuffer *sb) const;
|
||||
void deserialize(StreamBuffer *sb);
|
||||
};
|
||||
|
||||
class HttpInResponse {
|
||||
private:
|
||||
// The HTTP response status code.
|
||||
@@ -201,10 +219,13 @@ public:
|
||||
void parse(const StreamBuffer *sb, bool closed);
|
||||
|
||||
// Convert the HTTP response to a lua table.
|
||||
void store(LuaStack &LS, LuaSlot tab);
|
||||
void store(LuaStack &LS, LuaSlot tab) const;
|
||||
|
||||
// Convert to a debug string.
|
||||
eng::string DebugString() const;
|
||||
|
||||
// Synthesize an error response and store it in lua.
|
||||
static void store_fail(LuaStack &LS, LuaSlot tab, int status_code, std::string_view error);
|
||||
};
|
||||
|
||||
#endif // HTTP_HPP
|
||||
|
||||
@@ -91,6 +91,10 @@ public:
|
||||
stop_driver();
|
||||
}
|
||||
|
||||
void do_aborthttp_command(const util::StringVec &words) {
|
||||
master_->abort_all_http_requests(425, "http requests aborted from the server command line");
|
||||
}
|
||||
|
||||
void do_command(const util::StringVec &words) {
|
||||
if (words.empty()) return;
|
||||
else if (words[0] == "luainvoke") do_luainvoke_command(words);
|
||||
@@ -101,6 +105,7 @@ public:
|
||||
else if (words[0] == "tick") do_tick_command(words);
|
||||
else if (words[0] == "cpl") do_cpl_command(words);
|
||||
else if (words[0] == "quit") do_quit_command(words);
|
||||
else if (words[0] == "aborthttp") do_aborthttp_command(words);
|
||||
else {
|
||||
stdostream() << "Unsupported command: " << words[0] << std::endl;
|
||||
}
|
||||
|
||||
@@ -94,6 +94,10 @@ void LuaConsole::simplify(const StringVec &words) {
|
||||
if (words.size() != 1) {
|
||||
synerr("/work takes no arguments");
|
||||
}
|
||||
} else if (words[0] == "aborthttp") {
|
||||
if (words.size() != 1) {
|
||||
synerr("/aborthttp takes no arguments");
|
||||
}
|
||||
} else {
|
||||
synerr("unrecognized command");
|
||||
}
|
||||
|
||||
@@ -295,6 +295,9 @@ LuaDefine(tangible_scan, "plane,x,y,radius,omit_nowhere",
|
||||
LuaDefine(wait, "nticks",
|
||||
"|Wait the specified number of ticks.") {
|
||||
World *w = World::fetch_global_pointer(L);
|
||||
w->guard_blockable(L, "wait");
|
||||
|
||||
// Parse the argument.
|
||||
LuaArg seconds;
|
||||
LuaStack LS(L, seconds);
|
||||
int64_t n = LS.ckinteger(seconds);
|
||||
@@ -302,30 +305,19 @@ LuaDefine(wait, "nticks",
|
||||
luaL_error(L, "Argument to wait must be between 0 and 1000000");
|
||||
return LS.result();
|
||||
}
|
||||
if (!lua_isyieldable(L)) {
|
||||
// in a probe, wait throws an error.
|
||||
luaL_error(L, "cannot wait in a probe.");
|
||||
return LS.result();
|
||||
} else if (!w->is_authoritative()) {
|
||||
// in a nonauth model, yield is converted to nopredict.
|
||||
lua_yield(L, 0);
|
||||
return LS.result();
|
||||
} else {
|
||||
// in an authoritative model, wait schedules a continuation.
|
||||
|
||||
// Schedule a continuation.
|
||||
w->schedule(w->clock_ + n, w->lthread_thread_id_, w->lthread_place_id_);
|
||||
lua_yield(L, 0);
|
||||
return LS.result();
|
||||
}
|
||||
return lua_yield(L, 0);
|
||||
}
|
||||
|
||||
LuaDefine(nopredict, "",
|
||||
"|Stop predictive execution of this thread.") {
|
||||
if (lua_gettop(L) != 0) {
|
||||
luaL_error(L, "tangible.nopredict takes no arguments");
|
||||
}
|
||||
World *w = World::fetch_global_pointer(L);
|
||||
if (lua_isyieldable(L) && !w->is_authoritative()) {
|
||||
return lua_yield(L, 0);
|
||||
w->guard_nopredict(L, "nopredict");
|
||||
|
||||
if (lua_gettop(L) != 0) {
|
||||
luaL_error(L, "nopredict takes no arguments");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -530,3 +522,35 @@ LuaDefine(doc, "function",
|
||||
(*ostream) << doc;
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(http_get, "request",
|
||||
"|Make an HTTP GET request. Returns an HTTP response."
|
||||
"|See doc(http.request) and doc(http.response).") {
|
||||
World *w = World::fetch_global_pointer(L);
|
||||
w->guard_blockable(L, "http.get");
|
||||
|
||||
LuaArg request;
|
||||
LuaRet response;
|
||||
LuaStack LS(L, request, response);
|
||||
HttpOutRequest req;
|
||||
|
||||
// Parse the request and make sure it's valid.
|
||||
req.set_config(LS, request);
|
||||
req.set_defaults();
|
||||
eng::string error = req.check();
|
||||
if (!error.empty()) {
|
||||
HttpInResponse::store_fail(LS, response, 400, util::ss("bad request: ", error));
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
// Give the request an ID.
|
||||
req.set_request_id(w->id_global_pool_.get_one());
|
||||
req.set_place_id(w->lthread_place_id_);
|
||||
req.set_thread_id(w->lthread_thread_id_);
|
||||
|
||||
// Store it in the global request table.
|
||||
w->http_requests_[req.request_id()] = req;
|
||||
|
||||
// Block.
|
||||
return lua_yield(L, 0);
|
||||
}
|
||||
@@ -393,6 +393,66 @@ void World::update_source(const util::LuaSourceVec &source) {
|
||||
assert(stack_is_clear());
|
||||
}
|
||||
|
||||
void World::http_response(int64_t request_id, const HttpInResponse &response) {
|
||||
// Find the request.
|
||||
auto iter = http_requests_.find(request_id);
|
||||
if (iter == http_requests_.end()) {
|
||||
return;
|
||||
}
|
||||
HttpOutRequest request = iter->second;
|
||||
http_requests_.erase(iter);
|
||||
|
||||
// Get the place and thread as lua objects.
|
||||
LuaVar tangibles, place, mt, threads, thinfo, thread;
|
||||
LuaStack LS(state(), tangibles, place, mt, threads, thinfo, thread);
|
||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||
LS.rawget(place, tangibles, request.place_id());
|
||||
if (!LS.istable(place)) {
|
||||
return;
|
||||
}
|
||||
LS.getmetatable(mt, place);
|
||||
if (!LS.istable(mt)) {
|
||||
return;
|
||||
}
|
||||
LS.rawget(threads, mt, "threads");
|
||||
if (!LS.istable(threads)) {
|
||||
return;
|
||||
}
|
||||
LS.rawget(thinfo, threads, request.thread_id());
|
||||
if (!LS.istable(thinfo)) {
|
||||
return;
|
||||
}
|
||||
LS.rawget(thread, thinfo, "thread");
|
||||
if (!LS.isthread(thread)) {
|
||||
return;
|
||||
}
|
||||
lua_State *CO = LS.ckthread(thread);
|
||||
|
||||
// Push the response onto the awakening thread.
|
||||
LuaRet responsetable;
|
||||
LuaStack LSCO(CO, responsetable);
|
||||
response.store(LSCO, responsetable);
|
||||
|
||||
// Clean up lua stacks.
|
||||
LSCO.result();
|
||||
LS.result();
|
||||
assert(stack_is_clear());
|
||||
|
||||
// Awaken the thread, with its new return value.
|
||||
schedule(0, request.thread_id(), request.place_id());
|
||||
run_scheduled_threads();
|
||||
}
|
||||
|
||||
void World::abort_all_http_requests(int status_code, std::string_view error) {
|
||||
HttpInResponse abortresponse;
|
||||
abortresponse.fail(status_code, error);
|
||||
while (true) {
|
||||
auto iter = http_requests_.begin();
|
||||
if (iter == http_requests_.end()) break;
|
||||
http_response(iter->second.request_id(), abortresponse);
|
||||
}
|
||||
}
|
||||
|
||||
void World::run_unittests() {
|
||||
assert(stack_is_clear());
|
||||
source_db_.run_unittests();
|
||||
@@ -635,6 +695,30 @@ void World::invoke_lua_source(int64_t actor_id, int64_t place_id, const eng::str
|
||||
}
|
||||
}
|
||||
|
||||
void World::guard_blockable(lua_State *L, const char *fn) {
|
||||
if (lthread_thread_id_ == 0) {
|
||||
// in a probe, http.get throws an error.
|
||||
luaL_error(L, "cannot %s in a probe", fn);
|
||||
assert(false);
|
||||
}
|
||||
if (!is_authoritative()) {
|
||||
// in a nonauth model, http.get is converted to nopredict.
|
||||
lua_yield(L, 0);
|
||||
luaL_error(L, "unexplained nopredict failure in %s", fn);
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
void World::guard_nopredict(lua_State *L, const char *fn) {
|
||||
if (lthread_thread_id_ == 0) {
|
||||
return;
|
||||
}
|
||||
if (!is_authoritative()) {
|
||||
lua_yield(L, 0);
|
||||
luaL_error(L, "unexplained nopredict failure in %s", fn);
|
||||
}
|
||||
}
|
||||
|
||||
void World::schedule(int64_t clk, int64_t thid, int64_t plid) {
|
||||
assert(is_authoritative());
|
||||
thread_sched_.add(clk, thid, plid);
|
||||
@@ -796,6 +880,7 @@ void World::serialize(StreamBuffer *sb) {
|
||||
id_global_pool_.serialize(sb);
|
||||
sb->write_int64(clock_);
|
||||
thread_sched_.serialize(sb);
|
||||
http_requests_.serialize(sb);
|
||||
sb->write_uint32(tangibles_.size());
|
||||
for (const auto &p : tangibles_) {
|
||||
sb->write_int64(p.first);
|
||||
@@ -813,6 +898,7 @@ void World::deserialize(StreamBuffer *sb) {
|
||||
id_global_pool_.deserialize(sb);
|
||||
clock_ = sb->read_int64();
|
||||
thread_sched_.deserialize(sb);
|
||||
http_requests_.deserialize(sb);
|
||||
// Mark all tangibles for deletion by setting ID to zero.
|
||||
for (const auto &p : tangibles_) {
|
||||
p.second->plane_item_.set_id(0);
|
||||
@@ -837,6 +923,8 @@ void World::deserialize(StreamBuffer *sb) {
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
// After a save and load, http requests no longer should exist
|
||||
abort_all_http_requests(425, "http requests aborted by loading a save game");
|
||||
assert(stack_is_clear());
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "debugcollector.hpp"
|
||||
#include "printbuffer.hpp"
|
||||
#include "sched.hpp"
|
||||
#include "http.hpp"
|
||||
#include "source.hpp"
|
||||
#include "gui.hpp"
|
||||
#include "luasnap.hpp"
|
||||
@@ -188,6 +189,9 @@ public:
|
||||
// This is the primary dispatcher for all operations that mutate a world model.
|
||||
// To mutate a world model, create an invocation, then invoke it.
|
||||
//
|
||||
// It is legal to mutate a world model without using 'Invoke', but
|
||||
// only in authoritative world models.
|
||||
//
|
||||
void invoke(const Invocation &inv);
|
||||
|
||||
// Get the PrintBuffer of the actor.
|
||||
@@ -201,6 +205,14 @@ public:
|
||||
void update_source(const util::LuaSourcePtr &source);
|
||||
void update_source(const util::LuaSourceVec &source);
|
||||
|
||||
// Supply an HTTP response to an outstanding HTTP request.
|
||||
void http_response(int64_t request_id, const HttpInResponse &response);
|
||||
|
||||
// Abort all HTTP requests. This is typically used after
|
||||
// reloading a world from a save-game. The http requests that
|
||||
// were in progress are long-since dead.
|
||||
void abort_all_http_requests(int status_code, std::string_view error);
|
||||
|
||||
// Run all unit tests.
|
||||
//
|
||||
void run_unittests();
|
||||
@@ -216,6 +228,10 @@ public:
|
||||
//
|
||||
bool is_authoritative() const { return (world_type_ == util::WORLD_TYPE_MASTER) || (world_type_ == util::WORLD_TYPE_STANDALONE); }
|
||||
|
||||
// Get a table showing all outstanding HTTP requests.
|
||||
//
|
||||
const HttpOutRequestMap &http_requests() const { return http_requests_; }
|
||||
|
||||
// Serialize and deserialize.
|
||||
//
|
||||
void serialize(StreamBuffer *sb);
|
||||
@@ -263,6 +279,16 @@ public:
|
||||
//
|
||||
int64_t alloc_id_predictable();
|
||||
|
||||
// If we're in a probe, generate an error.
|
||||
// If we're in a nonauthoritative model, do a nopredict yield.
|
||||
// Otherwise, return.
|
||||
void guard_blockable(lua_State *L, const char *fn);
|
||||
|
||||
// If we're in a probe, return.
|
||||
// If we're in a nonauthoritative model, do a nopredict yield.
|
||||
// Otherwise, return.
|
||||
void guard_nopredict(lua_State *L, const char *fn);
|
||||
|
||||
private:
|
||||
// Add a thread to the scheduler queue.
|
||||
//
|
||||
@@ -478,6 +504,7 @@ private:
|
||||
eng::unordered_map<int64_t, UniqueTangible> tangibles_;
|
||||
|
||||
// Current time.
|
||||
//
|
||||
int64_t clock_;
|
||||
|
||||
// Thread schedule: must include every thread, except
|
||||
@@ -485,7 +512,13 @@ private:
|
||||
//
|
||||
Schedule thread_sched_;
|
||||
|
||||
// Outstanding HTTP requests, indexed by request ID.
|
||||
// Authoritative models only.
|
||||
//
|
||||
HttpOutRequestMap http_requests_;
|
||||
|
||||
// Serialized snapshot of world model.
|
||||
//
|
||||
StreamBuffer snapshot_;
|
||||
|
||||
// Redirects.
|
||||
@@ -513,6 +546,7 @@ private:
|
||||
friend int lfn_math_randomstate(lua_State *L);
|
||||
friend int lfn_wait(lua_State *L);
|
||||
friend int lfn_nopredict(lua_State *L);
|
||||
friend int lfn_http_get(lua_State *L);
|
||||
};
|
||||
|
||||
using UniqueWorld = std::unique_ptr<World>;
|
||||
|
||||
@@ -42,3 +42,9 @@ function setfoo(n)
|
||||
function buildq()
|
||||
return tangible.build{class="login", x=10, y=0, z=0, plane="nowhere", graphic="what"}
|
||||
end
|
||||
|
||||
function htest()
|
||||
print("hi")
|
||||
pprint(http.get{url="https://mit.edu/"})
|
||||
print("bye")
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user