/* * Copyright 2018 Alex Hultman and contributors. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef HTTPROUTER_HPP #define HTTPROUTER_HPP /* 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 */ #include #include #include #include #include #include #include #include namespace uWS { template class HttpRouter { private: static const unsigned int MAX_URL_SEGMENTS = 100; /* Basically a pre-allocated stack */ struct RouteParameters { friend class HttpRouter; private: std::string_view params[MAX_URL_SEGMENTS]; int paramsTop; void reset() { paramsTop = -1; } void push(std::string_view param) { /* We check these bounds indirectly via the urlSegments limit */ params[++paramsTop] = param; } void pop() { /* Same here, we cannot pop outside */ paramsTop--; } } routeParameters; std::vector)>> handlers; 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; /* Set URL for router. Will reset any URL cache */ inline void setUrl(std::string_view url) { /* Remove / from input URL */ currentUrl = url.substr(1); urlSegmentTop = -1; } /* Lazily parse or read from cache */ inline std::string_view getUrlSegment(int urlSegment) { if (urlSegment > urlSegmentTop) { /* Return empty segment if we are out of URL or stack space, but never for first url segment */ if (!currentUrl.length() || urlSegment > 99) { return {}; } auto segmentLength = currentUrl.find('/'); if (segmentLength == std::string::npos) { segmentLength = currentUrl.length(); /* Push to url segment vector */ urlSegmentVector[urlSegment] = currentUrl.substr(0, segmentLength); urlSegmentTop++; /* Update currentUrl */ currentUrl = currentUrl.substr(segmentLength); } else { /* Push to url segment vector */ urlSegmentVector[urlSegment] = currentUrl.substr(0, segmentLength); urlSegmentTop++; /* Update currentUrl */ currentUrl = currentUrl.substr(segmentLength + 1); } } /* In any case we return it */ return urlSegmentVector[urlSegment]; } int matchUrlSegment(Node *parent, int urlSegment) { /* If we have no more URL and not on first round, return where we may stand */ if (urlSegment && !getUrlSegment(urlSegment).length()) { return parent->handler; } for (auto *p : parent->children) { if (p->name.length() && p->name[0] == '*') { /* Wildcard match */ return p->handler; } else if (p->name.length() && p->name[0] == ':' && getUrlSegment(urlSegment).length()) { /* Parameter match */ routeParameters.push(getUrlSegment(urlSegment)); int handler = matchUrlSegment(p, urlSegment + 1); if (handler) { return handler; } else { // unwind parameter stack routeParameters.pop(); } } else if (p->name == getUrlSegment(urlSegment)) { /* Static match */ int handler = matchUrlSegment(p, urlSegment + 1); if (handler) { return handler; } } } return 0; } /* Route method and url to handlerIndex */ int lookupNew(std::string_view method, std::string_view url) { setUrl(url); routeParameters.reset(); /* Begin by finding the method node */ Node *parent = &tree; for (auto &p : parent->children) { if (p->name == method) { parent = p; } } if (parent != &tree) { return matchUrlSegment(parent, 0); } else { return 0; } } 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); } } public: HttpRouter() { /* Make sure unhandled is at index 0 */ unhandled([](USERDATA, auto args) { }); } ~HttpRouter() { // todo: delete all Nodes or use unique_ptr } /* For debugging you may want to print this */ void printTree() { printNode(&tree, -1); } /* Captures all unhandled routes */ HttpRouter *unhandled(std::function params)> handler) { if (handlers.size()) { handlers[0] = handler; } else { handlers.push_back(handler); } return this; } /* Register a route to be routed */ HttpRouter *add(std::string method, std::string_view pattern, std::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.push_back(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; } /* Route the method and url pair. Calls registered callback or unhandled handler */ void route(std::string_view method, std::string_view url, USERDATA userData) { handlers[lookupNew(method, url)](userData, {routeParameters.paramsTop, routeParameters.params}); } }; } #endif // HTTPROUTER_HPP