diff --git a/src/App.h b/src/App.h index 8c2b6b620..e4677f0fd 100644 --- a/src/App.h +++ b/src/App.h @@ -248,11 +248,11 @@ struct TemplatedApp { /* Tell the router that we did not handle this request */ req->setYield(true); } - })); + }, true)); } - TemplatedApp &&get(std::string pattern, fu2::unique_function *, HttpRequest *)> &&handler) { - httpContext->onHttp("get", pattern, std::move(handler)); + TemplatedApp &&get(std::string pattern, fu2::unique_function *, HttpRequest *)> &&handler, bool upgrade = false) { + httpContext->onHttp("get", pattern, std::move(handler), upgrade); return std::move(*this); } diff --git a/src/HttpContext.h b/src/HttpContext.h index 74e9afd16..ec3dc625f 100644 --- a/src/HttpContext.h +++ b/src/HttpContext.h @@ -140,15 +140,12 @@ struct HttpContext { /* Mark pending request and emit it */ httpResponseData->state = HttpResponseData::HTTP_RESPONSE_PENDING; - /* Route the method and URL in two passes */ - typename HttpContextData::RouterData routerData = {(HttpResponse *) s, httpRequest}; - if (!httpContextData->router.route(httpRequest->getMethod(), httpRequest->getUrl(), routerData)) { - /* If first pass failed, we try and match by "any" method */ - if (!httpContextData->router.route("*", httpRequest->getUrl(), routerData)) { - /* If second pass fail, we have to force close this socket as we have no handler for it */ - us_socket_close(SSL, (us_socket_t *) s); - return nullptr; - } + /* Route the method and URL */ + httpContextData->router.getUserData() = {(HttpResponse *) s, httpRequest}; + if (!httpContextData->router.route(httpRequest->getMethod(), httpRequest->getUrl())) { + /* We have to force close this socket as we have no handler for it */ + us_socket_close(SSL, (us_socket_t *) s); + return nullptr; } /* First of all we need to check if this socket was deleted due to upgrade */ @@ -346,12 +343,21 @@ struct HttpContext { } /* Register an HTTP route handler acording to URL pattern */ - void onHttp(std::string method, std::string pattern, fu2::unique_function *, HttpRequest *)> &&handler) { + void onHttp(std::string method, std::string pattern, fu2::unique_function *, HttpRequest *)> &&handler, bool upgrade = false) { HttpContextData *httpContextData = getSocketContextData(); - httpContextData->router.add(method, pattern, [handler = std::move(handler)](typename HttpContextData::RouterData &user, std::pair params) mutable { + /* Todo: This is ugly, fix */ + std::vector methods; + if (method == "*") { + methods = httpContextData->router.methods; + } else { + methods = {method}; + } + + httpContextData->router.add(methods, pattern, [handler = std::move(handler)](auto *r) mutable { + auto user = r->getUserData(); user.httpRequest->setYield(false); - user.httpRequest->setParameters(params); + user.httpRequest->setParameters(r->getParameters()); handler(user.httpResponse, user.httpRequest); /* If any handler yielded, the router will keep looking for a suitable handler. */ @@ -359,7 +365,7 @@ struct HttpContext { return false; } return true; - }); + }, method == "*" ? httpContextData->router.LOW_PRIORITY : (upgrade ? httpContextData->router.HIGH_PRIORITY : httpContextData->router.MEDIUM_PRIORITY)); } /* Listen to port using this HttpContext */ diff --git a/src/HttpRouter.h b/src/HttpRouter.h index eb8351339..c9e607f37 100644 --- a/src/HttpRouter.h +++ b/src/HttpRouter.h @@ -18,8 +18,6 @@ #ifndef UWS_HTTPROUTER_HPP #define UWS_HTTPROUTER_HPP -// todo: this module also needs a few clean-ups and simplifications - /* HTTP router is an independent module subject to unit testing and fuzz testing */ /* This module is not fully optimized yet, waiting for more features before doing so */ @@ -30,15 +28,55 @@ #include #include #include +#include +#include #include "f2/function2.hpp" namespace uWS { -template +template struct HttpRouter { -private: + + USERDATA userData; + + std::vector methods = {"GET", "POST", "HEAD", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"}; + + /* 32-bit */ + const static uint32_t HANDLER_MASK = 0x0fffffff; + const static uint32_t HIGH_PRIORITY = 0xd0000000; + const static uint32_t MEDIUM_PRIORITY = 0xe0000000; + const static uint32_t LOW_PRIORITY = 0xf0000000; + static const unsigned int MAX_URL_SEGMENTS = 100; + +private: + /* Methods and their respective priority */ + std::map priority; + + struct Node { + std::string name; + std::vector> children; + std::vector handlers; + } root = {"rootNode"}; + + /* List of handlers */ + std::vector> handlers; + + /* Advance from parent to child, adding child if necessary */ + Node *getNode(Node *parent, std::string child) { + for (std::unique_ptr &node : parent->children) { + if (node->name == child) { + return node.get(); + } + } + + /* Insert sorted, but keep order if parent is root (we sort methods by priority elsewhere) */ + std::unique_ptr newNode(new Node({child})); + return parent->children.emplace(std::upper_bound(parent->children.begin(), parent->children.end(), newNode, [parent, this](auto &a, auto &b) { + return b->name.length() && (parent != &root) && (b->name < a->name); + }), std::move(newNode))->get(); + } /* Basically a pre-allocated stack */ struct RouteParameters { @@ -62,16 +100,6 @@ struct HttpRouter { } } routeParameters; - std::vector)>> handlers; - - HttpRouter(const HttpRouter &other) = delete; - - struct Node { - std::string name; - std::vector children; - short handler = 0; // unhandled - } tree; - std::string_view currentUrl; std::string_view urlSegmentVector[MAX_URL_SEGMENTS]; int urlSegmentTop; @@ -119,165 +147,95 @@ struct HttpRouter { /* If we have no more URL and not on first round, return where we may stand */ if (urlSegment && !getUrlSegment(urlSegment).length()) { /* We have reached accross the entire URL with no stoppage, execute */ - int handlerIndex = parent->handler; - if (handlerIndex) { - return handlers[handlerIndex](userData, {routeParameters.paramsTop, routeParameters.params}); - } else { - /* Unhandled */ - return false; + for (int handler : parent->handlers) { + if (handlers[handler & HANDLER_MASK](this)) { + return true; + } } + /* We reached the end, so go back */ + return false; } - for (auto *p : parent->children) { + for (auto &p : parent->children) { if (p->name.length() && p->name[0] == '*') { /* Wildcard match (can be seen as a shortcut) */ - int handlerIndex = p->handler; - if (handlerIndex) { - int handler = handlers[handlerIndex](userData, {routeParameters.paramsTop, routeParameters.params}); - if (handler) { - return handler; + for (int handler : p->handlers) { + if (handlers[handler & HANDLER_MASK](this)) { + return true; } - } else { - /* Unhandled */ - return false; } } else if (p->name.length() && p->name[0] == ':' && getUrlSegment(urlSegment).length()) { /* Parameter match */ routeParameters.push(getUrlSegment(urlSegment)); - int handler = executeHandlers(p, urlSegment + 1, userData); - if (handler) { - return handler; - } else { - // unwind parameter stack - routeParameters.pop(); + if (executeHandlers(p.get(), urlSegment + 1, userData)) { + return true; } + routeParameters.pop(); } else if (p->name == getUrlSegment(urlSegment)) { /* Static match */ - int handler = executeHandlers(p, urlSegment + 1, userData); - if (handler) { - return handler; + if (executeHandlers(p.get(), urlSegment + 1, userData)) { + return true; } } } return false; } - void printNode(Node *node, int indentation) { - for (int i = 0; i < indentation; i++) { - std::cout << " "; - } - std::cout << node->name << "(" << node->handler << ")" << std::endl; - for (auto *p : node->children) { - printNode(p, indentation + 1); - } - } - - void freeNode(Node *node) { - for (auto *p : node->children) { - freeNode(p); - } - if (node != &tree) { - delete node; - } - } - public: HttpRouter() { - /* We want to use 0 as "no handler" */ - handlers.resize(1); + int p = 0; + for (std::string &method : methods) { + priority[method] = p++; + } } - ~HttpRouter() { - // todo: delete all Nodes or use unique_ptr - freeNode(&tree); + void print() { + printNode(&root, 0); } - /* For debugging you may want to print this */ - void printTree() { - printNode(&tree, -1); + std::pair getParameters() { + return {routeParameters.paramsTop, routeParameters.params}; } - /* Register a route to be routed */ - HttpRouter *add(std::string method, std::string_view pattern, fu2::unique_function)> &&handler) { - /* Step over any initial slash */ - if (pattern[0] == '/') { - pattern = pattern.substr(1); - } - - /* Parse the route as a vector of strings */ - std::vector route; - route.push_back(method); - - std::stringstream test; - test << pattern; - - /* Empty pattern or / is the default */ - if (!pattern.length()) { - route.push_back(""); - } - - std::string segment; - while(std::getline(test, segment, '/')) { - route.push_back(segment); - } - - /* Add this handler to the list of handlers */ - short handlerIndex = handlers.size(); - handlers.emplace_back(std::move(handler)); - - /* Build the routing tree */ - Node *parent = &tree; - for (unsigned int i = 0; i < route.size(); i++) { - std::string node = route[i]; - // do we already have this? - Node *found = nullptr; - for (auto *child : parent->children) { - if (child->name == node) { - found = child; - break; - } - } - - if (!found) { - if (i == route.size() - 1) { - // only ever touch the handler id on the leaf node - parent->children.push_back(found = new Node({node, {}, handlerIndex})); - } else { - parent->children.push_back(found = new Node({node, {}, 0})); - } - } else if (i == route.size() - 1) { - // touch leaf node of existing path - found->handler = handlerIndex; - } - parent = found; - } - - return this; + USERDATA &getUserData() { + return userData; } - /* Routes by method and url until handler found and said handler consumes the request by returning true. - * If a handler returns false, we keep searching for another match. If we cannot find a handler that - * a) matches the url and method and b) consume the request, then we fail and return false. - * In that case, a second pass where method changed to "*" to denote "any" could be used to - * give such routes a chance. If second pass fails, we have an unhandled request and you may - * do whatever you want with your connection, such as close it, or respond with a fix message */ - bool route(std::string_view method, std::string_view url, USERDATA &userData) { + /* Fast path */ + bool route(std::string_view method, std::string_view url) { /* Reset url parsing cache */ setUrl(url); routeParameters.reset(); /* Begin by finding the method node */ - for (auto &p : tree.children) { + for (auto &p : root.children) { if (p->name == method) { /* Then route the url */ - return executeHandlers(p, 0, userData); + return executeHandlers(p.get(), 0, userData); } } - /* We did not find any handler for this method. - * You may want to re-route with "*" as method. */ + /* We did not find any handler for this method and url */ return false; } + + /* Adds the corresponding entires in matching tree and handler list */ + void add(std::vector methods, std::string pattern, fu2::unique_function &&handler, int priority = MEDIUM_PRIORITY) { + for (std::string method : methods) { + /* Lookup method */ + Node *node = getNode(&root, method); + /* Iterate over all segments */ + setUrl(pattern); + for (int i = 0; getUrlSegment(i).length() || i == 0; i++) { + node = getNode(node, std::string(getUrlSegment(i))); + } + /* Insert handler in order sorted by priority (most significant 1 byte) */ + node->handlers.insert(std::upper_bound(node->handlers.begin(), node->handlers.end(), priority | handlers.size()), priority | handlers.size()); + } + + /* Alloate this handler */ + handlers.emplace_back(std::move(handler)); + } }; }