You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

229 lines
7.6 KiB
Python

"""
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()