diff --git a/gateway.py b/gateway.py deleted file mode 100644 index 246d099..0000000 --- a/gateway.py +++ /dev/null @@ -1,118 +0,0 @@ -from twisted.internet import protocol, reactor -from twisted.web import static, server, proxy -from twisted.web.resource import Resource -import base64, urllib -from os import fork, setsid, umask, dup2 -from sys import stdin, stdout, stderr -from os import getpid -from urllib import quote as urlquote - -pid_file = "/home/tahoe/gateway/gateway.pid" - -modHeaders = { - 'Cache-Control': 'max-age=31536000', - 'Server': 'Cryto-Tahoe-Gateway 1.1', - 'Expires': 'Fri, 12 Dec 2012 05:00:00 GMT' -} - - -outfile = open(pid_file, 'w') -outfile.write('%i' % getpid()) -outfile.close() -if fork(): exit(0) -umask(0) -setsid() -if fork(): exit(0) - -stdout.flush() -stderr.flush() -si = file('/dev/null', 'r') -so = file('/dev/null', 'a+') -se = file('/dev/null', 'a+', 0) -dup2(si.fileno(), stdin.fileno()) -dup2(so.fileno(), stdout.fileno()) -dup2(se.fileno(), stderr.fileno()) - -error_index = ("""\ - - - - Cryto-Tahoe-Gateway: Index - - -

This gateway does not provide an index page.

- Please use a direct URL to download a file hosted on this storage grid.

- Alternatively, learn more about the Cryto Coding Collective. -



-
Cryto-Tahoe-Gateway v1.1
- - """) - -error_404 = ("""\ - - - - Cryto-Tahoe-Gateway: 404 - - -

The specified resource was not found.

- The file may have expired, or the hyperlink you followed may have been broken.

- Learn more about the Cryto Coding Collective. -



-
Cryto-Tahoe-Gateway v1.1
- - """) - - -class ProxyClient(proxy.ProxyClient): - """A proxy class that injects headers to the response.""" - def handleEndHeaders(self): - for key, value in modHeaders.iteritems(): - self.father.responseHeaders.setRawHeaders(key, [value]) - - -class ProxyClientFactory(proxy.ProxyClientFactory): - protocol = ProxyClient - - -class ReverseProxyResource(proxy.ReverseProxyResource): - proxyClientFactoryClass = ProxyClientFactory - - def getChild(self, path, request): - return ReverseProxyResource( - self.host, self.port, self.path + '/' + urlquote(path, safe="")) - - - -class GatewayResource(Resource): - isLeaf = False - allowedMethods = ("GET") - - def getChild(self, name, request): - if name == "download": - try: - uri = request.path - uriParts = uri.split('/') - uriIdentifier = base64.urlsafe_b64decode(uriParts[2]) - fileName = uriParts[3] - localUri = "/file/" + urllib.quote(uriIdentifier) + "/@@named=/" + urllib.quote(fileName) - return ReverseProxyResource('localhost', 3456, localUri) - except: - return self - else: - return self - - def render_GET(self, request): - path = request.path - if path == "/": - return error_index - else: - return error_404 - -resource = GatewayResource() - -site = server.Site(resource) -reactor.listenTCP(3719, site) -reactor.run() - - diff --git a/gateway/gateway_wsgi.py b/gateway/gateway_wsgi.py new file mode 100644 index 0000000..3c59435 --- /dev/null +++ b/gateway/gateway_wsgi.py @@ -0,0 +1,228 @@ +""" + Cryto.net Tahoe-LAFS -> WSGI Gateway Proxy + -------------------- + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + + This wsgi application's entry point is `app`, you can run it with gunicorn: + gunicorn gateway:app [options] + + Or you can run it stand-alone: + python gateway.py [options, --help] + + +""" + +from wsgiref.util import is_hop_by_hop +import base64 +import socket +import urllib +import urllib2 +import logging +import time + +gatewayLog = logging.getLogger("gateway-wsgi") + +_config_gatewayTimeout = 15 +_config_tahoeServer = 'localhost:3456' +_config_chunkSize = 1 << 13 # 8192, 8kb chunks. + +_response_skeleton =\ +""" + + + + %(page_title)s + + +

