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

412 lines
12 KiB
C++

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