summaryrefslogtreecommitdiffstats
path: root/BrowserSource/Proxy/WebServer.cpp
diff options
context:
space:
mode:
authoryum <yum.food.vr@gmail.com>2023-07-03 18:44:43 -0700
committeryum <yum.food.vr@gmail.com>2023-07-03 19:36:13 -0700
commit76ae7c28ea6224b2c919122d5dc71bcc00a0ecaa (patch)
tree9723fd02715d747cfc439d1f66d36821a56069e9 /BrowserSource/Proxy/WebServer.cpp
parent7888ccc96d001512dd3bdc01f299856e86c876f5 (diff)
Begin work on proxy server
Create a simple server with 3 endpoints: * /create_session: Create a session and return its identifier. * /set_transcript: Update a session's transcript. * /get_transcript: Fetch a session's transcript. Right now the session ID provides authentication *and* authorization. There is no public/private ID so you have to trust whoever you share your ID with. IDs are long and generated by the server, so it should be somewhat secure against low-effort hacking. Other updates: * Drop whisper_requirements.txt - no longer needed. * Vendor curl to make it easier to interact with the server. TODO: * Fuzz test the server.
Diffstat (limited to 'BrowserSource/Proxy/WebServer.cpp')
-rw-r--r--BrowserSource/Proxy/WebServer.cpp226
1 files changed, 226 insertions, 0 deletions
diff --git a/BrowserSource/Proxy/WebServer.cpp b/BrowserSource/Proxy/WebServer.cpp
new file mode 100644
index 0000000..fab23d9
--- /dev/null
+++ b/BrowserSource/Proxy/WebServer.cpp
@@ -0,0 +1,226 @@
+#include "HTTPMapper.h"
+#include "HTTPParser.h"
+#include "Logging.h"
+#include "ScopeGuard.h"
+#include "WebServer.h"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <netinet/in.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+using ::Logging::Log;
+
+namespace WebServer {
+ WebServer::WebServer(uint16_t port)
+ : port_(port)
+ {
+ default_handler_ =
+ [](int& status_code, std::string& payload,
+ ContentType& type) -> void {
+ status_code = 404;
+ payload = "404: No route to URI";
+ type = HTML;
+ };
+ }
+
+ bool WebServer::RegisterPathHandler(const std::string& method,
+ const std::string& path, handler_t&& handler) {
+ dispatch_key_t key = GetDispatchKey(method, path);
+ if (dispatch_map_.contains(key)) {
+ Log("Failed to register path handler at {} {}: "
+ "Handler already exists!\n", method, path);
+ return false;
+ }
+
+ dispatch_map_[key] = std::move(handler);
+ return true;
+ }
+
+ void WebServer::RegisterDefaultHandler(handler_t&& handler) {
+ default_handler_ = std::move(handler);
+ }
+
+ bool WebServer::Run(volatile bool* run) {
+ int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+ if (sock == -1) {
+ Log("Failed to create socket: {}\n", strerror(errno));
+ return false;
+ }
+ ScopeGuard sock_cleanup([sock]() { close(sock); });
+
+ sockaddr_in saddr;
+ saddr.sin_family = AF_INET;
+ saddr.sin_addr.s_addr = INADDR_ANY;
+ saddr.sin_port = htons(port_);
+ if (bind(sock, (sockaddr*)&saddr, sizeof(saddr)) == -1) {
+ Log("Failed to bind to port {}: {}\n", port_, strerror(errno));
+ return false;
+ }
+
+ // enable non-blocking mode
+ int flags = fcntl(sock, F_GETFL, 0);
+ if (flags == -1) {
+ Log("Failed to get socket flags for port {}: {}\n", port_, strerror(errno));
+ return false;
+ }
+ if (fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1) {
+ Log("Failed to enable non-blocking mode on socket {}: {}\n", port_, strerror(errno));
+ return false;
+ }
+
+ if (listen(sock, SOMAXCONN) == -1) {
+ Log("Failed to listen on port {}: {}\n", port_, strerror(errno));
+ return false;
+ }
+
+ struct sockaddr_in bound_addr;
+ socklen_t len = sizeof(bound_addr);
+ if (getsockname(sock, (struct sockaddr *)&bound_addr, &len) == -1) {
+ Log("Failed to get socket name: {}\n", strerror(errno));
+ return false;
+ }
+ char ipstr[INET_ADDRSTRLEN];
+ inet_ntop(AF_INET, &bound_addr.sin_addr, ipstr, sizeof(ipstr));
+
+ Log("Server running on IP {} port {}\n", ipstr, port_);
+
+ sockaddr_in peer_addr;
+ int accept_cnt = 0;
+ while (*run) {
+ socklen_t peer_addr_sz = sizeof(peer_addr);
+ int csock = accept(sock, (sockaddr*)&peer_addr, &peer_addr_sz);
+ if (csock == -1) {
+ if (errno == EWOULDBLOCK) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ continue;
+ }
+ Log("Accept failed: {}\n", strerror(errno));
+ return false;
+ }
+
+ // enable non-blocking mode
+ int flags = fcntl(csock, F_GETFL, 0);
+ if (flags == -1) {
+ Log("Failed to get socket flags for client on port {}: {}\n", port_, strerror(errno));
+ return false;
+ }
+ if (fcntl(csock, F_SETFL, flags | O_NONBLOCK) == -1) {
+ Log("Failed to enable non-blocking mode on client socket {}: {}\n", port_, strerror(errno));
+ return false;
+ }
+
+ // Periodically cull dead connections to prevent runaway memory usage.
+ ++accept_cnt;
+ if (accept_cnt % 10 == 0) {
+ std::vector<std::future<void>> alive_conn;
+ for (size_t i = 0; i < connections_.size(); i++) {
+ if (connections_[i].valid()) {
+ continue;
+ }
+ alive_conn.push_back(std::move(connections_[i]));
+ }
+ //Log("Culled {} dead connections\n", connections_.size() - alive_conn.size());
+ connections_ = std::move(alive_conn);
+ accept_cnt = 0; // Prevent overflow
+ }
+
+ const auto& dispatch_map = dispatch_map_;
+ const auto& default_handler = default_handler_;
+ connections_.push_back(std::async(std::launch::async,
+ [csock, peer_addr, run, dispatch_map, default_handler]() -> void {
+ ScopeGuard csock_cleanup([csock]() { close(csock); });
+ char peer_ip_str[INET_ADDRSTRLEN]{};
+ inet_ntop(AF_INET, &peer_addr.sin_addr, peer_ip_str, sizeof(peer_ip_str));
+
+ std::string buf(4096 * 16, 0);
+ int cur_bytes_read = 0;
+ int sum_bytes_read = 0;
+
+ // Drain socket until we see a valid HTTP message.
+ while (*run) {
+ cur_bytes_read = recv(csock, buf.data() + sum_bytes_read,
+ buf.size() - (1 + sum_bytes_read), /*flags=*/0);
+ if (cur_bytes_read == -1) {
+ if (errno == EWOULDBLOCK || errno == EAGAIN) {
+ // Client may try to keep the connection open,
+ // so see if there's a complete request in the
+ // buffer. If so, terminate the recv loop.
+ HTTPParser p;
+ std::string err;
+ if (p.Parse(buf, err)) {
+ // In general we should verify that we got a
+ // full message, but since we only need to
+ // support GET, this is unnecessary.
+ cur_bytes_read = 0;
+ break;
+ }
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ continue;
+ }
+ break;
+ }
+ sum_bytes_read += cur_bytes_read;
+ if (cur_bytes_read == 0) {
+ break;
+ }
+ }
+ if (cur_bytes_read == -1) {
+ Log("Failed to read client socket: {}\n", strerror(errno));
+ return;
+ }
+ // Edge case: Server was stopped in the middle of serving a request.
+ if (!*run) {
+ Log("Server stop requested, bail out!\n");
+ return;
+ }
+ buf.resize(sum_bytes_read);
+
+ // Parse HTTP. Expect this to succeed, since we only exit the loop once the
+ // request parses.
+ // TODO(yum) this repeats work! The loop already parsed the request.
+ HTTPParser p;
+ std::string err;
+ if (!p.Parse(buf, err)) {
+ Log("Failed to parse client request: {}\n", err);
+ Log("Offending request:\n{}\n", buf);
+ return;
+ }
+
+ // Find the dispatch handler for the requested method and path.
+ dispatch_key_t dispatch_key = GetDispatchKey(p.GetMethod(), p.GetPath());
+ auto iter = dispatch_map.find(dispatch_key);
+ handler_t handler;
+ if (iter == dispatch_map.end()) {
+ handler = default_handler;
+ } else {
+ handler = iter->second;
+ }
+
+ // Generate a response.
+ int status_code;
+ std::string payload = p.GetPayload();
+ ContentType type;
+ handler(status_code, payload, type);
+ std::string response = HTTPMapper().Map(status_code, payload, type);
+
+ // Send the response.
+ if (send(csock, response.data(), response.size(), /*flags=*/0) == -1) {
+ Log("Failed to send response to client: {}\n", strerror(errno));
+ return;
+ }
+
+ // Implicitly close the connection by exiting scope. We
+ // completely ignore keep-alive requests for now. Browsers
+ // should handle this well, there are many reasons why
+ // keep-alive requests may be ignored, such as transient
+ // network failures.
+ }));
+ }
+ return true;
+ }
+}