%(page_header)s

+ %(page_text)s

+ Learn more about the Cryto Coding Collective. +



+
Cryto-Tahoe-Gateway v1.1
+ + +""" + +def error_404(environ, start_response): + """ + Returns a 404 page to the client. + """ + start_response("404 Not Found", [("Content-Type", "text/html")]) + return [_response_skeleton % dict( + page_title = 'Cryto-Tahoe-Gateway: 404', + page_header = 'The specified resource was not found.', + page_text = 'The file may have expired, or the hyperlink you followed may have been broken.' + )] + +def error_500(environ, start_response): + """ + Returns a 500 page to the client. + """ + start_response("500 Internal Server Error", [("Content-Type", "text/html")]) + return [_response_skeleton % dict( + page_title = 'Cryto-Tahoe-Gateway: 500', + page_header = 'An error has occurred, and the gateway could not process your request, please try again.', + page_text = '' + )] + +def error_50x(environ, start_response, code = '502', message = "Gateway Timeout"): + """ + Generic error page... can return any type of error, not only 502. + """ + start_response("%s %s" % (code, message), [("Content-Type", "text/html")]) + return [_response_skeleton % dict( + page_title = 'Cryto-Tahoe-Gateway: %s' % code, + page_header = 'Gateway Error %s: %s'% (code, message), + page_text = '' + )] + +def index(environ, start_response): + """ + Generic index. + """ + start_response("200 OK", [("Content-Type", "text/html")]) + return [_response_skeleton % dict( + page_title = "Cryto-Tahoe-Gateway: Index", + page_header = "This gateway does not provide an index page.", + page_text = "Please use a direct URL to download a file hosted on this storage grid." + )] + +def proxy_pass(environ, start_response): + """ + Proxy the request to tahoe. + """ + + path = environ['PATH_INFO'] + pathParts = path.split('/') + if len(pathParts) != 4: + raise NotFoundError() + + # Convert url to tahoe-type URL. + _, _, urlIdentifier, fileName = pathParts + urlIdentifier = urllib.quote(base64.urlsafe_b64decode(urlIdentifier)) + fileName = urllib.quote(fileName) + localUri = "http://%s/file/%s/@@named=/%s" % (_config_tahoeServer, urlIdentifier, fileName) + gatewayLog.debug("Proxy passing request (ident: %s, file: %s)", urlIdentifier, fileName) + + # The actual proxying starts here. + try: + fp = urllib2.urlopen(localUri, timeout = _config_gatewayTimeout) + + except urllib2.HTTPError, e: + + # Eat any non 200 errors + gatewayLog.exception("HTTP Error") + if int(e.code) == 404: + raise NotFoundError() + else: + return error_50x(environ, start_response, e.code, e.msg) + + except urllib2.URLError, e: + + # Something went awry connecting to the backend. + gatewayLog.exception("Error connecting to backend...") + try: + if isinstance(e.args[0], Exception): + raise e.args[0] + else: + raise e + except socket.timeout: + return error_50x(environ, start_response, '504', "Gateway Timeout") + except Exception, e: + return error_50x(environ, start_response, '503', "Service Unavailable") + + else: + + # Do the actual proxying + data_sent = 0 + req_start = time.time() + content_length = fp.info().getheader("Content-Length", 0) + + try: + response_headers = [(k, v) for k, v in fp.info().items() if not is_hop_by_hop(k)] + write = start_response("200 OK", response_headers) + + while True: + chunk = fp.read(_config_chunkSize) + if not chunk: + break + write(chunk) + data_sent += len(chunk) + + gatewayLog.debug("Finished proxied request of %s, elapsed: %.02fs, transfer: %s bytes.", fileName, + time.time() - req_start, data_sent) + + except Exception, e: + gatewayLog.exception("Error transfering proxied content... %s, sent %s of %s, elapsed %.02f", + fileName, data_sent, content_length, time.time() - req_start) + + finally: + fp.close() + + # Wsgi spec says we have to return an empty iterable ._. + return () + +class NotFoundError(Exception): + pass + +def app(environ, start_response): + """ + Application entry point, provide me to a wsgi handler! + """ + try: + path = environ['PATH_INFO'] + if not path or path == '/': + return index(environ, start_response) + elif path.startswith('/download/'): + return proxy_pass(environ, start_response) + else: + raise NotFoundError() + + except NotFoundError: + return error_404(environ, start_response) + + except Exception, e: + gatewayLog.exception("WSGI Application encountered error") + return error_500(environ, start_response) + +def main(): + from optparse import OptionParser + + parser = OptionParser() + parser.add_option("-i", '--interface', dest = "interface", help = "interface to bind on", default = "127.0.0.1") + parser.add_option("-p", '--listen-port', dest = "listen_port", help = "port to listen on", type="int", + default = 3719) + parser.add_option("-u", '--tahoe-url', dest = "tahoe_url", help = "address that tahoe is listening on, in form" + " host:port", default = 'localhost:3456') + parser.add_option("-c", '--proxy-chunk-size', dest = "chunk_size", help = "chunk size to read while proxying", + type = "int", default = 1 << 13) + + parser.add_option("-t", '--gateway-timeout', dest = "gateway_timeout", help = "timeout while connecting to gateway", + type = "int", default = 15) + + parser.add_option("-d", '--debug', dest = "debug", action = "store_true", default = False, + help = "debug logging level") + + options, args = parser.parse_args() + global _config_chunkSize, _config_gatewayTimeout, _config_tahoeServer + _config_chunkSize = options.chunk_size + _config_gatewayTimeout = options.gateway_timeout + _config_tahoeServer = options.tahoe_url + + if options.debug: + gatewayLog.setLevel(logging.DEBUG) + logging.basicConfig() + + import wsgiref.simple_server + wsgiref.simple_server.make_server(options.interface, options.listen_port, app).serve_forever() + +if __name__ == '__main__': + main() diff --git a/gateway/initscript/tahoegateway b/gateway/initscript/tahoegateway new file mode 100644 index 0000000..b212f43 --- /dev/null +++ b/gateway/initscript/tahoegateway @@ -0,0 +1,74 @@ +#! /bin/bash +### BEGIN INIT INFO +# Provides: Tahoe-LAFS Gateway +### END INIT INFO + +#### SERVER SPECIFIC CONFIGURATION +GATEWAY_PATH="/home/tahoe/gateway/run" +PID_PATH="/home/tahoe/gateway/gateway.pid" +RUN_AS=tahoe +#### DO NOT CHANGE ANYTHING AFTER THIS LINE! + +set -e + +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin +DESC="Tahoe-LAFS Gateway" +NAME=$0 +SCRIPTNAME=/etc/init.d/$NAME + +# +# Function that starts the daemon/service. +# +d_start() +{ + # Starting Tahoe-LAFS Gateway + if [ -f $PID_PATH ]; then + echo -n ", already running" + else + start-stop-daemon --start --quiet --pidfile $PID_PATH \ + --chuid $RUN_AS --exec /usr/bin/env -- $GATEWAY_PATH + chmod 400 $PID_PATH + fi +} + +# +# Function that stops the daemon/service. +# +d_stop() { + # Killing the Tahoe-LAFS Gateway + start-stop-daemon --stop --quiet --pidfile $PID_PATH \ + || echo -n ", not running" + if [ -f $PID_PATH ]; then + rm $PID_PATH + fi +} + +ACTION="$1" +case "$ACTION" in + start) + echo -n "Starting $DESC: $NAME" + d_start + echo "." + ;; + + stop) + echo -n "Stopping $DESC: $NAME" + d_stop + echo "." + ;; + + restart|force-reload) + echo -n "Restarting $DESC: $NAME" + d_stop + sleep 1 + d_start + echo "." + ;; + + *) + echo "Usage: $NAME {start|stop|restart|force-reload}" >&2 + exit 3 + ;; +esac + +exit 0 diff --git a/gateway/run b/gateway/run new file mode 100644 index 0000000..3126965 --- /dev/null +++ b/gateway/run @@ -0,0 +1,4 @@ +#!/bin/bash +cd /home/tahoe/gateway +gunicorn -b 0.0.0.0:3719 -k gevent gateway_wsgi:app >/dev/null 2>/dev/null & +echo $! > gateway.pid