diff --git a/src/App.h b/src/App.h index 34d3a42..46d3912 100644 --- a/src/App.h +++ b/src/App.h @@ -379,7 +379,7 @@ public: /* Calculate idleTimeoutCompnents */ webSocketContext->getExt()->calculateIdleTimeoutCompnents(behavior.idleTimeout); - httpContext->onHttp("get", pattern, [webSocketContext, behavior = std::move(behavior)](auto *res, auto *req) mutable { + httpContext->onHttp("GET", pattern, [webSocketContext, behavior = std::move(behavior)](auto *res, auto *req) mutable { /* If we have this header set, it's a websocket */ std::string_view secWebSocketKey = req->getHeader("sec-websocket-key"); @@ -438,63 +438,63 @@ public: TemplatedApp &&get(std::string pattern, MoveOnlyFunction *, HttpRequest *)> &&handler) { if (httpContext) { - httpContext->onHttp("get", pattern, std::move(handler)); + httpContext->onHttp("GET", pattern, std::move(handler)); } return std::move(*this); } TemplatedApp &&post(std::string pattern, MoveOnlyFunction *, HttpRequest *)> &&handler) { if (httpContext) { - httpContext->onHttp("post", pattern, std::move(handler)); + httpContext->onHttp("POST", pattern, std::move(handler)); } return std::move(*this); } TemplatedApp &&options(std::string pattern, MoveOnlyFunction *, HttpRequest *)> &&handler) { if (httpContext) { - httpContext->onHttp("options", pattern, std::move(handler)); + httpContext->onHttp("OPTIONS", pattern, std::move(handler)); } return std::move(*this); } TemplatedApp &&del(std::string pattern, MoveOnlyFunction *, HttpRequest *)> &&handler) { if (httpContext) { - httpContext->onHttp("delete", pattern, std::move(handler)); + httpContext->onHttp("DELETE", pattern, std::move(handler)); } return std::move(*this); } TemplatedApp &&patch(std::string pattern, MoveOnlyFunction *, HttpRequest *)> &&handler) { if (httpContext) { - httpContext->onHttp("patch", pattern, std::move(handler)); + httpContext->onHttp("PATCH", pattern, std::move(handler)); } return std::move(*this); } TemplatedApp &&put(std::string pattern, MoveOnlyFunction *, HttpRequest *)> &&handler) { if (httpContext) { - httpContext->onHttp("put", pattern, std::move(handler)); + httpContext->onHttp("PUT", pattern, std::move(handler)); } return std::move(*this); } TemplatedApp &&head(std::string pattern, MoveOnlyFunction *, HttpRequest *)> &&handler) { if (httpContext) { - httpContext->onHttp("head", pattern, std::move(handler)); + httpContext->onHttp("HEAD", pattern, std::move(handler)); } return std::move(*this); } TemplatedApp &&connect(std::string pattern, MoveOnlyFunction *, HttpRequest *)> &&handler) { if (httpContext) { - httpContext->onHttp("connect", pattern, std::move(handler)); + httpContext->onHttp("CONNECT", pattern, std::move(handler)); } return std::move(*this); } TemplatedApp &&trace(std::string pattern, MoveOnlyFunction *, HttpRequest *)> &&handler) { if (httpContext) { - httpContext->onHttp("trace", pattern, std::move(handler)); + httpContext->onHttp("TRACE", pattern, std::move(handler)); } return std::move(*this); } diff --git a/src/HttpContext.h b/src/HttpContext.h index 6cc6421..5b4c875 100644 --- a/src/HttpContext.h +++ b/src/HttpContext.h @@ -170,7 +170,7 @@ private: /* Route the method and URL */ selectedRouter->getUserData() = {(HttpResponse *) s, httpRequest}; - if (!selectedRouter->route(httpRequest->getMethod(), httpRequest->getUrl())) { + if (!selectedRouter->route(httpRequest->getCaseSensitiveMethod(), httpRequest->getUrl())) { /* We have to force close this socket as we have no handler for it */ us_socket_close(SSL, (us_socket_t *) s, 0, nullptr); return nullptr; @@ -423,7 +423,7 @@ public: /* Todo: This is ugly, fix */ std::vector methods; if (method == "*") { - methods = httpContextData->currentRouter->methods; + methods = httpContextData->currentRouter->upperCasedMethods; } else { methods = {method}; } diff --git a/src/HttpParser.h b/src/HttpParser.h index 59c03b7..4c12ed3 100644 --- a/src/HttpParser.h +++ b/src/HttpParser.h @@ -117,7 +117,17 @@ public: return std::string_view(headers->value.data(), headers->value.length()); } + /* Hack: this should be getMethod */ + std::string_view getCaseSensitiveMethod() { + return std::string_view(headers->key.data(), headers->key.length()); + } + std::string_view getMethod() { + /* Compatibility hack: lower case method (todo: remove when major version bumps) */ + for (unsigned int i = 0; i < headers->key.length(); i++) { + ((char *) headers->key.data())[i] |= 32; + } + return std::string_view(headers->key.data(), headers->key.length()); } @@ -208,11 +218,11 @@ private: } static inline uint64_t hasMore(uint64_t x, uint64_t n) { - return (((x)+~0UL/255*(127-(n))|(x))&~0UL/255*128); + return (( ((x)+~0UL/255*(127-(n))) |(x))&~0UL/255*128); } static inline uint64_t hasBetween(uint64_t x, uint64_t m, uint64_t n) { - return ((~0UL/255*(127+(n))-((x)&~0UL/255*127)&~(x)&((x)&~0UL/255*127)+~0UL/255*(127-(m)))&~0UL/255*128); + return (( (~0UL/255*(127+(n))-((x)&~0UL/255*127)) &~(x)& (((x)&~0UL/255*127)+~0UL/255*(127-(m))) )&~0UL/255*128); } static inline bool notFieldNameWord(uint64_t x) { @@ -235,6 +245,33 @@ private: } } + /* Puts method as key, target as value and returns non-null (or nullptr on error). */ + static inline char *consumeRequestLine(char *data, HttpRequest::Header &header) { + /* Scan until single SP, assume next is / (origin request) */ + char *start = data; + /* This catches the post padded CR and fails */ + while (data[0] > 32) data++; + if (data[0] == 32 && data[1] == '/') { + header.key = {start, (size_t) (data - start)}; + data++; + /* Scan for less than 33 (catches post padded CR and fails) */ + start = data; + for (; true; data += 8) { + if (hasLess(*(uint64_t *)data, 33)) { + while (*(unsigned char *)data > 32) data++; + /* Now we stand on space */ + header.value = {start, (size_t) (data - start)}; + /* Check that the following is http 1.1 */ + if (memcmp(" HTTP/1.1\r\n", data, 11) == 0) { + return data + 11; + } + return nullptr; + } + } + } + return nullptr; + } + /* RFC 9110: 5.5 Field Values (TLDR; anything above 31 is allowed; htab (9) is also allowed) * Field values are usually constrained to the range of US-ASCII characters [...] * Field values containing CR, LF, or NUL characters are invalid and dangerous [...] @@ -249,7 +286,7 @@ private: } /* End is only used for the proxy parser. The HTTP parser recognizes "\ra" as invalid "\r\n" scan and breaks. */ - static unsigned int getHeaders(char *postPaddedBuffer, char *end, struct HttpRequest::Header *headers, void *reserved) { + static unsigned int getHeaders(char *postPaddedBuffer, struct HttpRequest::Header *headers, void *reserved) { char *preliminaryKey, *preliminaryValue, *start = postPaddedBuffer; #ifdef UWS_WITH_PROXY @@ -277,35 +314,40 @@ private: * for PROXY means we can end up succeeding, yet leaving bytes in the fallback buffer * which is then removed, and our counters to flip due to overflow and we end up with a crash */ - for (unsigned int i = 0; i < HttpRequest::MAX_HEADERS - 1; i++) { + /* The request line is different from the field names / field values */ + if (!(postPaddedBuffer = consumeRequestLine(postPaddedBuffer, headers[0]))) { + /* Error - invalid request line */ + return 0; + } + headers++; + + for (unsigned int i = 1; i < HttpRequest::MAX_HEADERS - 1; i++) { /* Lower case and consume the field name */ preliminaryKey = postPaddedBuffer; postPaddedBuffer = (char *) consumeFieldName(postPaddedBuffer); headers->key = std::string_view(preliminaryKey, (size_t) (postPaddedBuffer - preliminaryKey)); - - /* Assume colon, space follows (this is fine as we have at least 2 bytes past) */ - if (postPaddedBuffer[0] == ':' && postPaddedBuffer[1] == ' ') { - postPaddedBuffer += 2; - } else { - /* We should not accept whitespace between key and colon (unless on request line) */ - if (i && postPaddedBuffer[0] != ':') { - return 0; - } - /* Trim until value starts */ - for (; (*postPaddedBuffer == ':' || *(unsigned char *)postPaddedBuffer < 33) && *postPaddedBuffer != '\r'; postPaddedBuffer++); + + /* We should not accept whitespace between key and colon, so colon must foloow immediately */ + if (postPaddedBuffer[0] != ':') { + /* Error: invalid chars in field name */ + return 0; } + preliminaryValue = postPaddedBuffer; /* The goal of this call is to find next "\r\n", or any invalid field value chars, fast */ - retry: - postPaddedBuffer = (char *) tryConsumeFieldValue(postPaddedBuffer); - /* If this is not CR then we caught some stinky invalid char on the way */ - if (postPaddedBuffer[0] != '\r') { - /* If TAB then keep searching */ - if (postPaddedBuffer[0] == '\t') { - postPaddedBuffer++; - goto retry; + while (true) { + postPaddedBuffer = (char *) tryConsumeFieldValue(postPaddedBuffer); + /* If this is not CR then we caught some stinky invalid char on the way */ + if (postPaddedBuffer[0] != '\r') { + /* If TAB then keep searching */ + if (postPaddedBuffer[0] == '\t') { + postPaddedBuffer++; + continue; + } + /* Error - invalid chars in field value */ + return 0; } - return 0; + break; } /* We fence end[0] with \r, followed by end[1] being something that is "not \n", to signify "not found". * This way we can have this one single check to see if we found \r\n WITHIN our allowed search space. */ @@ -313,6 +355,17 @@ private: /* Store this header, it is valid */ headers->value = std::string_view(preliminaryValue, (size_t) (postPaddedBuffer - preliminaryValue)); postPaddedBuffer += 2; + + /* Trim trailing whitespace (SP, HTAB) */ + while (headers->value.length() && headers->value.back() < 33) { + headers->value.remove_suffix(1); + } + + /* Trim initial whitespace (SP, HTAB) */ + while (headers->value.length() && headers->value.front() < 33) { + headers->value.remove_prefix(1); + } + headers++; /* We definitely have at least one header (or request line), so check if we are done */ @@ -350,21 +403,13 @@ private: data[length] = '\r'; data[length + 1] = 'a'; /* Anything that is not \n, to trigger "invalid request" */ - for (unsigned int consumed; length && (consumed = getHeaders(data, data + length, req->headers, reserved)); ) { + for (unsigned int consumed; length && (consumed = getHeaders(data, req->headers, reserved)); ) { data += consumed; length -= consumed; consumedTotal += consumed; /* Store HTTP version (ancient 1.0 or 1.1) */ - req->ancientHttp = req->headers->value.length() && (req->headers->value[req->headers->value.length() - 1] == '0'); - - /* We do not support ancient HTTP versions! */ - if (req->isAncient()) { - return {0, FULLPTR}; - } - - /* Strip away tail of first "header value" aka URL */ - req->headers->value = std::string_view(req->headers->value.data(), (size_t) std::max(0, (int) req->headers->value.length() - 9)); + req->ancientHttp = false; /* Add all headers to bloom filter */ req->bf.reset(); @@ -372,8 +417,8 @@ private: req->bf.add(h->key); } - /* Break if no host header */ - if (!req->getHeader("host").length()) { + /* Break if no host header (but we can have empty string which is different from nullptr) */ + if (!req->getHeader("host").data()) { return {0, FULLPTR}; } diff --git a/src/HttpRouter.h b/src/HttpRouter.h index 84ac665..00068d3 100644 --- a/src/HttpRouter.h +++ b/src/HttpRouter.h @@ -36,7 +36,6 @@ namespace uWS { template struct HttpRouter { /* These are public for now */ - std::vector methods = {"get", "post", "head", "put", "delete", "connect", "options", "trace", "patch"}; std::vector upperCasedMethods = {"GET", "POST", "HEAD", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"}; static const uint32_t HIGH_PRIORITY = 0xd0000000, MEDIUM_PRIORITY = 0xe0000000, LOW_PRIORITY = 0xf0000000; @@ -247,7 +246,7 @@ private: public: HttpRouter() { int p = 0; - for (std::string &method : methods) { + for (std::string &method : upperCasedMethods) { priority[method] = p++; } }