From a8d1b69d2afbb6ba43c3f97e383dc71b097f69fd Mon Sep 17 00:00:00 2001 From: yum Date: Sat, 25 Feb 2023 16:46:34 -0800 Subject: Complete OBS browser source * Implement HTTPMapper classes * Browser source respects user-configured source port --- GUI/GUI/GUI/BrowserSource.cpp | 52 +++++++++++++++++++++++++++- GUI/GUI/GUI/HTTPMapper.cpp | 69 +++++++++++++++++++++++++++++++++++++ GUI/GUI/GUI/HTTPMapper.h | 34 +++++++++++++++++++ GUI/GUI/GUI/WebCommon.h | 2 +- GUI/GUI/GUI/WebServer.cpp | 79 ++++++++++++++++++++++++++++++++++++------- GUI/GUI/GUI/WebServer.h | 5 +-- 6 files changed, 225 insertions(+), 16 deletions(-) (limited to 'GUI') diff --git a/GUI/GUI/GUI/BrowserSource.cpp b/GUI/GUI/GUI/BrowserSource.cpp index aa96f46..c43f1a0 100644 --- a/GUI/GUI/GUI/BrowserSource.cpp +++ b/GUI/GUI/GUI/BrowserSource.cpp @@ -1,10 +1,10 @@ #include "BrowserSource.h" #include "Logging.h" #include "ScopeGuard.h" +#include "WebCommon.h" #include "WebServer.h" using ::Logging::Log; -//using ::WebServer::WebServer; BrowserSource::BrowserSource(uint16_t port, wxTextCtrl *out, Transcript *transcript) : port_(port), out_(out), transcript_(transcript) @@ -13,6 +13,56 @@ BrowserSource::BrowserSource(uint16_t port, wxTextCtrl *out, Transcript *transcr void BrowserSource::Run(volatile bool* run) { WebServer::WebServer ws(out_, port_); + + ws.RegisterPathHandler("GET", "/", + [&](int& status_code, std::string& payload, + WebServer::ContentType& type) -> void { + auto html_path = std::filesystem::path("Resources/BrowserSource/index.html"); + + std::ifstream html_ifs(html_path); + std::vector resp(4096 * 16, 0); + html_ifs.read(resp.data(), resp.size()); + + std::string html(resp.data()); + resp.clear(); + + size_t pos = 0; + std::string key = "%PORT%"; + std::string value = std::to_string(port_); + while ((pos = html.find("%PORT%", pos)) != std::string::npos) { + html.replace(pos, key.size(), value); + pos += value.size(); + } + + status_code = 200; + payload = html; + type = WebServer::HTML; + }); + + ws.RegisterPathHandler("GET", "/api/transcript", + [&](int& status_code, std::string& payload, + WebServer::ContentType& type) -> void { + status_code = 200; + + std::ostringstream transcript_oss; + std::vector transcript = transcript_->Get(); + // Hack: escape transcription to work inside JSON blob. + for (auto& segment : transcript) { + size_t pos; + while ((pos = segment.find('"')) != std::string::npos) { + segment[pos] = '\''; + } + transcript_oss << segment; + } + + std::ostringstream resp_oss; + resp_oss << "{"; + resp_oss << "\"transcript\":\"" << transcript_oss.str() << "\""; + resp_oss << "}"; + payload = resp_oss.str(); + type = WebServer::JSON; + }); + if (!ws.Run(run)) { Log(out_, "Failed to launch browser source!\n"); } diff --git a/GUI/GUI/GUI/HTTPMapper.cpp b/GUI/GUI/GUI/HTTPMapper.cpp index e69de29..c9884ae 100644 --- a/GUI/GUI/GUI/HTTPMapper.cpp +++ b/GUI/GUI/GUI/HTTPMapper.cpp @@ -0,0 +1,69 @@ +#include "HTTPMapper.h" + +#include +#include + +namespace { + // Source: RFC 2616 section 6.1.1 + const std::map kStatusCodeToString{ + {100, "Continue" }, + {101, "Switching Protocols"}, + {200, "OK"}, + {201, "Created"}, + {202, "Accepted"}, + {203, "Non-Authoritative Information"}, + {204, "No Content"}, + {205, "Reset Content"}, + {206, "Partial Content"}, + {300, "Multiple Choices"}, + {301, "Moved Permanently"}, + {302, "Found"}, + {303, "See Other"}, + {304, "Not Modified"}, + {305, "Use Proxy"}, + {307, "Temporary Redirect"}, + {400, "Bad Request"}, + {401, "Unauthorized"}, + {402, "Payment Required"}, + {403, "Forbidden"}, + {404, "Not Found"}, + {405, "Method Not Allowed"}, + {406, "Not Acceptable"}, + }; +} + +namespace WebServer { + std::string HTTPMapper::Map(const int status_code, + const std::string& payload, const ContentType type) { + switch (type) { + case HTML: + return HTTPMapperHTML().Map(status_code, payload); + case JSON: + return HTTPMapperJSON().Map(status_code, payload); + } + } + + std::string HTTPMapperHTML::Map(const int status_code, + const std::string& payload) { + std::ostringstream oss; + // This might throw and crash the app, but that's ok, just don't use an unsupported code. + oss << "HTTP/1.1 " << status_code << " " << kStatusCodeToString.at(status_code) << "\r\n"; + oss << "Content-Type: text/html\r\n"; + oss << "Content-Length: " << std::to_string(payload.size()) << "\r\n"; + oss << "\r\n"; + oss << payload; + return oss.str(); + } + + std::string HTTPMapperJSON::Map(const int status_code, + const std::string& payload) { + std::ostringstream oss; + // This might throw and crash the app, but that's ok, just don't use an unsupported code. + oss << "HTTP/1.1 " << status_code << " " << kStatusCodeToString.at(status_code) << "\r\n"; + oss << "Content-Type: application/json\r\n"; + oss << "Content-Length: " << std::to_string(payload.size()) << "\r\n"; + oss << "\r\n"; + oss << payload; + return oss.str(); + } +} \ No newline at end of file diff --git a/GUI/GUI/GUI/HTTPMapper.h b/GUI/GUI/GUI/HTTPMapper.h index 50e9667..4086fe9 100644 --- a/GUI/GUI/GUI/HTTPMapper.h +++ b/GUI/GUI/GUI/HTTPMapper.h @@ -1 +1,35 @@ #pragma once + +#include "WebCommon.h" + +#include + +namespace WebServer { + + class HTTPMapper { + public: + HTTPMapper() {} + virtual ~HTTPMapper() {} + + std::string Map(int status_code, + const std::string& payload, ContentType type); + }; + + class HTTPMapperHTML : public HTTPMapper { + public: + HTTPMapperHTML() {} + virtual ~HTTPMapperHTML() {} + + std::string Map(int status_code, + const std::string& payload); + }; + + class HTTPMapperJSON : public HTTPMapper { + public: + HTTPMapperJSON() {} + virtual ~HTTPMapperJSON() {} + + std::string Map(int status_code, + const std::string& payload); + }; +} diff --git a/GUI/GUI/GUI/WebCommon.h b/GUI/GUI/GUI/WebCommon.h index e19223a..6e18bb2 100644 --- a/GUI/GUI/GUI/WebCommon.h +++ b/GUI/GUI/GUI/WebCommon.h @@ -2,7 +2,7 @@ namespace WebServer { enum ContentType { - HTTP, + HTML, JSON, }; }; diff --git a/GUI/GUI/GUI/WebServer.cpp b/GUI/GUI/GUI/WebServer.cpp index 0704950..ba7eecd 100644 --- a/GUI/GUI/GUI/WebServer.cpp +++ b/GUI/GUI/GUI/WebServer.cpp @@ -4,6 +4,7 @@ #include #endif +#include "HTTPMapper.h" #include "HTTPParser.h" #include "ScopeGuard.h" #include "WebServer.h" @@ -17,7 +18,15 @@ using ::Logging::Log; namespace WebServer { WebServer::WebServer(wxTextCtrl* out, uint16_t port) : out_(out), 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) { @@ -32,6 +41,10 @@ namespace WebServer { return true; } + void WebServer::RegisterDefaultHandler(handler_t&& handler) { + default_handler_ = std::move(handler); + } + bool WebServer::Run(volatile bool* run) { WSADATA wsaData; int result = WSAStartup(/*version=*/MAKEWORD(2, 2), &wsaData); @@ -50,7 +63,7 @@ namespace WebServer { sockaddr_in saddr; saddr.sin_family = AF_INET; - saddr.sin_addr.s_addr = INADDR_ANY; // TODO(yum) loopback? + saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons(port_); if (bind(sock, (sockaddr*)&saddr, sizeof(saddr)) == SOCKET_ERROR) { Log(out_, "Failed to bind to port {}: {}\n", port_, WSAGetLastError()); @@ -71,6 +84,7 @@ namespace WebServer { Log(out_, "Server running on port {}\n", port_); sockaddr_in peer_addr; + int accept_cnt = 0; while (*run) { int peer_addr_sz = sizeof(peer_addr); SOCKET csock = accept(sock, (sockaddr*)&peer_addr, &peer_addr_sz); @@ -83,20 +97,37 @@ namespace WebServer { Log(out_, "Accept failed: {}\n", WSAGetLastError()); return false; } - // TODO(yum) periodically cull connections_. + + // Periodically cull dead connections to prevent runaway memory usage. + ++accept_cnt; + if (accept_cnt % 10 == 0) { + std::vector> alive_conn; + for (int i = 0; i < connections_.size(); i++) { + if (connections_[i].valid()) { + continue; + } + alive_conn.push_back(std::move(connections_[i])); + } + //Log(out_, "Culled {} dead connections\n", connections_.size() - alive_conn.size()); + connections_ = std::move(alive_conn); + accept_cnt = 0; // Prevent overflow + } + wxTextCtrl* out = out_; const auto& dispatch_map = dispatch_map_; - connections_.push_back(std::async(std::launch::async, [csock, peer_addr, out, run, dispatch_map]() -> void { + const auto& default_handler = default_handler_; + connections_.push_back(std::async(std::launch::async, + [csock, peer_addr, out, run, dispatch_map, default_handler]() -> void { ScopeGuard csock_cleanup([csock]() { closesocket(csock); }); char peer_ip_str[INET_ADDRSTRLEN]{}; inet_ntop(AF_INET, &peer_addr.sin_addr, peer_ip_str, sizeof(peer_ip_str)); - Log(out, "Connection get: peer: {}:{}\n", peer_ip_str, ntohs(peer_addr.sin_port)); + //Log(out, "Connection get: peer: {}:{}\n", peer_ip_str, ntohs(peer_addr.sin_port)); std::string buf(4096 * 16, 0); int cur_bytes_read = 0; int sum_bytes_read = 0; - bool abort_client = false; + // 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); @@ -114,6 +145,7 @@ namespace WebServer { cur_bytes_read = 0; break; } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); continue; } break; @@ -123,15 +155,19 @@ namespace WebServer { break; } } - if (abort_client) { - return; - } if (cur_bytes_read == SOCKET_ERROR) { Log(out, "Failed to read client socket: {}\n", WSAGetLastError()); return; } + // Edge case: Server was stopped in the middle of serving a request. + if (!*run) { + 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)) { @@ -140,15 +176,34 @@ namespace WebServer { 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()) { - Log(out, "No route defined for client request: {} {}\n", - p.GetMethod(), p.GetPath()); + handler = default_handler; + } else { + handler = iter->second; + } + + // Generate a response. + int status_code; + std::string payload; + 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) == SOCKET_ERROR) { + Log(out, "Failed to send response to client: {}\n", WSAGetLastError()); return; } - // TODO(yum) send a response + // 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; diff --git a/GUI/GUI/GUI/WebServer.h b/GUI/GUI/GUI/WebServer.h index 96c4eb3..e476ba9 100644 --- a/GUI/GUI/GUI/WebServer.h +++ b/GUI/GUI/GUI/WebServer.h @@ -24,13 +24,13 @@ namespace WebServer { WebServer(wxTextCtrl *out, std::uint16_t port); typedef std::function handler_t; bool RegisterPathHandler(const std::string& method, const std::string& path, handler_t&& handler); + void RegisterDefaultHandler(handler_t&& handler); bool Run(volatile bool* run); @@ -45,6 +45,7 @@ namespace WebServer { typedef std::map dispatch_map_t; dispatch_map_t dispatch_map_; + handler_t default_handler_; wxTextCtrl* const out_; const uint16_t port_; -- cgit v1.2.3