Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decode path parameters and encode URLRouter paths. #35

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 66 additions & 9 deletions source/vibe/http/router.d
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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/"
Expand All @@ -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
Expand Down Expand Up @@ -746,14 +765,50 @@ 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;

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];
Expand All @@ -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
Expand All @@ -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);
}
Expand Down
Loading