@@ -6,10 +6,12 @@
#include <web-common/Server.h>
#include <ply-runtime/io/InStream.h>
#include <ply-runtime/io/OutStream.h>
#include <web-common/OutPipe_HTTPChunked.h>

namespace ply {
namespace web {

//-----------------------------------------------------------------------
struct ThreadParams {
Owned<TCPConnection> tcpConn;
RequestHandler reqHandler;
@@ -30,30 +32,56 @@ PLY_NO_INLINE Tuple<StringView, StringView> getResponseDescription(ResponseCode
}

struct ResponseIface_WebServer : ResponseIface {
enum State { NoResponse, BeganResponse, EndedHeader };

OutStream* outs = nullptr;
bool gotResponse = false;
State state = NoResponse;
bool isChunked = false; // implies keep-alive
Owned<OutStream> outsChunked;

PLY_INLINE ResponseIface_WebServer(OutStream* outs) : outs{outs} {
}
virtual OutStream* respondWithStream(ResponseCode responseCode) override {
this->gotResponse = true;
virtual OutStream* beginResponseHeader(ResponseCode responseCode) override {
// FIXME: Handle ResponseCode::InternalError the same way we would handle a crash
this->state = BeganResponse;
Tuple<StringView, StringView> responseDesc = getResponseDescription(responseCode);
this->outs->strWriter()->format("HTTP/1.1 {} {}\r\n", responseDesc.first,
responseDesc.second);
// FIXME: Handle ResponseCode::InternalError the same way we would handle a crash
return this->outs;
if (isChunked) {
*this->outs->strWriter() << "Transfer-Encoding: chunked\r\n"
"Connection: keep-alive\r\n";
outsChunked =
Owned<OutStream>::create(Owned<OutPipe_HTTPChunked>::create(borrow(this->outs)));
return outsChunked;
} else {
return this->outs;
}
}
PLY_NO_INLINE void handleMissingResponse() {
if (!this->gotResponse) {
virtual void endResponseHeader() {
if (isChunked) {
outsChunked->flushMem();
outsChunked->outPipe->cast<OutPipe_HTTPChunked>()->setChunkMode(true);
}
this->state = EndedHeader;
}
// Returns true if response was well-formed and it's possible to send another response over the
// same connection:
PLY_NO_INLINE bool handleMissingResponse() {
if (this->state == NoResponse) {
this->respondGeneric(ResponseCode::InternalError);
return true; // respondGeneric makes it a well-formed response
} else {
// FIXME: Log somewhere if this->state != EndedHeader
return this->state == EndedHeader;
}
}
};

void ResponseIface::respondGeneric(ResponseCode responseCode) {
OutStream* outs = respondWithStream(responseCode);
OutStream* outs = this->beginResponseHeader(responseCode);
Tuple<StringView, StringView> responseDesc = getResponseDescription(responseCode);
*outs->strWriter() << "Content-Type: text/html\r\n\r\n";
this->endResponseHeader();
outs->strWriter()->format(R"(<html>
<head><title>{} {}</title></head>
<body>
@@ -70,62 +98,72 @@ void serverThreadEntry(const ThreadParams& params) {
InStream ins = params.tcpConn->createInStream();
OutStream outs = params.tcpConn->createOutStream();

// Create responseIface
ResponseIface_WebServer responseIface{&outs};
responseIface.request.clientAddr = params.tcpConn->remoteAddress();
responseIface.request.clientPort = params.tcpConn->remotePort();

// Parse HTTP headers: Read input lines up until a blank one
// FIXME: Limit the size of the header to something like 16KB, otherwise someone could take down
// the server.
Array<String> lines;
for (;;) {
String line = ins.asStringReader()->readString<fmt::Line>();
if (!line && ins.atEOF()) {
// Ill-formed request
responseIface.respondGeneric(ResponseCode::BadRequest);
return;
// Create responseIface
ResponseIface_WebServer responseIface{&outs};
responseIface.request.clientAddr = params.tcpConn->remoteAddress();
responseIface.request.clientPort = params.tcpConn->remotePort();

// Parse HTTP headers: Read input lines up until a blank one
// FIXME: Limit the size of the header to something like 16KB, otherwise someone could take
// down the server.
Array<String> lines;
for (;;) {
String line = ins.asStringReader()->readString<fmt::Line>();
if (!line && ins.atEOF()) {
if (!lines.isEmpty()) {
// Ill-formed request
responseIface.respondGeneric(ResponseCode::BadRequest);
}
return;
}
if (line.findByte([](char u) { return !isWhite(u); }) < 0)
break; // Blank line
lines.append(line);
}
if (line.findByte([](char u) { return !isWhite(u); }) < 0)
break; // Blank line
lines.append(line);
}
if (lines.numItems() == 0)
return; // Ill-formed request

// Split the start line into tokens:
// FIXME: Add ability to split by any whitespace instead of just splitByte
Array<StringView> tokens = lines[0].rtrim(isWhite).splitByte(' ');
if (tokens.numItems() != 3) {
// Ill-formed request
responseIface.respondGeneric(ResponseCode::BadRequest);
return;
}
responseIface.request.startLine = {tokens[0], tokens[1], tokens[2]};

// Split remaining lines into name/value pairs:
for (u32 i = 1; i < lines.numItems(); i++) {
// FIXME: Support unfolding https://tools.ietf.org/html/rfc822#section-3.1
if (isWhite(lines[i][0]))
continue;
s32 colonPos = lines[i].findByte(':');
if (colonPos < 0) {
if (lines.numItems() == 0)
return; // Ill-formed request

// Split the start line into tokens:
// FIXME: Add ability to split by any whitespace instead of just splitByte
Array<StringView> tokens = lines[0].rtrim(isWhite).splitByte(' ');
if (tokens.numItems() != 3) {
// Ill-formed request
responseIface.respondGeneric(ResponseCode::BadRequest);
return;
}
responseIface.request.headerFields.append(
{lines[i].left(colonPos).rtrim(isWhite), lines[i].subStr(colonPos + 1).trim(isWhite)});
}
responseIface.request.startLine = {tokens[0], tokens[1], tokens[2]};

// Split remaining lines into name/value pairs:
for (u32 i = 1; i < lines.numItems(); i++) {
// FIXME: Support unfolding https://tools.ietf.org/html/rfc822#section-3.1
if (isWhite(lines[i][0]))
continue;
s32 colonPos = lines[i].findByte(':');
if (colonPos < 0) {
// Ill-formed request
responseIface.respondGeneric(ResponseCode::BadRequest);
return;
}
responseIface.request.headerFields.append(
{lines[i].left(colonPos).rtrim(isWhite),
lines[i].subStr(colonPos + 1).trim(isWhite)});
}

// Note: ins is still open, so in the future, we could continue reading past the HTTP
// header to support POST requests and WebSockets.
// FIXME: Decide isChunked/keep-alive based on HTTP request headers
responseIface.isChunked = (responseIface.request.startLine.httpVersion == "HTTP/1.1");

// Invoke request handler
params.reqHandler(tokens[1], &responseIface);
responseIface.handleMissingResponse();
// Note: ins is still open, so in the future, we could continue reading past the HTTP
// header to support POST requests and WebSockets.

// Invoke request handler
params.reqHandler(tokens[1], &responseIface);

return;
if (!responseIface.handleMissingResponse())
return; // Close connection if unable to distinguish between responses
if (!responseIface.isChunked)
return; // Close connection if not keep-alive
}
}

bool runServer(u16 port, const RequestHandler& reqHandler) {
@@ -23,9 +23,10 @@ PLY_NO_INLINE void SourceCode::serve(const SourceCode* params, StringView reques
return;
}

OutStream* outs = responseIface->respondWithStream(ResponseCode::OK);
OutStream* outs = responseIface->beginResponseHeader(ResponseCode::OK);
StringWriter* sw = outs->strWriter();
*sw << "Content-Type: text/html\r\n\r\n";
responseIface->endResponseHeader();
sw->format(R"#(<!DOCTYPE html>
<html>
<head>
@@ -163,9 +163,10 @@ void DocServer::serve(StringView requestPath, ResponseIface* responseIface) {
}
}

OutStream* outs = responseIface->respondWithStream(ResponseCode::OK);
OutStream* outs = responseIface->beginResponseHeader(ResponseCode::OK);
StringWriter* sw = outs->strWriter();
*sw << "Content-Type: text/html; charset=utf-8\r\n\r\n";
responseIface->endResponseHeader();
sw->format(R"#(<!DOCTYPE html>
<html>
<head>
@@ -210,11 +211,12 @@ void DocServer::serveContentOnly(StringView requestPath, ResponseIface* response
String pageHtml = getPageSource(this, requestPath, responseIface);
if (!pageHtml)
return;
OutStream* outs = responseIface->respondWithStream(ResponseCode::OK);
OutStream* outs = responseIface->beginResponseHeader(ResponseCode::OK);
StringWriter* sw = outs->strWriter();
StringViewReader svr{pageHtml};
String pageTitle = svr.readView<fmt::Line>().trim(isWhite);
*sw << "Content-Type: text/html\r\n\r\n";
responseIface->endResponseHeader();
sw->format("{}\n<h1>{}</h1>\n", pageTitle, pageTitle);
*sw << svr.viewAvailable();
}