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