diff --git a/source/vibe/http/router.d b/source/vibe/http/router.d index 31b9219..ecf705b 100644 --- a/source/vibe/http/router.d +++ b/source/vibe/http/router.d @@ -125,11 +125,19 @@ final class URLRouter : HTTPServerRequestHandler { URLRouter match(Handler)(HTTPMethod method, string path, Handler handler) if (isValidHandler!Handler) { - import std.algorithm; + import vibe.core.path : InetPath, PosixPath; + import vibe.textfilter.urlencode : urlDecode; + import std.algorithm : count, map; assert(path.length, "Cannot register null or empty path!"); assert(count(path, ':') <= maxRouteParameters, "Too many route parameters"); logDebug("add route %s %s", method, path); - m_routes.addTerminal(path, Route(method, path, handlerDelegate(handler))); + // Perform URL-encoding on the path before adding it as a route. + string iPath = PosixPath(path) + .bySegment + .map!(s => InetPath.Segment(urlDecode(s.name), s.separator)) + .InetPath + .toString; + m_routes.addTerminal(iPath, Route(method, iPath, handlerDelegate(handler))); return this; } @@ -193,7 +201,7 @@ final class URLRouter : HTTPServerRequestHandler { /// Handles a HTTP request by dispatching it to the registered route handlers. void handleRequest(HTTPServerRequest req, HTTPServerResponse res) { - import vibe.core.path : PosixPath; + import vibe.textfilter.urlencode : urlDecode; auto method = req.method; @@ -211,7 +219,7 @@ final class URLRouter : HTTPServerRequestHandler { // segments (i.e. containing path separators) here. Any request // handlers later in the queue may still choose to process them // appropriately. - try path = (cast(PosixPath)req.requestPath).toString(); + try path = req.requestPath.toString(); catch (Exception e) return; if (path.length < m_prefix.length || path[0 .. m_prefix.length] != m_prefix) return; @@ -223,7 +231,9 @@ final class URLRouter : HTTPServerRequestHandler { if (r.method != method) return false; logDebugV("route match: %s -> %s %s %s", req.requestPath, r.method, r.pattern, values); - foreach (i, v; values) req.params[m_routes.getTerminalVarNames(ridx)[i]] = v; + foreach (i, v; values) { + req.params[m_routes.getTerminalVarNames(ridx)[i]] = urlDecode(v); + } if (m_computeBasePath) req.params["routerRootDir"] = calcBasePath(); r.cb(req, res); return res.headerWritten; @@ -446,10 +456,14 @@ final class URLRouter : HTTPServerRequestHandler { void b(HTTPServerRequest req, HTTPServerResponse) { result ~= "B"; } void c(HTTPServerRequest req, HTTPServerResponse) { assert(req.params["test"] == "x", "Wrong variable contents: "~req.params["test"]); result ~= "C"; } void d(HTTPServerRequest req, HTTPServerResponse) { assert(req.params["test"] == "y", "Wrong variable contents: "~req.params["test"]); result ~= "D"; } + void e(HTTPServerRequest req, HTTPServerResponse) { assert(req.params["test"] == "z/z", "Wrong variable contents: "~req.params["test"]); result ~= "E"; } + void f(HTTPServerRequest req, HTTPServerResponse) { result ~= "F"; } router.get("/test", &a); router.post("/test", &b); router.get("/a/:test", &c); router.get("/a/:test/", &d); + router.get("/e/:test", &e); + router.get("/f%2F", &f); auto res = createTestHTTPServerResponse(); router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/")), res); @@ -467,6 +481,10 @@ final class URLRouter : HTTPServerRequestHandler { //assert(result == "ABC", "Matched empty string or slash as var. "~result); router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a/y/"), HTTPMethod.GET), res); assert(result == "ABCD", "Didn't match 1-character infix variable."); + router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/e/z%2Fz"), HTTPMethod.GET), res); + assert(result == "ABCDE", "URL-escaped '/' confused router."); + router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/f%2F"), HTTPMethod.GET), res); + assert(result == "ABCDEF", "Unable to match '%2F' in path."); } @safe unittest { @@ -509,7 +527,7 @@ final class URLRouter : HTTPServerRequestHandler { } assert(ensureMatch("/foo bar/", "/foo%20bar/") is null); // normalized pattern: "/foo%20bar/" - //assert(ensureMatch("/foo%20bar/", "/foo%20bar/") is null); // normalized pattern: "/foo%20bar/" + assert(ensureMatch("/foo%20bar/", "/foo%20bar/") is null); // normalized pattern: "/foo%20bar/" assert(ensureMatch("/foo/bar/", "/foo/bar/") is null); // normalized pattern: "/foo/bar/" //assert(ensureMatch("/foo/bar/", "/foo%2fbar/") !is null); //assert(ensureMatch("/foo%2fbar/", "/foo%2fbar/") is null); // normalized pattern: "/foo%2Fbar/" @@ -518,6 +536,7 @@ final class URLRouter : HTTPServerRequestHandler { //assert(ensureMatch("/foo%2fbar/", "/foo/bar/") !is null); //assert(ensureMatch("/:foo/", "/foo%2Fbar/", ["foo": "foo/bar"]) is null); assert(ensureMatch("/:foo/", "/foo/bar/") !is null); + assert(ensureMatch("/test", "/tes%74") is null); } unittest { // issue #2561 @@ -746,6 +765,37 @@ private struct MatchTree(T) { return false; } + /// Given a hexadecimal character in [0-9a-fA-F], convert it to an integer value in [0, 15]. + private static uint hexDigit(char ch) @safe nothrow @nogc { + assert((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f')); + if (ch >= '0' && ch <= '9') return ch - '0'; + else if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; + else return ch - 'A' + 10; + } + + /// Reads a single character from text, decoding any unreserved percent-encoded character, so + /// that it matches the format used for route matches. + static char nextMatchChar(string text, ref size_t i) { + import std.ascii : isHexDigit; + + char ch = text[i]; + // See if we have to decode an encoded unreserved character. + if (ch == '%' && i + 2 < text.length && isHexDigit(text[i+1]) && isHexDigit(text[i+2])) { + uint c = hexDigit(text[i+1]) * 16 + hexDigit(text[i+2]); + // Check if we have an encoded unreserved character: + // https://en.wikipedia.org/wiki/Percent-encoding + if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' + || c == '-' || c == '_' || c == '.' || c == '~') { + // Decode the character before performing route matching. + ch = cast(char) c; + i += 3; + return ch; + } + } + i += 1; + return ch; + } + private inout(Node)* matchTerminals(string text) inout { if (!m_nodes.length) return null; @@ -753,7 +803,12 @@ private struct MatchTree(T) { auto n = &m_nodes[0]; // follow the path through the match graph - foreach (i, char ch; text) { + + // Routes match according to their percent-encoded normal form, with reserved-characters + // percent-encoded and unreserved-charcters not percent-encoded. + size_t i = 0; + while (i < text.length) { + char ch = nextMatchChar(text, i); auto nidx = n.edges[cast(size_t)ch]; if (nidx == NodeIndex.max) return null; n = &m_nodes[nidx]; @@ -774,8 +829,9 @@ private struct MatchTree(T) { dst[] = null; - // folow the path throgh the match graph - foreach (i, char ch; text) { + // follow the path through the match graph + size_t i = 0; + while (i < text.length) { auto var = term.varMap.get(nidx, VarIndex.max); // detect end of variable @@ -790,6 +846,7 @@ private struct MatchTree(T) { activevarstart = i; } + char ch = nextMatchChar(text, i); nidx = m_nodes[nidx].edges[cast(ubyte)ch]; assert(nidx != NodeIndex.max); }