Skip to content

Commit

Permalink
Merge pull request #8 from minrk/trie
Browse files Browse the repository at this point in the history
use a trie for matching URLs
  • Loading branch information
minrk committed Sep 19, 2014
2 parents 96673f0 + f2bf20d commit 0f0a80c
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 26 deletions.
45 changes: 19 additions & 26 deletions lib/configproxy.js
Expand Up @@ -15,7 +15,8 @@ var http = require('http'),
log = require('loglevel'),
util = require('util'),
parse_url = require('url').parse,
parse_query = require('querystring').parse;
parse_query = require('querystring').parse,
URLTrie = require('./trie.js').URLTrie;

var bound = function (that, method) {
// bind a method, to ensure `this=that` when it is called
Expand Down Expand Up @@ -85,6 +86,7 @@ var authorized = function (method) {
var ConfigurableProxy = function (options) {
var that = this;
this.options = options || {};
this.trie = new URLTrie();
this.auth_token = this.options.auth_token;
this.default_target = this.options.default_target;
this.routes = {};
Expand Down Expand Up @@ -140,13 +142,15 @@ var ConfigurableProxy = function (options) {
ConfigurableProxy.prototype.add_route = function (path, data) {
// add a route to the routing table
this.routes[path] = data;
this.trie.add(path, data);
this.update_last_activity(path);
};

ConfigurableProxy.prototype.remove_route = function (path) {
// remove a route from teh routing table
if (this.routes[path] !== undefined) {
delete this.routes[path];
this.trie.remove(path);
}
};

Expand Down Expand Up @@ -216,34 +220,23 @@ ConfigurableProxy.prototype.delete_routes = function (req, res, path) {
res.end();
};

var url_startswith = function (url, prefix) {
// does the url path start with prefix?
// use array splitting to match prefix and avoid trailing-slash and partial-word issues
var prefix_parts = prefix.split('/');
var parts = url.split('/');
if (parts.length < prefix_parts.length) {
return false;
}
for (var i = 0; i < prefix_parts.length; i++) {
if (prefix_parts[i] != parts[i]) {
return false;
}
}
return true;
};

ConfigurableProxy.prototype.target_for_url = function (url) {
// return proxy target for a given url path
for (var prefix in this.routes) {
if (url_startswith(url, prefix)) {
return [prefix, this.routes[prefix].target];
}
var route = this.trie.get(url);
if (route) {
return {
prefix: route.prefix,
target: route.data.target,
};
}
// no custom target, fall back to default
if (!this.default_target) {
return;
}
return ['/', this.default_target];
return {
prefix: '/',
target: this.default_target
};
};

ConfigurableProxy.prototype.update_last_activity = function (prefix) {
Expand All @@ -261,13 +254,13 @@ ConfigurableProxy.prototype.handle_proxy = function (kind, req, res) {
// proxy any request
var that = this;
// get the proxy target
var both = this.target_for_url(req.url);
if (!both) {
var match = this.target_for_url(req.url);
if (!match) {
fail(req, res, 404, "No target for: " + req.url);
return;
}
var prefix = both[0];
var target = both[1];
var prefix = match.prefix;
var target = match.target;
log.debug("PROXY", kind.toUpperCase(), req.url, "to", target);

// pop method off the front
Expand Down
83 changes: 83 additions & 0 deletions lib/trie.js
@@ -0,0 +1,83 @@
// A simple trie for URL prefix matching
//
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
//
// Store data at nodes in the trie with Trie.add("/path/", {data})
//
// Get data for a prefix with Trie.get("/path/to/something/inside")
//
// jshint node: true
"use strict";

var URLTrie = function (prefix) {
this.prefix = prefix || '';
this.branches = {};
this.size = 0;
};

var _slashes_re = /^[\/]+|[\/]+$/g;
var string_to_path = function (s) {
return s.replace(_slashes_re, "").split('/');
};

URLTrie.prototype.add = function (path, data) {
// add data to a node in the trie at path
if (typeof path === 'string') {
path = string_to_path(path);
}
if (path.length === 0) {
this.data = data;
return;
}
var part = path.shift();
if (!this.branches.hasOwnProperty(part)) {
this.branches[part] = new URLTrie(this.prefix + '/' + part);
this.size += 1;
}
this.branches[part].add(path, data);
};

URLTrie.prototype.remove = function (path) {
// remove `path` from the trie
if (typeof path === 'string') {
path = string_to_path(path);
}
var part = path.shift();
if (path.length === 0) {
delete this.branches[part];
this.size -= 1;
return;
}
var child = this.branches[part];
child.remove(path);
if (child.size === 0 && child.data === undefined) {
// child has no branches and is not a leaf
delete this.branches[part];
this.size -= 1;
}
};

URLTrie.prototype.get = function (path) {
// get the data stored at a matching prefix
// returns:
// {
// prefix: "/the/matching/prefix",
// data: {whatever: "was stored by add"}
// }
if (typeof path === 'string') {
path = string_to_path(path);
}
if (path.length === 0) {
return this.data === undefined ? undefined: this;
}
var part = path.shift();
var child = this.branches[part];
if (child === undefined) {
return this.data === undefined ? undefined: this;
} else {
return child.get(path);
}
};

exports.URLTrie = URLTrie;
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -18,6 +18,7 @@
},
"files": [
"lib/configproxy.js",
"lib/trie.js",
"bin/configurable-http-proxy"
],
"bin": {
Expand Down
147 changes: 147 additions & 0 deletions test/test_trie.js
@@ -0,0 +1,147 @@
// jshint node: true
"use strict";

var assert = require('assert');
var http = require('http');
var util = require('../lib/testutil');
var URLTrie = require('../lib/trie').URLTrie;

var port = 8902;
var api_port = port + 1;
var proxy;
var api_url = "http://127.0.0.1:" + api_port + '/api/routes';

exports.setUp = function(callback) {
proxy = util.setup_proxy(port, callback);
};

exports.tearDown = util.teardown_servers;

var full_trie = function () {
// return a simple trie for testing
var trie = new URLTrie();
var paths = [
'/1',
'/2',
'/a/b/c/d',
'/a/b/d',
'/a/b/e',
'/b/c',
'/b/c/d',
];
for (var i=0; i < paths.length; i++) {
var path = paths[i];
trie.add(path, {path: path});
}
return trie;
};

exports.test_trie_init = function (test) {
var trie = new URLTrie();
test.equal(trie.prefix, '');
test.equal(trie.size, 0);
test.equal(trie.data, undefined);
test.deepEqual(trie.branches, {});

trie = new URLTrie('/foo');
test.equal(trie.size, 0);
test.equal(trie.prefix, '/foo');
test.deepEqual(trie.data, undefined);
test.deepEqual(trie.branches, {});

test.done();
};

exports.test_trie_add = function (test) {
var trie = new URLTrie();

trie.add('foo', 1);
test.equal(trie.size, 1);

test.equal(trie.data, undefined);
test.equal(trie.branches.foo.data, 1);
test.equal(trie.branches.foo.size, 0);

trie.add('bar/leaf', 2);
test.equal(trie.size, 2);
var bar = trie.branches.bar;
test.equal(bar.prefix, '/bar');
test.equal(bar.size, 1);
test.equal(bar.branches.leaf.data, 2);

trie.add('/a/b/c/d', 4);
test.equal(trie.size, 3);
var a = trie.branches.a;
test.equal(a.prefix, '/a');
test.equal(a.size, 1);
test.deepEqual(a.data, undefined);

var b = a.branches.b;
test.equal(b.prefix, '/a/b');
test.equal(b.size, 1);
test.equal(b.data, undefined);

var c = b.branches.c;
test.equal(c.prefix, '/a/b/c');
test.equal(c.size, 1);
test.deepEqual(c.data, undefined);
var d = c.branches.d;
test.equal(d.prefix, '/a/b/c/d');
test.equal(d.size, 0);
test.equal(d.data, 4);

test.done();
};

exports.test_trie_get = function (test) {
var trie = full_trie();
test.equal(trie.get('/not/found'), undefined);

var node = trie.get('/1');
test.equal(node.prefix, '/1');
test.equal(node.data.path, '/1');

node = trie.get('/1/etc/etc/');
test.ok(node);
test.equal(node.prefix, '/1');
test.equal(node.data.path, '/1');

test.deepEqual(trie.get('/a'), undefined);
test.deepEqual(trie.get('/a/b/c'), undefined);

node = trie.get('/a/b/c/d/e/f');
test.ok(node);
test.equal(node.prefix, '/a/b/c/d');
test.equal(node.data.path, '/a/b/c/d');

node = trie.get('/b/c/d/word');
test.ok(node);
test.equal(node.prefix, '/b/c/d');
test.equal(node.data.path, '/b/c/d');

node = trie.get('/b/c/dword');
test.ok(node);
test.equal(node.prefix, '/b/c');
test.equal(node.data.path, '/b/c');

test.done();
};

exports.test_trie_remove = function (test) {
var trie = full_trie();
var size = trie.size;
trie.remove('/b');
test.equal(trie.size, size - 1);
test.equal(trie.get('/b/c/dword'), undefined);

var node = trie.get('/a/b/c/d/word');
test.equal(node.prefix, '/a/b/c/d');
var b = trie.branches.a.branches.b;
test.equal(b.size, 3);
trie.remove('/a/b/c/d');
test.equal(b.size, 2);
test.equal(b.branches.c, undefined);

test.done();
};

0 comments on commit 0f0a80c

Please sign in to comment.