409 lines
13 KiB
C++
409 lines
13 KiB
C++
#include "AsyncHTTPServer.hh"
|
|
|
|
#include <inttypes.h>
|
|
#include <stdlib.h>
|
|
|
|
#include <phosg/Encoding.hh>
|
|
#include <phosg/Network.hh>
|
|
#include <phosg/Time.hh>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "AsyncUtils.hh"
|
|
#include "Loggers.hh"
|
|
#include "Revision.hh"
|
|
#include "Server.hh"
|
|
|
|
using namespace std;
|
|
|
|
static const unordered_map<int, const char*> explanation_for_response_code{
|
|
{100, "Continue"},
|
|
{101, "Switching Protocols"},
|
|
{102, "Processing"},
|
|
{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"},
|
|
{226, "IM Used"},
|
|
{300, "Multiple Choices"},
|
|
{301, "Moved Permanently"},
|
|
{302, "Found"},
|
|
{303, "See Other"},
|
|
{304, "Not Modified"},
|
|
{305, "Use Proxy"},
|
|
{307, "Temporary Redirect"},
|
|
{308, "Permanent Redirect"},
|
|
{400, "Bad Request"},
|
|
{401, "Unathorized"},
|
|
{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, "Request Entity Too Large"},
|
|
{414, "Request-URI Too Long"},
|
|
{415, "Unsupported Media Type"},
|
|
{416, "Requested Range Not Satisfiable"},
|
|
{417, "Expectation Failed"},
|
|
{418, "I\'m a Teapot"},
|
|
{420, "Enhance Your Calm"},
|
|
{422, "Unprocessable Entity"},
|
|
{423, "Locked"},
|
|
{424, "Failed Dependency"},
|
|
{426, "Upgrade Required"},
|
|
{428, "Precondition Required"},
|
|
{429, "Too Many Requests"},
|
|
{431, "Request Header Fields Too Large"},
|
|
{444, "No Response"},
|
|
{449, "Retry With"},
|
|
{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"},
|
|
{509, "Bandwidth Limit Exceeded"},
|
|
{510, "Not Extended"},
|
|
{511, "Network Authentication Required"},
|
|
{598, "Network Read Timeout Error"},
|
|
{599, "Network Connect Timeout Error"},
|
|
};
|
|
|
|
HTTPError::HTTPError(int code, const std::string& what)
|
|
: std::runtime_error(what), code(code) {}
|
|
|
|
const std::string* HTTPRequest::get_header(const std::string& name) const {
|
|
auto its = this->headers.equal_range(name);
|
|
if (its.first == its.second) {
|
|
return nullptr;
|
|
}
|
|
const string* ret = &its.first->second;
|
|
its.first++;
|
|
if (its.first != its.second) {
|
|
throw std::out_of_range("Header appears multiple times: " + name);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
const std::string* HTTPRequest::get_query_param(const std::string& name) const {
|
|
auto its = this->query_params.equal_range(name);
|
|
if (its.first == its.second) {
|
|
return nullptr;
|
|
}
|
|
const string* ret = &its.first->second;
|
|
its.first++;
|
|
if (its.first != its.second) {
|
|
throw std::out_of_range("Query parameter appears multiple times: " + name);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static void url_decode_inplace(string& s) {
|
|
size_t write_offset = 0, read_offset = 0;
|
|
for (; read_offset < s.size(); write_offset++) {
|
|
if ((s[read_offset] == '%') && (read_offset < s.size() - 2)) {
|
|
s[write_offset] =
|
|
static_cast<char>(phosg::value_for_hex_char(s[read_offset + 1]) << 4) |
|
|
static_cast<char>(phosg::value_for_hex_char(s[read_offset + 2]));
|
|
read_offset += 3;
|
|
} else if (s[write_offset] == '+') {
|
|
s[write_offset] = ' ';
|
|
read_offset++;
|
|
} else {
|
|
s[write_offset] = s[read_offset];
|
|
read_offset++;
|
|
}
|
|
}
|
|
s.resize(write_offset);
|
|
}
|
|
|
|
HTTPClient::HTTPClient(asio::ip::tcp::socket&& sock) : r(std::move(sock)) {}
|
|
|
|
asio::awaitable<HTTPRequest> HTTPClient::recv_http_request(size_t max_line_size, size_t max_body_size) {
|
|
HTTPRequest req;
|
|
std::string request_line = co_await this->r.read_line("\r\n", max_line_size);
|
|
auto line_tokens = phosg::split(request_line, ' ');
|
|
if (line_tokens.size() != 3) {
|
|
throw runtime_error("invalid HTTP request line");
|
|
}
|
|
const auto& method_token = line_tokens[0];
|
|
if (method_token == "GET") {
|
|
req.method = HTTPRequest::Method::GET;
|
|
} else if (method_token == "POST") {
|
|
req.method = HTTPRequest::Method::POST;
|
|
} else if (method_token == "DELETE") {
|
|
req.method = HTTPRequest::Method::DELETE;
|
|
} else if (method_token == "HEAD") {
|
|
req.method = HTTPRequest::Method::HEAD;
|
|
} else if (method_token == "PATCH") {
|
|
req.method = HTTPRequest::Method::PATCH;
|
|
} else if (method_token == "PUT") {
|
|
req.method = HTTPRequest::Method::PUT;
|
|
} else if (method_token == "UPDATE") {
|
|
req.method = HTTPRequest::Method::UPDATE;
|
|
} else if (method_token == "OPTIONS") {
|
|
req.method = HTTPRequest::Method::OPTIONS;
|
|
} else if (method_token == "CONNECT") {
|
|
req.method = HTTPRequest::Method::CONNECT;
|
|
} else if (method_token == "TRACE") {
|
|
req.method = HTTPRequest::Method::TRACE;
|
|
} else {
|
|
throw HTTPError(400, "Unknown request method");
|
|
}
|
|
|
|
req.http_version = std::move(line_tokens[2]);
|
|
|
|
size_t fragment_start_offset = line_tokens[1].find('#');
|
|
if (fragment_start_offset != string::npos) {
|
|
req.fragment = line_tokens[1].substr(fragment_start_offset + 1);
|
|
line_tokens[1].resize(fragment_start_offset);
|
|
}
|
|
|
|
size_t query_start_offset = line_tokens[1].find('?');
|
|
string query;
|
|
if (query_start_offset != string::npos) {
|
|
query = line_tokens[1].substr(query_start_offset + 1);
|
|
line_tokens[1].resize(query_start_offset);
|
|
}
|
|
|
|
req.path = std::move(line_tokens[1]);
|
|
if (req.path.empty()) {
|
|
throw std::runtime_error("request path is missing");
|
|
}
|
|
|
|
auto query_tokens = phosg::split(query, '&');
|
|
for (auto& token : query_tokens) {
|
|
size_t equals_pos = token.find('=');
|
|
if (equals_pos == string::npos) {
|
|
url_decode_inplace(token);
|
|
req.query_params.emplace(std::move(token), "");
|
|
} else {
|
|
string key = token.substr(0, equals_pos);
|
|
string value = token.substr(equals_pos + 1);
|
|
url_decode_inplace(key);
|
|
url_decode_inplace(value);
|
|
req.query_params.emplace(std::move(key), std::move(value));
|
|
}
|
|
}
|
|
|
|
auto prev_header_it = req.headers.end();
|
|
for (;;) {
|
|
std::string line = co_await this->r.read_line("\r\n", max_line_size);
|
|
if (line.empty()) {
|
|
break;
|
|
}
|
|
if (line[0] == ' ' || line[0] == '\t') {
|
|
if (prev_header_it == req.headers.end()) {
|
|
throw std::runtime_error("received header continuation line before any header");
|
|
} else {
|
|
phosg::strip_whitespace(line);
|
|
prev_header_it->second.append(1, ' ');
|
|
prev_header_it->second += line;
|
|
}
|
|
} else {
|
|
size_t colon_pos = line.find(':');
|
|
if (colon_pos == string::npos) {
|
|
throw runtime_error("malformed header line");
|
|
}
|
|
string key = line.substr(0, colon_pos);
|
|
string value = line.substr(colon_pos + 1);
|
|
phosg::strip_whitespace(key);
|
|
phosg::strip_whitespace(value);
|
|
prev_header_it = req.headers.emplace(phosg::tolower(key), std::move(value));
|
|
}
|
|
}
|
|
|
|
auto transfer_encoding_header = req.get_header("transfer-encoding");
|
|
if (transfer_encoding_header && phosg::tolower(*transfer_encoding_header) == "chunked") {
|
|
deque<string> chunks;
|
|
size_t total_data_bytes = 0;
|
|
for (;;) {
|
|
auto line = co_await this->r.read_line("\r\n", 0x20);
|
|
size_t parse_offset = 0;
|
|
size_t chunk_size = stoull(line, &parse_offset, 16);
|
|
if (parse_offset != line.size()) {
|
|
throw HTTPError(400, "Invalid chunk header during chunked encoding");
|
|
}
|
|
if (chunk_size == 0) {
|
|
break;
|
|
}
|
|
total_data_bytes += chunk_size;
|
|
if (total_data_bytes > max_body_size) {
|
|
throw HTTPError(400, "Request data size too large");
|
|
}
|
|
chunks.emplace_back(co_await this->r.read_data(chunk_size));
|
|
auto after_chunk_data = co_await this->r.read_line("\r\n", 0x20);
|
|
if (!after_chunk_data.empty()) {
|
|
throw HTTPError(400, "Incorrect trailing sequence after chunk data");
|
|
}
|
|
}
|
|
} else {
|
|
auto content_length_header = req.get_header("content-length");
|
|
size_t content_length = content_length_header ? stoull(*content_length_header) : 0;
|
|
if (content_length > max_body_size) {
|
|
throw HTTPError(400, "Request data size too large");
|
|
} else if (content_length > 0) {
|
|
req.data = co_await this->r.read_data(content_length);
|
|
}
|
|
}
|
|
|
|
co_return req;
|
|
}
|
|
|
|
asio::awaitable<void> HTTPClient::send_http_response(const HTTPResponse& resp) {
|
|
AsyncWriteCollector w;
|
|
w.add(std::format("{} {} {}\r\n",
|
|
resp.http_version, resp.response_code, explanation_for_response_code.at(resp.response_code)));
|
|
for (const auto& it : resp.headers) {
|
|
w.add(it.first + ": " + it.second + "\r\n");
|
|
}
|
|
if (!resp.data.empty()) {
|
|
w.add(std::format("Content-Length: {}\r\n", resp.data.size()));
|
|
}
|
|
w.add("\r\n");
|
|
if (!resp.data.empty()) {
|
|
w.add_reference(resp.data.data(), resp.data.size());
|
|
}
|
|
co_await w.write(this->r.get_socket());
|
|
}
|
|
|
|
asio::awaitable<WebSocketMessage> HTTPClient::recv_websocket_message(size_t max_data_size) {
|
|
WebSocketMessage prev_msg;
|
|
bool prev_msg_present = false;
|
|
|
|
while (this->r.get_socket().is_open()) {
|
|
WebSocketMessage msg;
|
|
|
|
// We need at most 10 bytes to determine if there's a valid frame, or as little as 2
|
|
co_await this->r.read_data_into(msg.header, 2);
|
|
|
|
// Get the payload size
|
|
bool has_mask = msg.header[1] & 0x80;
|
|
size_t payload_size = msg.header[1] & 0x7F;
|
|
if (payload_size == 0x7F) {
|
|
phosg::be_uint64_t wire_size;
|
|
co_await this->r.read_data_into(&wire_size, sizeof(wire_size));
|
|
payload_size = wire_size;
|
|
} else if (payload_size == 0x7E) {
|
|
phosg::be_uint16_t wire_size;
|
|
co_await this->r.read_data_into(&wire_size, sizeof(wire_size));
|
|
payload_size = wire_size;
|
|
}
|
|
|
|
if (payload_size > max_data_size) {
|
|
throw runtime_error("Incoming WebSocket message exceeds size limit");
|
|
}
|
|
|
|
// Read the masking key if present
|
|
if (has_mask) {
|
|
co_await this->r.read_data_into(msg.mask_key, sizeof(msg.mask_key));
|
|
}
|
|
|
|
// Read and unmask message data
|
|
msg.data = co_await this->r.read_data(payload_size);
|
|
if (has_mask) {
|
|
for (size_t x = 0; x < msg.data.size(); x++) {
|
|
msg.data[x] ^= msg.mask_key[x & 3];
|
|
}
|
|
}
|
|
|
|
this->last_communication_time = phosg::now();
|
|
|
|
// If the current message is a control message, respond appropriately
|
|
// (these can be sent in the middle of fragmented messages)
|
|
uint8_t opcode = msg.header[0] & 0x0F;
|
|
if (opcode & 0x08) {
|
|
if (opcode == 0x0A) {
|
|
// Ping response; ignore it
|
|
|
|
} else if (opcode == 0x08) {
|
|
// Close message
|
|
co_await this->send_websocket_message(msg.data, msg.opcode);
|
|
this->r.close();
|
|
|
|
} else if (opcode == 0x09) {
|
|
// Ping message
|
|
co_await this->send_websocket_message(msg.data, 0x0A);
|
|
|
|
} else {
|
|
// Unknown control message type
|
|
this->r.close();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// If there's an existing fragment, the current message's opcode should be
|
|
// zero; if there's no pending message, it must not be zero
|
|
if (prev_msg_present == (opcode != 0)) {
|
|
this->r.close();
|
|
continue;
|
|
}
|
|
|
|
// Save the message opcode, if present, and append the frame data
|
|
if (!prev_msg_present) {
|
|
prev_msg = std::move(msg);
|
|
} else {
|
|
prev_msg.header[0] = msg.header[0];
|
|
prev_msg.header[1] = msg.header[1];
|
|
if (opcode) {
|
|
prev_msg.opcode = msg.opcode;
|
|
}
|
|
if (has_mask) {
|
|
prev_msg.mask_key[0] = msg.mask_key[0];
|
|
prev_msg.mask_key[1] = msg.mask_key[1];
|
|
prev_msg.mask_key[2] = msg.mask_key[2];
|
|
prev_msg.mask_key[3] = msg.mask_key[3];
|
|
}
|
|
prev_msg.data += msg.data;
|
|
}
|
|
|
|
// If the FIN bit is set, then the frame is complete - append the payload
|
|
// to any pending payloads and call the message handler. If the FIN bit
|
|
// isn't set, we need to receive at least one continuation frame to
|
|
// complete the message.
|
|
if (prev_msg.header[0] & 0x80) {
|
|
co_return prev_msg;
|
|
}
|
|
}
|
|
|
|
throw logic_error("failed to receive websocket message");
|
|
}
|
|
|
|
asio::awaitable<void> HTTPClient::send_websocket_message(const void* data, size_t size, uint8_t opcode) {
|
|
phosg::StringWriter w;
|
|
w.put_u8(0x80 | (opcode & 0x0F));
|
|
if (size > 0xFFFF) {
|
|
w.put_u8(0x7F);
|
|
w.put_u64b(size);
|
|
} else if (size > 0x7D) {
|
|
w.put_u8(0x7E);
|
|
w.put_u16b(size);
|
|
} else {
|
|
w.put_u8(size);
|
|
}
|
|
|
|
array<asio::const_buffer, 2> bufs = {asio::const_buffer(w.data(), w.size()), asio::const_buffer(data, size)};
|
|
co_await asio::async_write(this->r.get_socket(), bufs, asio::use_awaitable);
|
|
}
|
|
|
|
asio::awaitable<void> HTTPClient::send_websocket_message(const std::string& data, uint8_t opcode) {
|
|
return this->send_websocket_message(data.data(), data.size(), opcode);
|
|
}
|
|
|
|
const HTTPServerLimits DEFAULT_HTTP_LIMITS;
|