mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-17 15:03:02 +00:00
proxy moved to its own repo
moved to jupyter/configurable-http-proxy closes #11
This commit is contained in:
@@ -6,7 +6,7 @@ python:
|
||||
before_install:
|
||||
# workaround for https://github.com/travis-ci/travis-cookbooks/issues/155
|
||||
- sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm
|
||||
- time npm install -g bower .
|
||||
- time npm install -g bower jupyter/configurable-http-proxy
|
||||
- time sudo apt-get install libzmq3-dev
|
||||
- time pip install -f https://nipy.bic.berkeley.edu/wheelhouse/travis -r dev-requirements.txt
|
||||
install:
|
||||
|
@@ -17,8 +17,9 @@ Basic principals:
|
||||
|
||||
## dependencies
|
||||
|
||||
# get the dependencies of the nodejs proxy (-g for global install)
|
||||
npm install [-g]
|
||||
# get the nodejs proxy (-g for global install)
|
||||
npm install [-g] jupyter/configurable-http-proxy
|
||||
|
||||
# install the Python pargs (-e for editable/development install)
|
||||
pip install [-e] .
|
||||
|
||||
|
@@ -1,284 +0,0 @@
|
||||
// A Configurable node-http-proxy
|
||||
//
|
||||
// POST, DELETE to /api/routes[:/path/to/proxy] to update the routing table
|
||||
// GET /api/routes to see the current routing table
|
||||
//
|
||||
|
||||
var http = require('http'),
|
||||
httpProxy = require('http-proxy'),
|
||||
log = require('loglevel');
|
||||
|
||||
var bound = function (that, method) {
|
||||
// bind a method, to ensure `this=that` when it is called
|
||||
// because prototype languages are bad
|
||||
return function () {
|
||||
method.apply(that, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
var arguments_array = function (args) {
|
||||
// cast arguments object to array, because Javascript.
|
||||
return Array.prototype.slice.call(args, 0);
|
||||
};
|
||||
|
||||
var json_handler = function (handler) {
|
||||
// wrap json handler, so the handler is called with parsed data,
|
||||
// rather than implementing streaming parsing in the handler itself
|
||||
return function (req, res) {
|
||||
var args = arguments_array(arguments);
|
||||
var buf = '';
|
||||
req.on('data', function (chunk) {
|
||||
buf += chunk;
|
||||
});
|
||||
req.on('end', function () {
|
||||
try {
|
||||
data = JSON.parse(buf) || {};
|
||||
} catch (e) {
|
||||
that.fail(req, res, 400, "Body not valid JSON: " + e);
|
||||
return;
|
||||
}
|
||||
args.push(data);
|
||||
handler.apply(handler, args);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
var authorized = function (method) {
|
||||
/// decorator for token-authorized handlers
|
||||
return function (req, res) {
|
||||
if (!this.auth_token) {
|
||||
return method.apply(this, arguments);
|
||||
}
|
||||
var match = (req.headers.authorization || '').match(/token\s+(\S+)/);
|
||||
var token;
|
||||
if (match !== null) {
|
||||
token = match[1];
|
||||
}
|
||||
if (token == this.auth_token) {
|
||||
return method.apply(this, arguments);
|
||||
} else {
|
||||
res.writeHead(403);
|
||||
res.end();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var ConfigurableProxy = function (options) {
|
||||
var that = this;
|
||||
this.options = options || {};
|
||||
this.auth_token = this.options.auth_token;
|
||||
this.upstream_ip = this.options.upstream_ip || 'localhost';
|
||||
this.upstream_port = this.options.upstream_port || 8081;
|
||||
|
||||
this.default_target = "http://" + this.upstream_ip + ":" + this.upstream_port;
|
||||
this.routes = {};
|
||||
|
||||
var proxy = this.proxy = httpProxy.createProxyServer({
|
||||
ws : true
|
||||
});
|
||||
|
||||
// tornado-style regex routing,
|
||||
// because cross-language cargo-culting is always a good idea
|
||||
|
||||
this.api_handlers = [
|
||||
[ /^\/api\/routes$/, {
|
||||
get : bound(this, authorized(this.get_routes))
|
||||
} ],
|
||||
[ /^\/api\/routes(\/.*)$/, {
|
||||
post : json_handler(bound(this, authorized(this.post_routes))),
|
||||
'delete' : bound(this, authorized(this.delete_routes))
|
||||
} ]
|
||||
];
|
||||
|
||||
var log_errors = function(handler) {
|
||||
return function (req, res) {
|
||||
try {
|
||||
return handler.apply(that, arguments);
|
||||
} catch (e) {
|
||||
log.error("Error in handler for " +
|
||||
req.method + ' ' + req.url + ': ', e
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// handle API requests
|
||||
this.api_server = http.createServer(
|
||||
log_errors(that.handle_api_request)
|
||||
);
|
||||
|
||||
// proxy web requests separately
|
||||
this.proxy_server = http.createServer(
|
||||
log_errors(that.handle_proxy_web)
|
||||
);
|
||||
// proxy websockets
|
||||
this.proxy_server.on('upgrade', bound(this, this.handle_proxy_ws));
|
||||
};
|
||||
|
||||
ConfigurableProxy.prototype.fail = function (req, res, code, msg) {
|
||||
// log a failure, and finish the HTTP request with an error code
|
||||
msg = msg || '';
|
||||
log.error('[' + code + ']', req.method, req.url + ':', msg);
|
||||
res.writeHead(code);
|
||||
res.write(msg);
|
||||
res.end();
|
||||
};
|
||||
|
||||
ConfigurableProxy.prototype.add_route = function (path, data) {
|
||||
// add a route to the routing table
|
||||
this.routes[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];
|
||||
}
|
||||
};
|
||||
|
||||
ConfigurableProxy.prototype.get_routes = function (req, res) {
|
||||
// GET returns routing table as JSON dict
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.write(JSON.stringify(this.routes));
|
||||
res.end();
|
||||
};
|
||||
|
||||
ConfigurableProxy.prototype.post_routes = function (req, res, path, data) {
|
||||
// POST adds a new route
|
||||
log.debug('POST', path, data);
|
||||
|
||||
// ensure path starts with /
|
||||
if (path[0] != '/') {
|
||||
path = '/' + path;
|
||||
}
|
||||
// ensure path *doesn't* end with /
|
||||
if (path[path.length - 1] == '/') {
|
||||
path = path.substr(0, path.length - 1);
|
||||
}
|
||||
this.add_route(path, data);
|
||||
res.writeHead(201);
|
||||
res.end();
|
||||
};
|
||||
|
||||
ConfigurableProxy.prototype.delete_routes = function (req, res, path) {
|
||||
// DELETE removes an existing route
|
||||
log.debug('DELETE', path);
|
||||
if (this.routes[path] === undefined) {
|
||||
res.writeHead(404);
|
||||
} else {
|
||||
this.remove_route(path, data);
|
||||
res.writeHead(202);
|
||||
}
|
||||
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
|
||||
prefix_parts = prefix.split('/');
|
||||
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];
|
||||
}
|
||||
}
|
||||
// no custom target, fall back to default
|
||||
return ['/', this.default_target];
|
||||
};
|
||||
|
||||
ConfigurableProxy.prototype.update_last_activity = function (prefix) {
|
||||
// note last activity in routing table
|
||||
if (prefix === '/') {
|
||||
return;
|
||||
}
|
||||
this.routes[prefix].last_activity = new Date();
|
||||
};
|
||||
|
||||
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);
|
||||
var prefix = both[0];
|
||||
var target = both[1];
|
||||
log.debug("PROXY", kind.toUpperCase(), req.url, "to", target);
|
||||
|
||||
// pop method off the front
|
||||
var args = arguments_array(arguments);
|
||||
args.shift();
|
||||
|
||||
// add config argument
|
||||
args.push({
|
||||
target: target
|
||||
});
|
||||
|
||||
// add error handling
|
||||
args.push(function (e) {
|
||||
log.error("Proxy error: ", e);
|
||||
res.writeHead(502);
|
||||
res.write("Proxy target missing");
|
||||
res.end();
|
||||
});
|
||||
|
||||
// update last activity timestamp in routing table
|
||||
this.update_last_activity(prefix);
|
||||
|
||||
// update timestamp on any reply data as well (this includes websocket data)
|
||||
req.on('data', function () {
|
||||
that.update_last_activity(prefix);
|
||||
});
|
||||
res.on('data', function () {
|
||||
that.update_last_activity(prefix);
|
||||
});
|
||||
|
||||
// dispatch the actual method
|
||||
this.proxy[kind].apply(this.proxy, args);
|
||||
};
|
||||
|
||||
ConfigurableProxy.prototype.handle_proxy_ws = function (req, res, head) {
|
||||
// Proxy a websocket request
|
||||
return this.handle_proxy('ws', req, res, head);
|
||||
};
|
||||
|
||||
ConfigurableProxy.prototype.handle_proxy_web = function (req, res) {
|
||||
// Proxy a web request
|
||||
return this.handle_proxy('web', req, res);
|
||||
};
|
||||
|
||||
ConfigurableProxy.prototype.handle_api_request = function (req, res) {
|
||||
// Handle a request to the REST API
|
||||
for (var i = 0; i < this.api_handlers.length; i++) {
|
||||
var pat = this.api_handlers[i][0];
|
||||
var match = pat.exec(req.url);
|
||||
if (match) {
|
||||
var handlers = this.api_handlers[i][1];
|
||||
var handler = handlers[req.method.toLowerCase()];
|
||||
if (!handler) {
|
||||
// 405 on found resource, but not found method
|
||||
this.fail(req, res, 405, "Method not supported.");
|
||||
return;
|
||||
}
|
||||
var args = [req, res];
|
||||
match.slice(1).forEach(function (arg) { args.push(arg); });
|
||||
handler.apply(handler, args);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.fail(req, res, 404);
|
||||
};
|
||||
|
||||
exports.ConfigurableProxy = ConfigurableProxy;
|
57
lib/main.js
57
lib/main.js
@@ -1,57 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
cli entrypoint for starting a Configurable Proxy
|
||||
|
||||
*/
|
||||
var fs = require('fs'),
|
||||
args = require('commander'),
|
||||
log = require('loglevel');
|
||||
|
||||
args
|
||||
.version('0.0.1')
|
||||
// .option('-h', '--help', 'show help')
|
||||
.option('--key <keyfile>', 'SSL key to use, if any')
|
||||
.option('--cert <certfile>', 'SSL certificate to use, if any')
|
||||
.option('--ip <n>', 'Public-facing IP of the proxy', 'localhost')
|
||||
.option('--port <n>', 'Public-facing port of the proxy', parseInt)
|
||||
.option('--upstream-ip <ip>', 'IP address of the default proxy target', 'localhost')
|
||||
.option('--upstream-port <n>', 'Port of the default proxy target', parseInt)
|
||||
.option('--api-ip <ip>', 'Inward-facing IP for API request', 'localhost')
|
||||
.option('--api-port <n>', 'Inward-facing port for API request', parseInt)
|
||||
.option('--log-level <loglevel>', 'Log level (debug, info, warn, error)', 'info');
|
||||
|
||||
args.parse(process.argv);
|
||||
log.setLevel(args.logLevel);
|
||||
|
||||
var ConfigurableProxy = require('./configproxy.js').ConfigurableProxy;
|
||||
|
||||
var options = {};
|
||||
if (args.key) {
|
||||
options.key = fs.readFileSync(args.key);
|
||||
}
|
||||
|
||||
if (args.cert) {
|
||||
options.cert = fs.readFileSync(args.cert);
|
||||
}
|
||||
|
||||
// because camelCase is the js way!
|
||||
options.upstream_ip = args.upstreamIp;
|
||||
options.upstream_port = args.upstreamPort;
|
||||
options.auth_token = process.env.CONFIGPROXY_AUTH_TOKEN;
|
||||
|
||||
var proxy = new ConfigurableProxy(options);
|
||||
|
||||
var listen = {};
|
||||
listen.port = args.port || 8000;
|
||||
listen.ip = args.ip;
|
||||
listen.api_ip = args.apiIp || 'localhost';
|
||||
listen.api_port = args.apiPort || listen.port + 1;
|
||||
|
||||
proxy.proxy_server.listen(listen.port, listen.ip);
|
||||
proxy.api_server.listen(listen.api_port, listen.api_ip);
|
||||
|
||||
log.info("Proxying %s:%s to %s:%s", (listen.ip || '*'), listen.port,
|
||||
proxy.upstream_ip, proxy.upstream_port
|
||||
);
|
||||
log.info("Proxy API at %s:%s/api/routes", (listen.api_ip || '*'), listen.api_port);
|
||||
|
22
package.json
22
package.json
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "configurable-http-proxy",
|
||||
"description": "A configurable-on-the-fly HTTP Proxy",
|
||||
"author": "IPython Developers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jupyter/jupyterhub"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "~2.2",
|
||||
"http-proxy": "~1.1",
|
||||
"loglevel": "~1.0"
|
||||
},
|
||||
"files": [
|
||||
"lib/configproxy.js",
|
||||
"lib/main.js"
|
||||
],
|
||||
"bin": {
|
||||
"configurable-http-proxy": "lib/main.js"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user