Skip to content

Commit

Permalink
Merge pull request #35 from vnayar/vibe-http-34_decode-path-params-en…
Browse files Browse the repository at this point in the history
…code-router-paths

Decode path parameters and encode URLRouter paths.
  • Loading branch information
s-ludwig authored Apr 24, 2024
2 parents 39e7429 + 1d24e18 commit fcebb30
Showing 1 changed file with 66 additions and 9 deletions.
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

0 comments on commit fcebb30

Please sign in to comment.