Fix the router (todo: req->getParameter hook up)
This commit is contained in:
+10
-4
@@ -12,22 +12,28 @@ int main(int argc, char **argv) {
|
|||||||
.cert_file_name = "/home/alexhultman/uWebSockets/misc/ssl/cert.pem",
|
.cert_file_name = "/home/alexhultman/uWebSockets/misc/ssl/cert.pem",
|
||||||
.dh_params_file_name = "/home/alexhultman/dhparams.pem",
|
.dh_params_file_name = "/home/alexhultman/dhparams.pem",
|
||||||
.passphrase = "1234"
|
.passphrase = "1234"
|
||||||
}*/).get("/*", [](auto *res, auto *req) {
|
}*/)/*.get("/*", [](auto *res, auto *req) {
|
||||||
|
|
||||||
res->end("GET /WILDCARD");
|
res->end("GET /WILDCARD");
|
||||||
|
|
||||||
}).post("/hello", [asyncFileStreamer](auto *res, auto *req) {
|
})*/.get("/:param1/:param2", [](auto *res, auto *req/*, auto ¶ms*/) {
|
||||||
|
|
||||||
|
// todo: read from params (req->getParameter(0))
|
||||||
|
res->write("GET /:param1/:param2 = ");
|
||||||
|
res->end(req->getUrl());
|
||||||
|
|
||||||
|
}).post("/hello", [asyncFileStreamer](auto *res, auto *req/*, auto ¶ms*/) {
|
||||||
|
|
||||||
// depending on the file type we want to also add mime!
|
// depending on the file type we want to also add mime!
|
||||||
//asyncFileStreamer->streamFile(res, req->getUrl());
|
//asyncFileStreamer->streamFile(res, req->getUrl());
|
||||||
|
|
||||||
res->end("POST /hello");
|
res->end("POST /hello");
|
||||||
|
|
||||||
}).get("/hello", [](auto *res, auto *req) {
|
}).get("/hello", [](auto *res, auto *req/*, auto ¶ms*/) {
|
||||||
|
|
||||||
res->end("GET /hello");
|
res->end("GET /hello");
|
||||||
|
|
||||||
}).unhandled([](auto *res, auto *req) {
|
}).unhandled([](auto *res, auto *req/*, auto ¶ms*/) {
|
||||||
|
|
||||||
res->end("Here's nothing for you to see!");
|
res->end("Here's nothing for you to see!");
|
||||||
|
|
||||||
|
|||||||
+7
-4
@@ -111,7 +111,7 @@ private:
|
|||||||
typename uWS::HttpContextData<SSL>::UserData userData = {
|
typename uWS::HttpContextData<SSL>::UserData userData = {
|
||||||
(HttpResponse<SSL> *) s, httpRequest
|
(HttpResponse<SSL> *) s, httpRequest
|
||||||
};
|
};
|
||||||
httpContextData->router.route(httpRequest->getMethod().data(), httpRequest->getMethod().length(), httpRequest->getUrl().data(), httpRequest->getUrl().length(), &userData);
|
httpContextData->router.route(httpRequest->getMethod(), httpRequest->getUrl(), &userData);
|
||||||
|
|
||||||
// here we can be closed and in shutdown?
|
// here we can be closed and in shutdown?
|
||||||
|
|
||||||
@@ -215,7 +215,10 @@ public:
|
|||||||
void onGet(std::string pattern, std::function<void(uWS::HttpResponse<SSL> *, uWS::HttpRequest *)> handler) {
|
void onGet(std::string pattern, std::function<void(uWS::HttpResponse<SSL> *, uWS::HttpRequest *)> handler) {
|
||||||
HttpContextData<SSL> *httpContextData = getSocketContextData();
|
HttpContextData<SSL> *httpContextData = getSocketContextData();
|
||||||
|
|
||||||
httpContextData->router.add("get", pattern.c_str(), [handler](typename HttpContextData<SSL>::UserData *user, auto *args) {
|
httpContextData->router.add("get", pattern, [handler](typename HttpContextData<SSL>::UserData *user, auto &args) {
|
||||||
|
|
||||||
|
// todo: attach params to the req here!
|
||||||
|
|
||||||
handler(user->httpResponse, user->httpRequest);
|
handler(user->httpResponse, user->httpRequest);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -223,7 +226,7 @@ public:
|
|||||||
void onPost(std::string pattern, std::function<void(uWS::HttpResponse<SSL> *, uWS::HttpRequest *)> handler) {
|
void onPost(std::string pattern, std::function<void(uWS::HttpResponse<SSL> *, uWS::HttpRequest *)> handler) {
|
||||||
HttpContextData<SSL> *httpContextData = getSocketContextData();
|
HttpContextData<SSL> *httpContextData = getSocketContextData();
|
||||||
|
|
||||||
httpContextData->router.add("post", pattern.c_str(), [handler](typename HttpContextData<SSL>::UserData *user, auto *args) {
|
httpContextData->router.add("post", pattern, [handler](typename HttpContextData<SSL>::UserData *user, auto &args) {
|
||||||
handler(user->httpResponse, user->httpRequest);
|
handler(user->httpResponse, user->httpRequest);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -231,7 +234,7 @@ public:
|
|||||||
void onUnhandled(std::function<void(uWS::HttpResponse<SSL> *, uWS::HttpRequest *)> handler) {
|
void onUnhandled(std::function<void(uWS::HttpResponse<SSL> *, uWS::HttpRequest *)> handler) {
|
||||||
HttpContextData<SSL> *httpContextData = getSocketContextData();
|
HttpContextData<SSL> *httpContextData = getSocketContextData();
|
||||||
|
|
||||||
httpContextData->router.unhandled([handler](typename HttpContextData<SSL>::UserData *user, auto *args) {
|
httpContextData->router.unhandled([handler](typename HttpContextData<SSL>::UserData *user, auto &args) {
|
||||||
handler(user->httpResponse, user->httpRequest);
|
handler(user->httpResponse, user->httpRequest);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ public:
|
|||||||
|
|
||||||
/* Write parts of the response in chunking fashion */
|
/* Write parts of the response in chunking fashion */
|
||||||
bool write(std::string_view data) {
|
bool write(std::string_view data) {
|
||||||
|
writeStatus(HTTP_200_OK);
|
||||||
|
|
||||||
/* Do not allow sending 0 chunks, they mark end of response */
|
/* Do not allow sending 0 chunks, they mark end of response */
|
||||||
if (!data.length()) {
|
if (!data.length()) {
|
||||||
return true; // are we corked still?
|
return true; // are we corked still?
|
||||||
|
|||||||
+206
-146
@@ -2,7 +2,7 @@
|
|||||||
#define HTTPROUTER_HPP
|
#define HTTPROUTER_HPP
|
||||||
|
|
||||||
/* HTTP router is an independent module subject to unit testing and fuzz testing */
|
/* HTTP router is an independent module subject to unit testing and fuzz testing */
|
||||||
/* TODO: this module needs much work and fixes */
|
/* This module is not fully optimized yet, waiting for more features before doing so */
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@@ -10,185 +10,245 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace uWS {
|
namespace uWS {
|
||||||
|
|
||||||
template <class USERDATA>
|
template <class USERDATA>
|
||||||
class HttpRouter {
|
class HttpRouter {
|
||||||
private:
|
private:
|
||||||
std::vector<std::function<void(USERDATA, std::vector<std::string_view> *)>> handlers;
|
static const unsigned int MAX_URL_SEGMENTS = 100;
|
||||||
std::vector<std::string_view> params;
|
|
||||||
std::function<void(USERDATA, std::vector<std::string_view> *)> unhandledHandler;
|
/* 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--;
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
std::string_view operator[](unsigned int index) {
|
||||||
|
if ((int) index <= paramsTop) {
|
||||||
|
return params[index];
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} routeParameters;
|
||||||
|
|
||||||
|
std::vector<std::function<void(USERDATA, RouteParameters &)>> handlers;
|
||||||
|
|
||||||
struct Node {
|
struct Node {
|
||||||
std::string name;
|
std::string name;
|
||||||
std::map<std::string, Node *> children;
|
std::vector<Node *> children;
|
||||||
short handler;
|
short handler = 0; // unhandled
|
||||||
} tree;
|
} tree;
|
||||||
|
|
||||||
std::string compiled_tree;
|
std::string_view currentUrl;
|
||||||
|
std::string_view urlSegmentVector[MAX_URL_SEGMENTS];
|
||||||
|
int urlSegmentTop;
|
||||||
|
|
||||||
void add(std::vector<std::string> route, short handler) {
|
/* Set URL for router. Will reset any URL cache */
|
||||||
//std::cout << "add" << std::endl;
|
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;
|
Node *parent = &tree;
|
||||||
for (std::string node : route) {
|
for (auto &p : parent->children) {
|
||||||
//std::cout << "Node: <" << node << ">" << std::endl;
|
if (p->name == method) {
|
||||||
|
parent = p;
|
||||||
if (parent->children.find(node) == parent->children.end()) {
|
|
||||||
parent->children[node] = new Node({node, {}, handler});
|
|
||||||
}
|
}
|
||||||
parent = parent->children[node];
|
}
|
||||||
|
|
||||||
|
if (parent != &tree) {
|
||||||
|
return matchUrlSegment(parent, 0);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// serialize tree
|
void printNode(Node *node, int indentation) {
|
||||||
unsigned short compile_tree(Node *n) {
|
for (int i = 0; i < indentation; i++) {
|
||||||
unsigned short nodeLength = 6 + n->name.length();
|
std::cout << " ";
|
||||||
for (auto c : n->children) {
|
|
||||||
nodeLength += compile_tree(c.second);
|
|
||||||
}
|
}
|
||||||
|
std::cout << node->name << "(" << node->handler << ")" << std::endl;
|
||||||
unsigned short nodeNameLength = n->name.length();
|
for (auto *p : node->children) {
|
||||||
|
printNode(p, indentation + 1);
|
||||||
std::string compiledNode;
|
|
||||||
compiledNode.append((char *) &nodeLength, sizeof(nodeLength));
|
|
||||||
compiledNode.append((char *) &nodeNameLength, sizeof(nodeNameLength));
|
|
||||||
compiledNode.append((char *) &n->handler, sizeof(n->handler));
|
|
||||||
compiledNode.append(n->name.data(), /*n->name.length()*/ nodeNameLength);
|
|
||||||
|
|
||||||
compiled_tree = compiledNode + compiled_tree;
|
|
||||||
return nodeLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline const char *find_node(const char *parent_node, const char *name, int name_length, bool *foundWildcard) {
|
|
||||||
unsigned short nodeLength = *(unsigned short *) &parent_node[0];
|
|
||||||
unsigned short nodeNameLength = *(unsigned short *) &parent_node[2];
|
|
||||||
|
|
||||||
//std::cout << "Finding node: <" << std::string(name, name_length) << ">" << std::endl;
|
|
||||||
|
|
||||||
const char *stoppp = parent_node + nodeLength;
|
|
||||||
for (const char *candidate = parent_node + 6 + nodeNameLength; candidate < stoppp; ) {
|
|
||||||
|
|
||||||
unsigned short nodeLength = *(unsigned short *) &candidate[0];
|
|
||||||
unsigned short nodeNameLength = *(unsigned short *) &candidate[2];
|
|
||||||
|
|
||||||
// whildcard, parameter, equal
|
|
||||||
if (nodeNameLength == 1 && candidate[6] == '*') {
|
|
||||||
*foundWildcard = true;
|
|
||||||
return candidate;
|
|
||||||
} else if (candidate[6] == ':') {
|
|
||||||
// parameter
|
|
||||||
|
|
||||||
// todo: push this pointer on the stack of args!
|
|
||||||
params.push_back(std::string_view(name, name_length));
|
|
||||||
|
|
||||||
return candidate;
|
|
||||||
} else if (nodeNameLength == name_length && !memcmp(candidate + 6, name, name_length)) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
candidate = candidate + nodeLength;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns next slash from start or end
|
|
||||||
inline const char *getNextSegment(const char *start, const char *end) {
|
|
||||||
const char *stop = (const char *) memchr(start, '/', end - start);
|
|
||||||
return stop ? stop : end;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline int lookup(const char *method, int method_length, const char *url, int length) {
|
|
||||||
// all urls start with /
|
|
||||||
url++;
|
|
||||||
length--;
|
|
||||||
|
|
||||||
const char *treeStart = (char *) compiled_tree.data();
|
|
||||||
bool foundWildcard = false;
|
|
||||||
|
|
||||||
// step1: lookup this method (we lookup treeStart)
|
|
||||||
treeStart = find_node(treeStart, method, method_length, &foundWildcard);
|
|
||||||
if (treeStart == 0) {
|
|
||||||
//std::cout << "We do not even have this method!" << std::endl;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *stop, *start = url, *end_ptr = url + length;
|
|
||||||
do {
|
|
||||||
// start and stop are pointers in the URL we are getting, end_ptr is the end of url
|
|
||||||
stop = getNextSegment(start, end_ptr);
|
|
||||||
|
|
||||||
//std::cout << "Matching(" << std::string(start, stop - start) << ")" << std::endl;
|
|
||||||
|
|
||||||
if(nullptr == (treeStart = find_node(treeStart, start, stop - start, &foundWildcard))) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the candidate was a wildcard, we do not care for the rest
|
|
||||||
if (foundWildcard) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
start = stop + 1;
|
|
||||||
} while (stop != end_ptr);
|
|
||||||
|
|
||||||
return *(short *) &treeStart[4];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
HttpRouter() {
|
HttpRouter() {
|
||||||
// maximum 100 parameters
|
/* Make sure unhandled is at index 0 */
|
||||||
params.reserve(100);
|
unhandled([](USERDATA, auto args) {
|
||||||
|
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpRouter *unhandled(std::function<void(USERDATA, std::vector<std::string_view> *)> handler) {
|
~HttpRouter() {
|
||||||
unhandledHandler = handler;
|
// todo: delete all Nodes or use unique_ptr
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpRouter *add(const char *method, const char *pattern, std::function<void(USERDATA, std::vector<std::string_view> *)> handler) {
|
/* For debugging you may want to print this */
|
||||||
|
void printTree() {
|
||||||
|
printNode(&tree, -1);
|
||||||
|
}
|
||||||
|
|
||||||
// step over any initial slash
|
/* Captures all unhandled routes */
|
||||||
if (pattern[0] == '/') {
|
HttpRouter *unhandled(std::function<void(USERDATA, RouteParameters &)> handler) {
|
||||||
pattern++;
|
if (handlers.size()) {
|
||||||
|
handlers[0] = handler;
|
||||||
|
} else {
|
||||||
|
handlers.push_back(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> nodes;
|
|
||||||
nodes.push_back(method);
|
|
||||||
|
|
||||||
const char *stop, *start = pattern, *end_ptr = pattern + strlen(pattern);
|
|
||||||
do {
|
|
||||||
stop = getNextSegment(start, end_ptr);
|
|
||||||
//std::cout << "Segment(" << std::string(start, stop - start) << ")" << std::endl;
|
|
||||||
nodes.push_back(std::string(start, stop - start));
|
|
||||||
start = stop + 1;
|
|
||||||
} while (stop != end_ptr);
|
|
||||||
|
|
||||||
// add this path to the tree
|
|
||||||
add(nodes, handlers.size());
|
|
||||||
handlers.push_back(handler);
|
|
||||||
|
|
||||||
compile();
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
void compile() {
|
/* Register a route to be routed */
|
||||||
compiled_tree.clear();
|
HttpRouter *add(std::string method, std::string_view pattern, std::function<void(USERDATA, RouteParameters &)> handler) {
|
||||||
compile_tree(&tree);
|
/* Step over any initial slash */
|
||||||
}
|
if (pattern[0] == '/') {
|
||||||
|
pattern = pattern.substr(1);
|
||||||
void route(const char *method, unsigned int method_length, const char *url, unsigned int url_length, USERDATA userData) {
|
|
||||||
|
|
||||||
// todo: simplify so that unhandled is 0!
|
|
||||||
int index = lookup(method, method_length, url, url_length);
|
|
||||||
if (index != -1) {
|
|
||||||
handlers[index](userData, ¶ms);
|
|
||||||
} else {
|
|
||||||
unhandledHandler(userData, ¶ms);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// will this counter the reserve?
|
/* Parse the route as a vector of strings */
|
||||||
params.clear();
|
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.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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user