From aea255f72c582833648d94f26748d173cc43060b Mon Sep 17 00:00:00 2001 From: Alex Hultman Date: Tue, 26 Jun 2018 21:48:22 +0200 Subject: [PATCH] Rough skeleton for proper HTTP parsing --- main.cpp | 9 +++ src/App.h | 33 +++------ src/HttpRequest.h | 56 ++++++++++---- src/HttpSocket.h | 182 +++++++++++++++++++++++++++++++++++++--------- uSockets | 2 +- 5 files changed, 210 insertions(+), 72 deletions(-) diff --git a/main.cpp b/main.cpp index e1fcd3e..33fe71c 100644 --- a/main.cpp +++ b/main.cpp @@ -30,10 +30,19 @@ int main(int argc, char **argv) { app.onGet("/", [](auto *s, auto *req, auto *args) { + // streams need to expose more information about lifetime! s->writeStatus("200 OK")->write([](int offset) { return std::string_view(buffer.data() + offset, buffer.length() - offset); }, buffer.length()); + }).onPost("/upload", [](auto *s, auto *req, auto *args) { + + s->read([s](std::string_view chunk) { + std::cout << "Received chunk on URL /upload: <" << chunk << ">" << std::endl; + + s->writeStatus("200 OK")->end("Thanks for posting!"); + }); + }).onWebSocket("/wsApi", []() { }).listen("localhost", 3000, 0); diff --git a/src/App.h b/src/App.h index 181a9cf..dd3343c 100644 --- a/src/App.h +++ b/src/App.h @@ -25,6 +25,7 @@ protected: } typedef typename std::conditional::type SOCKET_CONTEXT_TYPE; + typedef typename HttpSocket::Data HTTP_SOCKET_DATA_TYPE; struct Data { Data() { @@ -64,40 +65,28 @@ protected: new (data = (Data *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(httpServerContext)) Data(); static_dispatch(us_ssl_socket_context_on_open, us_socket_context_on_open)(httpServerContext, [](auto *s) { - Data *data = (Data *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(static_dispatch(us_ssl_socket_get_context, us_socket_get_context)(s)); + Data *appData = (Data *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(static_dispatch(us_ssl_socket_get_context, us_socket_get_context)(s)); - new (static_dispatch(us_ssl_socket_ext, us_socket_ext)(s)) typename HttpSocket::Data; + new (static_dispatch(us_ssl_socket_ext, us_socket_ext)(s)) HTTP_SOCKET_DATA_TYPE; - if (!data->onHttpConnection) { - return; + if (appData->onHttpConnection) { + appData->onHttpConnection((HttpSocket *) s); } - - data->onHttpConnection((HttpSocket *) s); }); static_dispatch(us_ssl_socket_context_on_close, us_socket_context_on_close)(httpServerContext, [](auto *s) { - Data *data = (Data *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(static_dispatch(us_ssl_socket_get_context, us_socket_get_context)(s)); + Data *appData = (Data *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(static_dispatch(us_ssl_socket_get_context, us_socket_get_context)(s)); - // todo: run the destructor! - //((typename HttpSocket::Data *) static_dispatch(us_ssl_socket_ext, us_socket_ext)(s))->~(typename HttpSocket::Data)(); + ((HTTP_SOCKET_DATA_TYPE *) static_dispatch(us_ssl_socket_ext, us_socket_ext)(s))->~HTTP_SOCKET_DATA_TYPE(); - if (!data->onHttpDisconnection) { - return; + if (appData->onHttpDisconnection) { + appData->onHttpDisconnection((HttpSocket *) s); } - - data->onHttpDisconnection((HttpSocket *) s); }); static_dispatch(us_ssl_socket_context_on_data, us_socket_context_on_data)(httpServerContext, [](auto *s, char *data, int length) { - - Data *contextData = (Data *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(static_dispatch(us_ssl_socket_get_context, us_socket_get_context)(s)); - - HttpRequest req(data, length); - if (req.isComplete()) { - contextData->onHttpRequest((HttpSocket *) s, &req); - } else { - std::cout << "Got chunked HTTP headers!" << std::endl; - } + Data *appData = (Data *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(static_dispatch(us_ssl_socket_get_context, us_socket_get_context)(s)); + ((HttpSocket *) s)->onData(data, length, appData->onHttpRequest); }); static_dispatch(us_ssl_socket_context_on_writable, us_socket_context_on_writable)(httpServerContext, [](auto *s) { diff --git a/src/HttpRequest.h b/src/HttpRequest.h index fbc0525..f7eb134 100644 --- a/src/HttpRequest.h +++ b/src/HttpRequest.h @@ -1,19 +1,27 @@ #ifndef HTTPREQUEST_H #define HTTPREQUEST_H +#include +#include +#include + // holds the header pointers and wrappers struct HttpRequest { struct Header { char *key, *value; unsigned int keyLength, valueLength; + + operator bool() { + return key; + } }; #define MAX_HEADERS 100 Header headers[MAX_HEADERS]; // UNSAFETY NOTE: assumes *end == '\r' (might unref end pointer) - char *getHeaders(char *buffer, char *end, struct Header *headers, size_t maxHeaders) { + static char *getHeaders(char *buffer, char *end, struct Header *headers, size_t maxHeaders) { for (unsigned int i = 0; i < maxHeaders; i++) { for (headers->key = buffer; (*buffer != ':') & (*buffer > 32); *(buffer++) |= 32); if (*buffer == '\r') { @@ -40,27 +48,45 @@ struct HttpRequest { return 0; } - std::string_view url; - HttpRequest(char *data, int length) { - // parse the shit - data[length] = '\r'; - - if (getHeaders(data, data + length, headers, MAX_HEADERS)) { + int consumePostPadded(char *data, int length) { + char *cursor = data; + if (cursor = getHeaders(data, data + length, headers, MAX_HEADERS)) { + // strip out the initial stuff headers->valueLength = std::max(0, headers->valueLength - 9); - - // headers should really just be string_view from the start! - url = std::string_view(headers[0].value, headers[0].valueLength); } + + return cursor - data; + } + + void fenceRegion(char *data, int length) { + data[length] = '\r'; + } + + Header getHeader(const char *key, size_t length) { + if (headers) { + for (Header *h = headers; *++h; ) { + if (h->keyLength == length && !strncmp(h->key, key, length)) { + return *h; + } + } + } + return {nullptr, nullptr, 0, 0}; + } + + std::string_view getHeader(std::string_view header) { + Header h = getHeader(header.data(), header.length()); + + if (h.key) { + return std::string_view(h.value, h.valueLength); + } + + return std::string_view(nullptr, 0); } std::string_view getUrl() { - return url; - } - - bool isComplete() { - return url.length(); + return std::string_view(headers[0].value, headers[0].valueLength); } }; diff --git a/src/HttpSocket.h b/src/HttpSocket.h index 254e79b..29fff1e 100644 --- a/src/HttpSocket.h +++ b/src/HttpSocket.h @@ -3,13 +3,17 @@ #include "libusockets.h" #include "Loop.h" +#include "HttpRequest.h" #include #include +#include +#include -// HttpSocket is an alias for us_socket template struct HttpSocket { + const size_t MAX_FALLBACK_SIZE = 1024 * 4; + template static constexpr typename std::conditional::type *static_dispatch(A *a, B *b) { if constexpr(SSL) { @@ -21,34 +25,11 @@ struct HttpSocket { typedef typename std::conditional::type SOCKET_TYPE; - // chunked response will be tricky with this buffering scheme - // if we do not fit, we can always use the header buffer for this (both in and out!) - // put first 8kb chunk in the http buffer, then from there it's the stream's job! - // httpheaders should only have 1 stream in and 1 stream out, but we can have helper wrappers - - // data is stored in ext - struct Data { - // incomplete headers buffer - std::string headerBuffer; - int offset = 0; - - std::function outStream; - }; - - // only this one should be used! - void writeToCorkBuffer(const char *src, int length) { - uWS::Loop::Data *loopData = (uWS::Loop::Data *) us_loop_ext(us_socket_context_loop(us_socket_get_context((us_socket *) this))); - - memcpy(loopData->corkBuffer + loopData->corkOffset, src, length); - loopData->corkOffset += length; - } - - // sprintf is super slow, this one is a lot faster - int u32toa_naive(uint32_t value, char *dst) { + int u32toa(uint32_t value, char *dst) { char temp[10]; char *p = temp; do { - *p++ = char(value % 10) + '0'; + *p++ = (char) (value % 10) + '0'; value /= 10; } while (value > 0); @@ -61,6 +42,42 @@ struct HttpSocket { return ret; } + int str2int(const char *str, int len) { + int i; + int ret = 0; + for (i = 0; i < len; i++) { + ret = ret * 10 + (str[i] - '0'); + } + return ret; + } + + // chunked response will be tricky with this buffering scheme + // if we do not fit, we can always use the header buffer for this (both in and out!) + // put first 8kb chunk in the http buffer, then from there it's the stream's job! + // httpheaders should only have 1 stream in and 1 stream out, but we can have helper wrappers + + // data is stored in ext + struct Data { + // fallback buffering + std::string fallback; + + // these two control input streaming + int contentLength = 0; + std::function inStream; + + // out streaming (.end should be a wrapper of this!) + int offset = 0; + std::function outStream; + }; + + // only this one should be used! + void writeToCorkBuffer(const char *src, int length) { + uWS::Loop::Data *loopData = (uWS::Loop::Data *) us_loop_ext(us_socket_context_loop(us_socket_get_context((us_socket *) this))); + + memcpy(loopData->corkBuffer + loopData->corkOffset, src, length); + loopData->corkOffset += length; + } + // never rely on this one! int writeToCorkBufferAndReset(const char *src, int length, int contentLength) { uWS::Loop::Data *loopData = (uWS::Loop::Data *) us_loop_ext(us_socket_context_loop(us_socket_get_context((us_socket *) this))); @@ -68,7 +85,7 @@ struct HttpSocket { memcpy(loopData->corkBuffer + loopData->corkOffset, "Content-Length: ", 16); loopData->corkOffset += 16; - loopData->corkOffset += u32toa_naive(contentLength, loopData->corkBuffer + loopData->corkOffset); + loopData->corkOffset += u32toa(contentLength, loopData->corkBuffer + loopData->corkOffset); memcpy(loopData->corkBuffer + loopData->corkOffset, "\r\n\r\n", 4); loopData->corkOffset += 4; @@ -86,7 +103,6 @@ struct HttpSocket { } loopData->corkOffset = 0; - return written; } @@ -105,6 +121,7 @@ struct HttpSocket { return this; } + // this should not be anything other than a simple convenience wrapper of streams! void end(std::string_view data) { // end should not explicitly flush the cork buffer! delay to when done with all http data! writeToCorkBufferAndReset(data.data(), data.length(), data.length()); @@ -157,17 +174,114 @@ struct HttpSocket { } } - // can't create this! - HttpSocket() = delete; + void onData(char *data, int length, std::function *, HttpRequest *)> &onHttpRequest) { + Data *httpData = (Data *) static_dispatch(us_ssl_socket_ext, us_socket_ext)((SOCKET_TYPE *) this); - // the HttpParser should maybe be moved out of this into its own HttpProtocol.h like with websocket? - void parse(char *data, int length) { + //std::cout << std::string_view(data, length) << std::endl; - std::cout << "Parsing headers: " << std::string_view(data, length) << std::endl; + HttpRequest req; + req.fenceRegion(data, length); + + if (httpData->contentLength) { + // at this point we reset the timeout timer + if (httpData->contentLength >= length) { + httpData->inStream(std::string_view(data, length)); + httpData->contentLength -= length; + // no change to the socket here! + return; + } else { + httpData->inStream(std::string_view(data, httpData->contentLength)); + + data += httpData->contentLength; + length -= httpData->contentLength; + + httpData->contentLength = 0; + } + } else if (httpData->fallback.length()) { + int maxCopyDistance = std::min(MAX_FALLBACK_SIZE - httpData->fallback.length(), (size_t) length); + + httpData->fallback.reserve(maxCopyDistance + 32); // padding should be same as libus + httpData->fallback.append(data, maxCopyDistance); + + + if (int consumed = req.consumePostPadded(httpData->fallback.data(), httpData->fallback.length()); consumed) { + httpData->fallback.clear(); + + data += consumed; + length -= consumed; + + onHttpRequest(this, &req); + + // see if we can read any posted data here (do we have contentLength header set?) + } else { + if (httpData->fallback.length() < MAX_FALLBACK_SIZE) { + std::cout << "Http headers coming in chunks I see, fine I'll pass!" << std::endl; + } else { + // here we failed to parse any header in the 4kb we were given! + std::cout << "INVALID HTTP! no more chances!" << std::endl; + } + // no change in socket, or a closed socket! + return; + } + } + + for (int consumed = 0; length && (consumed = req.consumePostPadded(data, length)); ) { + + //std::cout << "Parsing now <" << std::string_view(data, length) << ">" << std::endl; + + data += consumed; + length -= consumed; + + // first emit the request + onHttpRequest(this, &req); + + // then consume and stream any data! + if (std::string_view contentLength = req.getHeader("content-length"); contentLength.length()) { + // can we read everything off right now? + + httpData->contentLength = str2int(contentLength.data(), contentLength.length()); + + //std::cout << "content length!" << std::endl; + + + int emittable = std::min(httpData->contentLength, length); + + + //std::cout << "Emittable: " << emittable << std::endl; + + httpData->inStream(std::string_view(data, emittable)); + + + httpData->contentLength -= emittable; + + + length -= emittable; + + // otherwise, enter contentLength state! + } else { + //std::cout << "We don't have content-length!" << std::endl; + } + + } + + if (length) { + if (length < MAX_FALLBACK_SIZE) { + // buffer up for next + } else { + // invalid http! + std::cout << "invalid http! fuck off!" << std::endl; + } + } } - void stream(int length, decltype(Data::outStream) stream); + void read(decltype(Data::inStream) stream) { + Data *httpData = (Data *) static_dispatch(us_ssl_socket_ext, us_socket_ext)((SOCKET_TYPE *) this); + + httpData->inStream = stream; + } + + HttpSocket() = delete; }; #endif // HTTP_H diff --git a/uSockets b/uSockets index 4ef0f2e..ad9a5fb 160000 --- a/uSockets +++ b/uSockets @@ -1 +1 @@ -Subproject commit 4ef0f2ecd1d4c3b36b585e8e34c94fc9bf0155fc +Subproject commit ad9a5fb11d417b49403bf9bf2d2d6f190c2b6e7a