init with basic working example

This commit is contained in:
MinRK
2014-06-12 16:22:06 -07:00
commit d4c5e70ed3
7 changed files with 452 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules
*.pyx
*~
.DS_Store

30
README.md Normal file
View File

@@ -0,0 +1,30 @@
# Proof of concept for configurable proxy
This is a proof of concept implementation of a configurable proxy for managed multi-user webapps,
ultimately for use in IPython.
Three actors:
- multi user server (tornado process)
- configurable http proxy (node-http-proxy)
- multiple single user servers (tornado)
Basic principals:
- MUS spawns proxy
- proxy forwards ~all requests to MUS by default
- MUS handles login, and spawns single-user servers on demand
- MUS configures proxy to forward url prefixes to single-user servers
## dependencies
npm install node-http-proxy
pip install tornado
## to use
$> python multiuser.py
visit `http://localhost:8000`, and login (any username, password=`password`).

159
lib/configproxy.js Normal file
View File

@@ -0,0 +1,159 @@
var http = require('http'),
httpProxy = require('http-proxy');
// director = require('director');
var bound = function (that, method) {
return function () {
method.apply(that, arguments);
};
};
var arguments_array = function (args) {
// cast arguments object to array, because it isn't one already (?!)
return Array.prototype.slice.call(args, 0);
};
var json_handler = function (handler) {
// wrap json handler
return function (req, res) {
var args = arguments_array(arguments);
var buf = '';
req.on('data', function (chunk) {
console.log('data', chunk);
buf += chunk;
});
req.on('end', function () {
console.log('buf', buf);
try {
data = JSON.parse(buf) || {};
} catch (e) {
that.fail(res, 400, "Body not valid JSON: " + e);
return;
}
args.push(data);
handler.apply(handler, args);
});
};
};
var ConfigurableProxy = function (options) {
var that = this;
this.options = options || {};
this.upstream_port = this.options.upstream_port || 8001;
this.default_target = 'http://localhost:' + this.upstream_port;
this.routes = {};
this.proxy = httpProxy.createProxyServer({
ws : true
});
// tornado-style regex routing,
// because cross-language cargo-culting is always a good idea
this.handlers = [
[ /^\/api\/routes$/, {
get : bound(this, this.get_routes)
} ],
[ /^\/api\/routes(\/.*)$/, {
post : json_handler(bound(this, this.post_routes)),
'delete' : bound(this, this.delete_routes)
} ]
];
this.server = http.createServer(
function (req, res) {
try {
return that.handle_request(req, res);
} catch (e) {
console.log("Error in handler for " +
req.method + ' ' + req.url + ': ', e
);
}
}
);
};
ConfigurableProxy.prototype.listen = function (port) {
this.server.listen(port);
};
ConfigurableProxy.prototype.fail = function (res, code, msg) {
res.writeHead(code);
res.write(msg);
res.end();
};
ConfigurableProxy.prototype.add_route = function (path, data) {
this.routes[path] = data;
};
ConfigurableProxy.prototype.remove_route = function (path, data) {
if (this.routes[path] !== undefined) {
delete this.routes[path];
}
};
ConfigurableProxy.prototype.get_routes = function (req, res) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.write(JSON.stringify(this.routes));
res.end();
};
ConfigurableProxy.prototype.post_routes = function (req, res, path, data) {
console.log('post', path, data);
this.add_route(path, data);
res.writeHead(201);
res.end();
};
ConfigurableProxy.prototype.delete_routes = function (req, res, path) {
console.log('delete', path);
this.add_route(path, data);
res.writeHead(202);
res.end();
};
ConfigurableProxy.prototype.target_for_url = function (url) {
// return proxy target for a given url
for (var path in this.routes) {
if (url.indexOf(path) === 0) {
return this.routes[path].target;
}
}
// no custom target, fall back to default
return this.default_target;
};
ConfigurableProxy.prototype.handle_request = function (req, res) {
console.log("handle", req.method, req.url);
for (var i = 0; i < this.handlers.length; i++) {
var pat = this.handlers[i][0];
var match = pat.exec(req.url);
if (match) {
var handlers = this.handlers[i][1];
var handler = handlers[req.method.toLowerCase()];
if (!handler) {
// 405 on found resource, but not found method
this.fail(res, 405, req.method + " " + req.url + " not supported.");
return;
}
console.log("match", pat, req.url, match);
var args = [req, res];
match.slice(1).forEach(function (arg){ args.push(arg); });
handler.apply(handler, args);
return;
}
}
// no local route found, time to proxy
var target = this.target_for_url(req.url);
console.log("proxy " + req.url + " to " + target);
this.proxy.web(req, res, {
target: target
}, function (e) {
console.log("Proxy error: ", e);
res.writeHead(502);
res.write("Proxy target missing");
res.end();
});
};
exports.ConfigurableProxy = ConfigurableProxy;

18
login.html Normal file
View File

@@ -0,0 +1,18 @@
<html>
<head>
</head>
<body>
{% if message %}
<div id="message">{{message}}</div>
{% end if %}
<form action="/login?next={{next}}" method="post">
<input type="text" name="user" id="user_input" value="{{user}}">
<input type="password" name="password" id="password_input">
<button type="submit" id="login_submit">Log in</button>
</form>
<script type="text/javascript">
</script>
</body>
</html>

186
multiuser.py Normal file
View File

