New gateway script
parent
444f294bea
commit
235bf849fe
@ -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 = ("""\
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Cryto-Tahoe-Gateway: Index</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>This gateway does not provide an index page.</h2>
|
||||
Please use a direct URL to download a file hosted on this storage grid.<br><br>
|
||||
Alternatively, <a href="http://cryto.net/">learn more about the Cryto Coding Collective</a>.
|
||||
<br><br><br><hr>
|
||||
<div style="text-align: right; font-style: italic;">Cryto-Tahoe-Gateway v1.1</div>
|
||||
</body>
|
||||
</html>""")
|
||||
|
||||
error_404 = ("""\
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Cryto-Tahoe-Gateway: 404</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>The specified resource was not found.</h2>
|
||||
The file may have expired, or the hyperlink you followed may have been broken.<br><br>
|
||||
<a href="http://cryto.net/">Learn more about the Cryto Coding Collective</a>.
|
||||
<br><br><br><hr>
|
||||
<div style="text-align: right; font-style: italic;">Cryto-Tahoe-Gateway v1.1</div>
|
||||
</body>
|
||||
</html>""")
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
@ -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 =\
|
||||
"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>%(page_title)s</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>%(page_header)s</h2>
|
||||
%(page_text)s<br><br>
|
||||
<a href="http://cryto.net/">Learn more about the Cryto Coding Collective</a>.
|
||||
<br><br><br><hr>
|
||||
<div style="text-align: right; font-style: italic;">Cryto-Tahoe-Gateway v1.1</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
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()
|
@ -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
|
@ -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
|
Loading…
Reference in New Issue