diff options
| -rw-r--r-- | BrowserSource/index.html | 3 | ||||
| -rw-r--r-- | GUI/GUI/GUI/BrowserSource.cpp | 52 | ||||
| -rw-r--r-- | GUI/GUI/GUI/HTTPMapper.cpp | 69 | ||||
| -rw-r--r-- | GUI/GUI/GUI/HTTPMapper.h | 34 | ||||
| -rw-r--r-- | GUI/GUI/GUI/WebCommon.h | 2 | ||||
| -rw-r--r-- | GUI/GUI/GUI/WebServer.cpp | 79 | ||||
| -rw-r--r-- | GUI/GUI/GUI/WebServer.h | 5 |
7 files changed, 226 insertions, 18 deletions
diff --git a/BrowserSource/index.html b/BrowserSource/index.html index 29cffbf..c422e4b 100644 --- a/BrowserSource/index.html +++ b/BrowserSource/index.html @@ -21,8 +21,7 @@ } function getTranscript() { $.ajax({ - // TODO(yum) parameterize the port - url: 'http://localhost:9517/api/transcript', + url: 'http://localhost:%PORT%/api/transcript', method: 'GET', dataType: 'json', success: function(data) { 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<char> 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<std::string> 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 <sstream>
+#include <map>
+
+namespace {
+ // Source: RFC 2616 section 6.1.1
+ const std::map<int, std::string> 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 <string>
+
+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 <wx/wx.h>
#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<std::future<void>> 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<void(
- const std::string& method,
- const std::string& path,
+ int& status_code,
std::string& payload,
ContentType& type)> 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_key_t, handler_t> dispatch_map_t;
dispatch_map_t dispatch_map_;
+ handler_t default_handler_;
wxTextCtrl* const out_;
const uint16_t port_;
|