@@ -0,0 +1,186 @@
#!/usr/bin/env python
import socket
import sys
import uuid
from subprocess import Popen
import json
import requests
import time
import tornado.httpserver
import tornado.ioloop
import tornado.options
from tornado.log import app_log
from tornado.escape import url_escape
from tornado.httputil import url_concat
from tornado.web import RequestHandler, Application
from tornado import web
from tornado.options import define, options
def random_port():
sock = socket.socket()
sock.bind(('', 0))
port = sock.getsockname()[1]
sock.close()
return port
define("port", default=8001, help="run on the given port", type=int)
class SingleUserManager(object):
routes_t = 'http://localhost:8000/api/routes{uri}'
single_user_t = 'http://localhost:{port}'
def __init__(self):
self.processes = {}
self.ports = {}
def _wait_for_port(self, port, timeout=2):
tic = time.time()
while time.time() - tic < timeout:
try:
socket.create_connection(('localhost', port))
except socket.error:
time.sleep(0.1)
else:
break
def spawn(self, user):
assert user not in self.processes
port = random_port()
self.processes[user] = Popen(
[sys.executable, 'singleuser.py', '--port=%i' % port, '--user=%s' % user])
self.ports[user] = port
r = requests.post(
self.routes_t.format(uri=u'/user/%s' % user),
data=json.dumps(dict(
target=self.single_user_t.format(port=port),
user=user,
)),
)
self._wait_for_port(port)
r.raise_for_status()
print("spawn done")
def exists(self, user):
if user in self.processes:
if self.processes[user].poll() is None:
return True
else:
self.processes.pop(user)
self.ports.pop(user)
return False
def get(self, user):
"""ensure process exists and return its port"""
if not self.exists(user):
self.spawn(user)
return self.ports[user]
def shutdown(self, user):
assert user in self.processes
port = self.ports[user]
self.processes[user].terminate()
r = requests.delete(self.routes_url,
data=json.dumps(user=user, port=port),
)
r.raise_for_status()
class BaseHandler(RequestHandler):
cookie_name = 'multiusertest'
def get_current_user(self):
user = self.get_cookie(self.cookie_name, '')
if user:
return user
@property
def user_manager(self):
return self.settings['user_manager']
def clear_login_cookie(self):
self.clear_cookie(self.cookie_name)
class MainHandler(BaseHandler):
@web.authenticated
def get(self):
self.redirect("/user/%s" % self.get_current_user())
class UserHandler(BaseHandler):
@web.authenticated
def get(self, user):
self.write("multi-user at single-user url: %s" % user)
if self.get_current_user() == user:
self.user_manager.spawn(user)
self.redirect('')
else:
self.clear_login_cookie()
self.redirect(url_concat(self.settings['login_url'], {
'next' : '/user/%s' % user
}))
class LogoutHandler(BaseHandler):
def get(self):
self.clear_login_cookie()
self.write("logged out")
class LoginHandler(BaseHandler):
def _render(self, message=None, user=None):
self.render('login.html',
next=url_escape(self.get_argument('next', default='')),
user=user,
message=message,
)
def get(self):
if self.get_current_user():
self.redirect(self.get_argument('next', default='/'))
else:
user = self.get_argument('user', default='')
self._render(user=user)
def post(self):
user = self.get_argument('user', default='')
pwd = self.get_argument('password', default=u'')
next_url = self.get_argument('next', default='') or '/user/%s' % user
if user and pwd == 'password':
self.set_cookie(self.cookie_name, user)
else:
self._render(
message={'error': 'Invalid username or password'},
user=user,
)
return
self.redirect(next_url)
def main():
tornado.options.parse_command_line()
application = Application([
(r"/", MainHandler),
(r"/login", LoginHandler),
# (r"/shutdown/([^/]+)", ShutdownHandler),
# (r"/start/([^/]+)", StartHandler),
(r"/user/([^/]+)/?.*", UserHandler),
],
user_manager=SingleUserManager(),
cookie_secret='super secret',
login_url='/login',
)
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(options.port)
proxy = Popen(["node", "proxy.js"])
try:
tornado.ioloop.IOLoop.instance().start()
finally:
proxy.terminate()
if __name__ == "__main__":
main()

6
proxy.js Normal file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env node
var ConfigurableProxy = require('./lib/configproxy.js').ConfigurableProxy;
var proxy = new ConfigurableProxy();
proxy.listen(8000);

49
singleuser.py Normal file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python
import tornado.httpserver
import tornado.ioloop
import tornado.options
from tornado.web import RequestHandler, Application
from tornado import web
from tornado.log import app_log
from tornado.options import define, options
define("port", default=8888, help="run on the given port", type=int)
define("user", default='', help="my username", type=str)
class BaseHandler(RequestHandler):
cookie_name = 'multiusertest'
@property
def user(self):
return self.settings['user']
def get_current_user(self):
user = self.get_cookie(self.cookie_name, '')
if user and user == self.user:
return user
else:
raise web.HTTPError(403, "User %s does not have access to %s" % (user, self.user))
class MainHandler(BaseHandler):
@web.authenticated
def get(self, uri):
self.write("single-user %s: %s" % (self.user, uri))
def main():
tornado.options.parse_command_line()
application = Application([
(r"(.*)", MainHandler),
],
user=options.user,
login_url='/login',
)
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(options.port)
app_log.info("single user %s listening on %s" % (options.user, options.port))
tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
main()