Skip to content

Commit

Permalink
First stab at new router
Browse files Browse the repository at this point in the history
  • Loading branch information
uNetworkingAB committed Dec 25, 2019
1 parent 5ab190f commit d3b43e3
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 148 deletions.
6 changes: 3 additions & 3 deletions src/App.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<void(HttpResponse<SSL> *, HttpRequest *)> &&handler) {
httpContext->onHttp("get", pattern, std::move(handler));
TemplatedApp &&get(std::string pattern, fu2::unique_function<void(HttpResponse<SSL> *, HttpRequest *)> &&handler, bool upgrade = false) {
httpContext->onHttp("get", pattern, std::move(handler), upgrade);
return std::move(*this);
}

Expand Down
32 changes: 19 additions & 13 deletions src/HttpContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,12 @@ struct HttpContext {
/* Mark pending request and emit it */
httpResponseData->state = HttpResponseData<SSL>::HTTP_RESPONSE_PENDING;

/* Route the method and URL in two passes */
typename HttpContextData<SSL>::RouterData routerData = {(HttpResponse<SSL> *) 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<SSL> *) 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 */
Expand Down Expand Up @@ -346,20 +343,29 @@ struct HttpContext {
}

/* Register an HTTP route handler acording to URL pattern */
void onHttp(std::string method, std::string pattern, fu2::unique_function<void(HttpResponse<SSL> *, HttpRequest *)> &&handler) {
void onHttp(std::string method, std::string pattern, fu2::unique_function<void(HttpResponse<SSL> *, HttpRequest *)> &&handler, bool upgrade = false) {
HttpContextData<SSL> *httpContextData = getSocketContextData();

httpContextData->router.add(method, pattern, [handler = std::move(handler)](typename HttpContextData<SSL>::RouterData &user, std::pair<int, std::string_view *> params) mutable {
/* Todo: This is ugly, fix */
std::vector<std::string> 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. */
if (user.httpRequest->getYield()) {
return false;
}
return true;
});
}, method == "*" ? httpContextData->router.LOW_PRIORITY : (upgrade ? httpContextData->router.HIGH_PRIORITY : httpContextData->router.MEDIUM_PRIORITY));
}

/* Listen to port using this HttpContext */
Expand Down
222 changes: 90 additions & 132 deletions src/HttpRouter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand All @@ -30,15 +28,55 @@
#include <string_view>
#include <sstream>
#include <string>
#include <algorithm>
#include <memory>

#include "f2/function2.hpp"

namespace uWS {

template <typename USERDATA>
template <class USERDATA>
struct HttpRouter {
private:

USERDATA userData;

std::vector<std::string> 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<std::string, int> priority;

struct Node {
std::string name;
std::vector<std::unique_ptr<Node>> children;
std::vector<uint32_t> handlers;
} root = {"rootNode"};

/* List of handlers */
std::vector<fu2::unique_function<bool(HttpRouter *)>> handlers;

/* Advance from parent to child, adding child if necessary */
Node *getNode(Node *parent, std::string child) {
for (std::unique_ptr<Node> &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<Node> 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 {
Expand All @@ -62,16 +100,6 @@ struct HttpRouter {
}
} routeParameters;

std::vector<fu2::unique_function<bool(USERDATA &, std::pair<int, std::string_view *>)>> handlers;

HttpRouter(const HttpRouter &other) = delete;

struct Node {
std::string name;
std::vector<Node *> children;
short handler = 0; // unhandled
} tree;

std::string_view currentUrl;
std::string_view urlSegmentVector[MAX_URL_SEGMENTS];
int urlSegmentTop;
Expand Down Expand Up @@ -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<int, std::string_view *> getParameters() {
return {routeParameters.paramsTop, routeParameters.params};
}

/* Register a route to be routed */
HttpRouter *add(std::string method, std::string_view pattern, fu2::unique_function<bool(USERDATA &, std::pair<int, std::string_view *>)> &&handler) {
/* Step over any initial slash */
if (pattern[0] == '/') {
pattern = pattern.substr(1);
}

/* Parse the route as a vector of strings */
std::vector<std::string> 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<std::string> methods, std::string pattern, fu2::unique_function<bool(HttpRouter *)> &&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));
}
};

}
Expand Down

0 comments on commit d3b43e3

Please sign in to comment.