11 changed files with 3359 additions and 16 deletions
@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python |
||||
|
||||
import hmac |
||||
from hashlib import sha1 |
||||
from os.path import basename |
||||
from sys import argv, exit |
||||
from time import time |
||||
|
||||
|
||||
if __name__ == '__main__': |
||||
if len(argv) != 7: |
||||
prog = basename(argv[0]) |
||||
print 'Syntax: %s <path> <redirect> <max_file_size> ' \ |
||||
'<max_file_count> <seconds> <key>' % prog |
||||
print |
||||
print 'Where:' |
||||
print ' <path> The prefix to use for form uploaded' |
||||
print ' objects. For example:' |
||||
print ' /v1/account/container/object_prefix_ would' |
||||
print ' ensure all form uploads have that path' |
||||
print ' prepended to the browser-given file name.' |
||||
print ' <redirect> The URL to redirect the browser to after' |
||||
print ' the uploads have completed.' |
||||
print ' <max_file_size> The maximum file size per file uploaded.' |
||||
print ' <max_file_count> The maximum number of uploaded files' |
||||
print ' allowed.' |
||||
print ' <seconds> The number of seconds from now to allow' |
||||
print ' the form post to begin.' |
||||
print ' <key> The X-Account-Meta-Temp-URL-Key for the' |
||||
print ' account.' |
||||
print |
||||
print 'Example output:' |
||||
print ' Expires: 1323842228' |
||||
print ' Signature: 18de97e47345a82c4dbfb3b06a640dbb' |
||||
exit(1) |
||||
path, redirect, max_file_size, max_file_count, seconds, key = argv[1:] |
||||
try: |
||||
max_file_size = int(max_file_size) |
||||
except ValueError: |
||||
max_file_size = -1 |
||||
if max_file_size < 0: |
||||
print 'Please use a <max_file_size> value greater than or equal to 0.' |
||||
exit(1) |
||||
try: |
||||
max_file_count = int(max_file_count) |
||||
except ValueError: |
||||
max_file_count = 0 |
||||
if max_file_count < 1: |
||||
print 'Please use a positive <max_file_count> value.' |
||||
exit(1) |
||||
try: |
||||
expires = int(time() + int(seconds)) |
||||
except ValueError: |
||||
expires = 0 |
||||
if expires < 1: |
||||
print 'Please use a positive <seconds> value.' |
||||
exit(1) |
||||
parts = path.split('/', 4) |
||||
# Must be four parts, ['', 'v1', 'a', 'c'], must be a v1 request, have |
||||
# account and container values, and optionally have an object prefix. |
||||
if len(parts) < 4 or parts[0] or parts[1] != 'v1' or not parts[2] or \ |
||||
not parts[3]: |
||||
print '<path> must point to a container at least.' |
||||
print 'For example: /v1/account/container' |
||||
print ' Or: /v1/account/container/object_prefix' |
||||
exit(1) |
||||
sig = hmac.new(key, '%s\n%s\n%s\n%s\n%s' % (path, redirect, max_file_size, |
||||
max_file_count, expires), sha1).hexdigest() |
||||
print ' Expires:', expires |
||||
print 'Signature:', sig |
@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python |
||||
|
||||
import hmac |
||||
from hashlib import sha1 |
||||
from os.path import basename |
||||
from sys import argv, exit |
||||
from time import time |
||||
|
||||
|
||||
if __name__ == '__main__': |
||||
if len(argv) != 5: |
||||
prog = basename(argv[0]) |
||||
print 'Syntax: %s <method> <seconds> <path> <key>' % prog |
||||
print |
||||
print 'Where:' |
||||
print ' <method> The method to allow, GET or PUT.' |
||||
print ' Note: HEAD will also be allowed.' |
||||
print ' <seconds> The number of seconds from now to allow requests.' |
||||
print ' <path> The full path to the resource.' |
||||
print ' Example: /v1/AUTH_account/c/o' |
||||
print ' <key> The X-Account-Meta-Temp-URL-Key for the account.' |
||||
print |
||||
print 'Example output:' |
||||
print ' /v1/AUTH_account/c/o?temp_url_sig=34d49efc32fe6e3082e411e' \ |
||||
'eeb85bd8a&temp_url_expires=1323482948' |
||||
print |
||||
print 'This can be used to form a URL to give out for the access ' |
||||
print 'allowed. For example:' |
||||
print ' echo https://swift-cluster.example.com`%s GET 60 ' \ |
||||
'/v1/AUTH_account/c/o mykey`' % prog |
||||
print |
||||
print 'Might output:' |
||||
print ' https://swift-cluster.example.com/v1/AUTH_account/c/o?' \ |
||||
'temp_url_sig=34d49efc32fe6e3082e411eeeb85bd8a&' \ |
||||
'temp_url_expires=1323482948' |
||||
exit(1) |
||||
method, seconds, path, key = argv[1:] |
||||
if method not in ('GET', 'PUT'): |
||||
print 'Please use either the GET or PUT method.' |
||||
exit(1) |
||||
try: |
||||
expires = int(time() + int(seconds)) |
||||
except ValueError: |
||||
expires = 0 |
||||
if expires < 1: |
||||
print 'Please use a positive <seconds> value.' |
||||
exit(1) |
||||
parts = path.split('/', 4) |
||||
# Must be five parts, ['', 'v1', 'a', 'c', 'o'], must be a v1 request, have |
||||
# account, container, and object values, and the object value can't just |
||||
# have '/'s. |
||||
if len(parts) != 5 or parts[0] or parts[1] != 'v1' or not parts[2] or \ |
||||
not parts[3] or not parts[4].strip('/'): |
||||
print '<path> must point to an object.' |
||||
print 'For example: /v1/account/container/object' |
||||
exit(1) |
||||
sig = hmac.new(key, '%s\n%s\n%s' % (method, expires, path), |
||||
sha1).hexdigest() |
||||
print '%s?temp_url_sig=%s&temp_url_expires=%s' % (path, sig, expires) |
@ -0,0 +1,542 @@
|
||||
# Copyright (c) 2011 OpenStack, LLC. |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||
# you may not use this file except in compliance with the License. |
||||
# You may obtain a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, |
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||
# implied. |
||||
# See the License for the specific language governing permissions and |
||||
# limitations under the License. |
||||
|
||||
""" |
||||
FormPost Middleware |
||||
|
||||
Translates a browser form post into a regular Swift object PUT. |
||||
|
||||
The format of the form is:: |
||||
|
||||
<form action="<swift-url>" method="POST" |
||||
enctype="multipart/form-data"> |
||||
<input type="hidden" name="redirect" value="<redirect-url>" /> |
||||
<input type="hidden" name="max_file_size" value="<bytes>" /> |
||||
<input type="hidden" name="max_file_count" value="<count>" /> |
||||
<input type="hidden" name="expires" value="<unix-timestamp>" /> |
||||
<input type="hidden" name="signature" value="<hmac>" /> |
||||
<input type="file" name="file1" /><br /> |
||||
<input type="submit" /> |
||||
</form> |
||||
|
||||
The <swift-url> is the URL to the Swift desination, such as:: |
||||
|
||||
https://swift-cluster.example.com/AUTH_account/container/object_prefix |
||||
|
||||
The name of each file uploaded will be appended to the <swift-url> |
||||
given. So, you can upload directly to the root of container with a |
||||
url like:: |
||||
|
||||
https://swift-cluster.example.com/AUTH_account/container/ |
||||
|
||||
Optionally, you can include an object prefix to better separate |
||||
different users' uploads, such as:: |
||||
|
||||
https://swift-cluster.example.com/AUTH_account/container/object_prefix |
||||
|
||||
Note the form method must be POST and the enctype must be set as |
||||
"multipart/form-data". |
||||
|
||||
The redirect attribute is the URL to redirect the browser to after |
||||
the upload completes. The URL will have status and message query |
||||
parameters added to it, indicating the HTTP status code for the |
||||
upload (2xx is success) and a possible message for further |
||||
information if there was an error (such as "max_file_size exceeded"). |
||||
|
||||
The max_file_size attribute must be included and indicates the |
||||
largest single file upload that can be done, in bytes. |
||||
|
||||
The max_file_count attribute must be included and indicates the |
||||
maximum number of files that can be uploaded with the form. Include |
||||
additional ``<input type="file" name="filexx" />`` attributes if |
||||
desired. |
||||
|
||||
The expires attribute is the Unix timestamp before which the form |
||||
must be submitted before it's invalidated. |
||||
|
||||
The signature attribute is the HMAC-SHA1 signature of the form. Here is |
||||
sample code for computing the signature:: |
||||
|
||||
import hmac |
||||
from hashlib import sha1 |
||||
from time import time |
||||
path = '/v1/account/container/object_prefix' |
||||
redirect = 'https://myserver.com/some-page' |
||||
max_file_size = 104857600 |
||||
max_file_count = 10 |
||||
expires = int(time() + 600) |
||||
key = 'mykey' |
||||
hmac_body = '%s\\n%s\\n%s\\n%s\\n%s' % (path, redirect, |
||||
max_file_size, max_file_count, expires) |
||||
signature = hmac.new(key, hmac_body, sha1).hexdigest() |
||||
|
||||
The key is the value of the X-Account-Meta-Temp-URL-Key header on the |
||||
account. |
||||
|
||||
The command line tool ``swift-form-signature`` may be used (mostly |
||||
just when testing) to compute expires and signature. |
||||
|
||||
Also note that the file attributes must be after the other attributes |
||||
in order to be processed correctly. If attributes come after the |
||||
file, they won't be sent with the subrequest (there is no way to |
||||
parse all the attributes on the server-side without reading the whole |
||||
thing into memory -- to service many requests, some with large files, |
||||
there just isn't enough memory on the server, so attributes following |
||||
the file are simply ignored). |
||||
""" |
||||
|
||||
__all__ = ['FormPost', 'filter_factory', 'READ_CHUNK_SIZE', 'MAX_VALUE_LENGTH'] |
||||
|
||||
import hmac |
||||
import re |
||||
import rfc822 |
||||
from hashlib import sha1 |
||||
from StringIO import StringIO |
||||
from time import gmtime, strftime, time |
||||
from time import time |
||||
from urllib import quote, unquote |
||||
|
||||
from swift.common.utils import get_logger |
||||
|
||||
|
||||
#: The size of data to read from the form at any given time. |
||||
READ_CHUNK_SIZE = 4096 |
||||
|
||||
#: The maximum size of any attribute's value. Any additional data will be |
||||
#: truncated. |
||||
MAX_VALUE_LENGTH = 4096 |
||||
|
||||
#: Regular expression to match form attributes. |
||||
ATTRIBUTES_RE = re.compile(r'(\w+)=(".*?"|[^";]+)(; ?|$)') |
||||
|
||||
|
||||
class FormInvalid(Exception): |
||||
pass |
||||
|
||||
|
||||
def _parse_attrs(header): |
||||
""" |
||||
Given the value of a header like: |
||||
Content-Disposition: form-data; name="somefile"; filename="test.html" |
||||
|
||||
Return data like |
||||
("form-data", {"name": "somefile", "filename": "test.html"}) |
||||
|
||||
:param header: Value of a header (the part after the ': '). |
||||
:returns: (value name, dict) of the attribute data parsed (see above). |
||||
""" |
||||
attributes = {} |
||||
attrs = '' |
||||
if '; ' in header: |
||||
header, attrs = header.split('; ', 1) |
||||
m = True |
||||
while m: |
||||
m = ATTRIBUTES_RE.match(attrs) |
||||
if m: |
||||
attrs = attrs[len(m.group(0)):] |
||||
attributes[m.group(1)] = m.group(2).strip('"') |
||||
return header, attributes |
||||
|
||||
|
||||
class _IterRequestsFileLikeObject(object): |
||||
|
||||
def __init__(self, wsgi_input, boundary, input_buffer): |
||||
self.no_more_data_for_this_file = False |
||||
self.no_more_files = False |
||||
self.wsgi_input = wsgi_input |
||||
self.boundary = boundary |
||||
self.input_buffer = input_buffer |
||||
|
||||
def read(self, length=None): |
||||
if not length: |
||||
length = READ_CHUNK_SIZE |
||||
if self.no_more_data_for_this_file: |
||||
return '' |
||||
|
||||
# read enough data to know whether we're going to run |
||||
# into a boundary in next [length] bytes |
||||
if len(self.input_buffer) < length + len(self.boundary) + 2: |
||||
to_read = length + len(self.boundary) + 2 |
||||
while to_read > 0: |
||||
chunk = self.wsgi_input.read(to_read) |
||||
to_read -= len(chunk) |
||||
self.input_buffer += chunk |
||||
if not chunk: |
||||
self.no_more_files = True |
||||
break |
||||
|
||||
boundary_pos = self.input_buffer.find(self.boundary) |
||||
|
||||
# boundary does not exist in the next (length) bytes |
||||
if boundary_pos == -1 or boundary_pos > length: |
||||
ret = self.input_buffer[:length] |
||||
self.input_buffer = self.input_buffer[length:] |
||||
# if it does, just return data up to the boundary |
||||
else: |
||||
ret, self.input_buffer = self.input_buffer.split(self.boundary, 1) |
||||
self.no_more_files = self.input_buffer.startswith('--') |
||||
self.no_more_data_for_this_file = True |
||||
self.input_buffer = self.input_buffer[2:] |
||||
return ret |
||||
|
||||
def readline(self): |
||||
if self.no_more_data_for_this_file: |
||||
return '' |
||||
boundary_pos = newline_pos = -1 |
||||
while newline_pos < 0 and boundary_pos < 0: |
||||
chunk = self.wsgi_input.read(READ_CHUNK_SIZE) |
||||
self.input_buffer += chunk |
||||
newline_pos = self.input_buffer.find('\r\n') |
||||
boundary_pos = self.input_buffer.find(self.boundary) |
||||
if not chunk: |
||||
self.no_more_files = True |
||||
break |
||||
# found a newline |
||||
if newline_pos >= 0 and \ |
||||
(boundary_pos < 0 or newline_pos < boundary_pos): |
||||
# Use self.read to ensure any logic there happens... |
||||
ret = '' |
||||
to_read = newline_pos + 2 |
||||
while to_read > 0: |
||||
chunk = self.read(to_read) |
||||
# Should never happen since we're reading from input_buffer, |
||||
# but just for completeness... |
||||
if not chunk: |
||||
break |
||||
to_read -= len(chunk) |
||||
ret += chunk |
||||
return ret |
||||
else: # no newlines, just return up to next boundary |
||||
return self.read(len(self.input_buffer)) |
||||
|
||||
|
||||
def _iter_requests(wsgi_input, boundary): |
||||
""" |
||||
Given a multi-part mime encoded input file object and boundary, |
||||
yield file-like objects for each part. |
||||
|
||||
:param wsgi_input: The file-like object to read from. |
||||
:param boundary: The mime boundary to separate new file-like |
||||
objects on. |
||||
:returns: A generator of file-like objects for each part. |
||||
""" |
||||
boundary = '--' + boundary |
||||
if wsgi_input.readline().strip() != boundary: |
||||
raise FormInvalid('invalid starting boundary') |
||||
boundary = '\r\n' + boundary |
||||
input_buffer = '' |
||||
done = False |
||||
while not done: |
||||
it = _IterRequestsFileLikeObject(wsgi_input, boundary, input_buffer) |
||||
yield it |
||||
done = it.no_more_files |
||||
input_buffer = it.input_buffer |
||||
|
||||
|
||||
class _CappedFileLikeObject(object): |
||||
""" |
||||
A file-like object wrapping another file-like object that raises |
||||
an EOFError if the amount of data read exceeds a given |
||||
max_file_size. |
||||
|
||||
:param fp: The file-like object to wrap. |
||||
:param max_file_size: The maximum bytes to read before raising an |
||||
EOFError. |
||||
""" |
||||
|
||||
def __init__(self, fp, max_file_size): |
||||
self.fp = fp |
||||
self.max_file_size = max_file_size |
||||
self.amount_read = 0 |
||||
|
||||
def read(self, size=None): |
||||
ret = self.fp.read(size) |
||||
self.amount_read += len(ret) |
||||
if self.amount_read > self.max_file_size: |
||||
raise EOFError('max_file_size exceeded') |
||||
return ret |
||||
|
||||
def readline(self): |
||||
ret = self.fp.readline() |
||||
self.amount_read += len(ret) |
||||
if self.amount_read > self.max_file_size: |
||||
raise EOFError('max_file_size exceeded') |
||||
return ret |
||||
|
||||
|
||||
class FormPost(object): |
||||
""" |
||||
FormPost Middleware |
||||
|
||||
See above for a full description. |
||||
|
||||
:param app: The next WSGI filter or app in the paste.deploy |
||||
chain. |
||||
:param conf: The configuration dict for the middleware. |
||||
""" |
||||
|
||||
def __init__(self, app, conf): |
||||
#: The next WSGI application/filter in the paste.deploy pipeline. |
||||
self.app = app |
||||
#: The filter configuration dict. |
||||
self.conf = conf |
||||
#: The logger to use with this middleware. |
||||
self.logger = get_logger(conf, log_route='formpost') |
||||
|
||||
def __call__(self, env, start_response): |
||||
""" |
||||
Main hook into the WSGI paste.deploy filter/app pipeline. |
||||
|
||||
:param env: The WSGI environment dict. |
||||
:param start_response: The WSGI start_response hook. |
||||
:returns: Response as per WSGI. |
||||
""" |
||||
if env['REQUEST_METHOD'] == 'POST': |
||||
try: |
||||
content_type, attrs = \ |
||||
_parse_attrs(env.get('CONTENT_TYPE') or '') |
||||
if content_type == 'multipart/form-data' and \ |
||||
'boundary' in attrs: |
||||
resp_status = [0] |
||||
|
||||
def _start_response(status, headers, exc_info=None): |
||||
resp_status[0] = int(status.split(' ', 1)[0]) |
||||
start_response(status, headers, exc_info) |
||||
|
||||
self._log_request(env, resp_status) |
||||
return self._translate_form(env, start_response, |
||||
attrs['boundary']) |
||||
except (FormInvalid, EOFError), err: |
||||
self._log_request(env, 400) |
||||
body = 'FormPost: %s' % err |
||||
start_response('400 Bad Request', |
||||
(('Content-Type', 'text/plain'), |
||||
('Content-Length', str(len(body))))) |
||||
return [body] |
||||
return self.app(env, start_response) |
||||
|
||||
def _translate_form(self, env, start_response, boundary): |
||||
""" |
||||
Translates the form data into subrequests and issues a |
||||
response. |
||||
|
||||
:param env: The WSGI environment dict. |
||||
:param start_response: The WSGI start_response hook. |
||||
:returns: Response as per WSGI. |
||||
""" |
||||
key = self._get_key(env) |
||||
status = message = '' |
||||
attributes = {} |
||||
file_count = 0 |
||||
for fp in _iter_requests(env['wsgi.input'], boundary): |
||||
hdrs = rfc822.Message(fp, 0) |
||||
disp, attrs = \ |
||||
_parse_attrs(hdrs.getheader('Content-Disposition', '')) |
||||
if disp == 'form-data' and attrs.get('filename'): |
||||
file_count += 1 |
||||
try: |
||||
if file_count > int(attributes.get('max_file_count') or 0): |
||||
status = '400 Bad Request' |
||||
message = 'max file count exceeded' |
||||
break |
||||
except ValueError: |
||||
raise FormInvalid('max_file_count not an integer') |
||||
attributes['filename'] = attrs['filename'] or 'filename' |
||||
if 'content-type' not in attributes and 'content-type' in hdrs: |
||||
attributes['content-type'] = \ |
||||
hdrs['Content-Type'] or 'application/octet-stream' |
||||
status, message = self._perform_subrequest(env, start_response, |
||||
attributes, fp, key) |
||||
if status[:1] != '2': |
||||
break |
||||
else: |
||||
data = '' |
||||
mxln = MAX_VALUE_LENGTH |
||||
while mxln: |
||||
chunk = fp.read(mxln) |
||||
if not chunk: |
||||
break |
||||
mxln -= len(chunk) |
||||
data += chunk |
||||
while fp.read(READ_CHUNK_SIZE): |
||||
pass |
||||
if 'name' in attrs: |
||||
attributes[attrs['name'].lower()] = data.rstrip('\r\n--') |
||||
if not status: |
||||
status = '400 Bad Request' |
||||
message = 'no files to process' |
||||
if not attributes.get('redirect'): |
||||
body = status |
||||
if message: |
||||
body = status + '\r\nFormPost: ' + message.title() |
||||
start_response(status, [('Content-Type', 'text/plain'), |
||||
('Content-Length', len(body))]) |
||||
return [body] |
||||
status = status.split(' ', 1)[0] |
||||
body = '<html><body><p><a href="%s?status=%s&message=%s">Click to ' \ |
||||
'continue...</a></p></body></html>' % \ |
||||
(attributes['redirect'], quote(status), quote(message)) |
||||
start_response('303 See Other', |
||||
[('Location', '%s?status=%s&message=%s' % |
||||
(attributes['redirect'], quote(status), quote(message))), |
||||
('Content-Length', str(len(body)))]) |
||||
return [body] |
||||
|
||||
def _perform_subrequest(self, env, start_response, attributes, fp, key): |
||||
""" |
||||
Performs the subrequest and returns a new response. |
||||
|
||||
:param env: The WSGI environment dict. |
||||
:param start_response: The WSGI start_response hook. |
||||
:param attributes: dict of the attributes of the form so far. |
||||
:param fp: The file-like object containing the request body. |
||||
:param key: The account key to validate the signature with. |
||||
:returns: Response as per WSGI. |
||||
""" |
||||
if not key: |
||||
return '401 Unauthorized', 'invalid signature' |
||||
try: |
||||
max_file_size = int(attributes.get('max_file_size') or 0) |
||||
except ValueError: |
||||
raise FormInvalid('max_file_size not an integer') |
||||
subenv = {'REQUEST_METHOD': 'PUT', |
||||
'SCRIPT_NAME': '', |
||||
'SERVER_NAME': env['SERVER_NAME'], |
||||
'SERVER_PORT': env['SERVER_PORT'], |
||||
'SERVER_PROTOCOL': env['SERVER_PROTOCOL'], |
||||
'HTTP_TRANSFER_ENCODING': 'chunked', |
||||
'wsgi.input': _CappedFileLikeObject(fp, max_file_size), |
||||
'swift.cache': env['swift.cache']} |
||||
subenv['PATH_INFO'] = env['PATH_INFO'] |
||||
if subenv['PATH_INFO'][-1] != '/' and \ |
||||
subenv['PATH_INFO'].count('/') < 4: |
||||
subenv['PATH_INFO'] += '/' |
||||
subenv['PATH_INFO'] += attributes['filename'] or 'filename' |
||||
if 'content-type' in attributes: |
||||
subenv['CONTENT_TYPE'] = \ |
||||
attributes['content-type'] or 'application/octet-stream' |
||||
try: |
||||
if int(attributes.get('expires') or 0) < time(): |
||||
return '401 Unauthorized', 'form expired' |
||||
except ValueError: |
||||
raise FormInvalid('expired not an integer') |
||||
hmac_body = '%s\n%s\n%s\n%s\n%s' % ( |
||||
env['PATH_INFO'], |
||||
attributes.get('redirect') or '', |
||||
attributes.get('max_file_size') or '0', |
||||
attributes.get('max_file_count') or '0', |
||||
attributes.get('expires') or '0' |
||||
) |
||||
sig = hmac.new(key, hmac_body, sha1).hexdigest() |
||||
if sig != (attributes.get('signature') or 'invalid'): |
||||
return '401 Unauthorized', 'invalid signature' |
||||
subenv['swift.authorize'] = lambda req: None |
||||
subenv['swift.authorize_override'] = True |
||||
substatus = [None] |
||||
|
||||
def _start_response(status, headers, exc_info=None): |
||||
substatus[0] = status |
||||
|
||||
self.app(subenv, _start_response) |
||||
return substatus[0], '' |
||||
|
||||
def _get_key(self, env): |
||||
""" |
||||
Returns the X-Account-Meta-Temp-URL-Key header value for the |
||||
account, or None if none is set. |
||||
|
||||
:param env: The WSGI environment for the request. |
||||
:returns: X-Account-Meta-Temp-URL-Key str value, or None. |
||||
""" |
||||
parts = env['PATH_INFO'].split('/', 4) |
||||
if len(parts) < 4 or parts[0] or parts[1] != 'v1' or not parts[2] or \ |
||||
not parts[3]: |
||||
return None |
||||
account = parts[2] |
||||
key = None |
||||
memcache = env.get('swift.cache') |
||||
if memcache: |
||||
key = memcache.get('temp-url-key/%s' % account) |
||||
if not key: |
||||
newenv = {'REQUEST_METHOD': 'HEAD', 'SCRIPT_NAME': '', |
||||
'PATH_INFO': '/v1/' + account, 'CONTENT_LENGTH': '0', |
||||
'SERVER_PROTOCOL': 'HTTP/1.0', |
||||
'HTTP_USER_AGENT': 'FormPost', 'wsgi.version': (1, 0), |
||||
'wsgi.url_scheme': 'http', 'wsgi.input': StringIO('')} |
||||
for name in ('SERVER_NAME', 'SERVER_PORT', 'wsgi.errors', |
||||
'wsgi.multithread', 'wsgi.multiprocess', |
||||
'wsgi.run_once', 'swift.cache', 'swift.trans_id'): |
||||
if name in env: |
||||
newenv[name] = env[name] |
||||
newenv['swift.authorize'] = lambda req: None |
||||
newenv['swift.authorize_override'] = True |
||||
key = [None] |
||||
|
||||
def _start_response(status, response_headers, exc_info=None): |
||||
for h, v in response_headers: |
||||
if h.lower() == 'x-account-meta-temp-url-key': |
||||
key[0] = v |
||||
|
||||
self.app(newenv, _start_response) |
||||
key = key[0] |
||||
if key and memcache: |
||||
memcache.set('temp-url-key/%s' % account, key, timeout=60) |
||||
return key |
||||
|
||||
def _log_request(self, env, response_status_int): |
||||
""" |
||||
Used when a request might not be logged by the underlying |
||||
WSGI application, but we'd still like to record what |
||||
happened. An early 401 Unauthorized is a good example of |
||||
this. |
||||
|
||||
:param env: The WSGI environment for the request. |
||||
:param response_status_int: The HTTP status we'll be replying |
||||
to the request with. |
||||
""" |
||||
the_request = quote(unquote(env.get('PATH_INFO') or '/')) |
||||
if env.get('QUERY_STRING'): |
||||
the_request = the_request + '?' + env['QUERY_STRING'] |
||||
client = env.get('HTTP_X_CLUSTER_CLIENT_IP') |
||||
if not client and 'HTTP_X_FORWARDED_FOR' in env: |
||||
# remote host for other lbs |
||||
client = env['HTTP_X_FORWARDED_FOR'].split(',')[0].strip() |
||||
if not client: |
||||
client = env.get('REMOTE_ADDR') |
||||
self.logger.info(' '.join(quote(str(x)) for x in ( |
||||
client or '-', |
||||
env.get('REMOTE_ADDR') or '-', |
||||
strftime('%d/%b/%Y/%H/%M/%S', gmtime()), |
||||
env.get('REQUEST_METHOD') or 'GET', |
||||
the_request, |
||||
env.get('SERVER_PROTOCOL') or '1.0', |
||||
response_status_int, |
||||
env.get('HTTP_REFERER') or '-', |
||||
(env.get('HTTP_USER_AGENT') or '-') + ' FormPOST', |
||||
env.get('HTTP_X_AUTH_TOKEN') or '-', |
||||
'-', |
||||
'-', |
||||
'-', |
||||
env.get('swift.trans_id') or '-', |
||||
'-', |
||||
'-', |
||||
))) |
||||
|
||||
|
||||
def filter_factory(global_conf, **local_conf): |
||||
""" Returns the WSGI filter for use with paste.deploy. """ |
||||
conf = global_conf.copy() |
||||
conf.update(local_conf) |
||||
return lambda app: FormPost(app, conf) |
@ -0,0 +1,486 @@
|
||||
# Copyright (c) 2010-2011 OpenStack, LLC. |
||||
# |
||||
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||
# you may not use this file except in compliance with the License. |
||||
# You may obtain a copy of the License at |
||||
# |
||||
# http://www.apache.org/licenses/LICENSE-2.0 |
||||
# |
||||
# Unless required by applicable law or agreed to in writing, software |
||||
# distributed under the License is distributed on an "AS IS" BASIS, |
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||
# implied. |
||||
# See the License for the specific language governing permissions and |
||||
# limitations under the License. |
||||
|
||||
""" |
||||
TempURL Middleware |
||||
|
||||
Allows the creation of URLs to provide temporary access to objects. |
||||
|
||||
For example, a website may wish to provide a link to download a large |
||||
object in Swift, but the Swift account has no public access. The |
||||
website can generate a URL that will provide GET access for a limited |
||||
time to the resource. When the web browser user clicks on the link, |
||||
the browser will download the object directly from Swift, obviating |
||||
the need for the website to act as a proxy for the request. |
||||
|
||||
If the user were to share the link with all his friends, or |
||||
accidentally post it on a forum, etc. the direct access would be |
||||
limited to the expiration time set when the website created the link. |
||||
|
||||
To create such temporary URLs, first an X-Account-Meta-Temp-URL-Key |
||||
header must be set on the Swift account. Then, an HMAC-SHA1 (RFC 2104) |
||||
signature is generated using the HTTP method to allow (GET or PUT), |
||||
the Unix timestamp the access should be allowed until, the full path |
||||
to the object, and the key set on the account. |
||||
|
||||
For example, here is code generating the signature for a GET for 60 |
||||
seconds on /v1/AUTH_account/container/object:: |
||||
|
||||
import hmac |
||||
from hashlib import sha1 |
||||
from time import time |
||||
method = 'GET' |
||||
expires = int(time() + 60) |
||||
path = '/v1/AUTH_account/container/object' |
||||
key = 'mykey' |
||||
hmac_body = '%s\\n%s\\n%s\\n%s' % (method, expires, path, key) |
||||
sig = hmac.new(key, hmac_body, sha1).hexdigest() |
||||
|
||||
Let's say the sig ends up equaling ee796f3a89cf82ef7a36ac75fed54b83 |
||||
and expires ends up 1323479485. Then, for example, the website could |
||||
provide a link to:: |
||||
|
||||
https://swift-cluster.example.com/v1/AUTH_account/container/object? |
||||
temp_url_sig=ee796f3a89cf82ef7a36ac75fed54b83&temp_url_expires=1323479485 |
||||
|
||||
Any alteration of the resource path or query arguments would result |
||||
in 401 Unauthorized. Similary, a PUT where GET was the allowed method |
||||
would 401. HEAD is allowed if GET or PUT is allowed. |
||||
|
||||
Using this in combination with browser form post translation |
||||
middleware could also allow direct-from-browser uploads to specific |
||||
locations in Swift. |
||||
|
||||
Note that changing the X-Account-Meta-Temp-URL-Key will invalidate |
||||
any previously generated temporary URLs within 60 seconds (the |
||||
memcache time for the key). |
||||
""" |
||||
|
||||
__all__ = ['TempURL', 'filter_factory', |
||||
'DEFAULT_INCOMING_REMOVE_HEADERS', |
||||
'DEFAULT_INCOMING_ALLOW_HEADERS', |
||||
'DEFAULT_OUTGOING_REMOVE_HEADERS', |
||||
'DEFAULT_OUTGOING_ALLOW_HEADERS'] |
||||
|
||||
|
||||
import hmac |
||||
from hashlib import sha1 |
||||
from os.path import basename |
||||
from StringIO import StringIO |
||||
from time import gmtime, strftime, time |
||||
from urllib import quote, unquote |
||||
from urlparse import parse_qs |
||||
|
||||
from swift.common.utils import get_logger |
||||
|
||||
|
||||
#: Default headers to remove from incoming requests. Simply a whitespace |
||||
#: delimited list of header names and names can optionally end with '*' to |
||||
#: indicate a prefix match. DEFAULT_INCOMING_ALLOW_HEADERS is a list of |
||||
#: exceptions to these removals. |
||||
DEFAULT_INCOMING_REMOVE_HEADERS = 'x-timestamp' |
||||
|
||||
#: Default headers as exceptions to DEFAULT_INCOMING_REMOVE_HEADERS. Simply a |
||||
#: whitespace delimited list of header names and names can optionally end with |
||||
#: '*' to indicate a prefix match. |
||||
DEFAULT_INCOMING_ALLOW_HEADERS = '' |
||||
|
||||
#: Default headers to remove from outgoing responses. Simply a whitespace |
||||
#: delimited list of header names and names can optionally end with '*' to |
||||
#: indicate a prefix match. DEFAULT_OUTGOING_ALLOW_HEADERS is a list of |
||||
#: exceptions to these removals. |
||||
DEFAULT_OUTGOING_REMOVE_HEADERS = 'x-object-meta-*' |
||||
|
||||
#: Default headers as exceptions to DEFAULT_OUTGOING_REMOVE_HEADERS. Simply a |
||||
#: whitespace delimited list of header names and names can optionally end with |
||||
#: '*' to indicate a prefix match. |
||||
DEFAULT_OUTGOING_ALLOW_HEADERS = 'x-object-meta-public-*' |
||||
|
||||
|
||||
class TempURL(object): |
||||
""" |
||||
WSGI Middleware to grant temporary URLs specific access to Swift |
||||
resources. See the overview for more information. |
||||
|
||||
This middleware understands the following configuration settings:: |
||||
|
||||
incoming_remove_headers |
||||
The headers to remove from incoming requests. Simply a |
||||
whitespace delimited list of header names and names can |
||||
optionally end with '*' to indicate a prefix match. |
||||
incoming_allow_headers is a list of exceptions to these |
||||
removals. |
||||
Default: x-timestamp |
||||
|
||||
incoming_allow_headers |
||||
The headers allowed as exceptions to |
||||
incoming_remove_headers. Simply a whitespace delimited |
||||
list of header names and names can optionally end with |
||||
'*' to indicate a prefix match. |
||||
Default: None |
||||
|
||||
outgoing_remove_headers |
||||
The headers to remove from outgoing responses. Simply a |
||||
whitespace delimited list of header names and names can |
||||
optionally end with '*' to indicate a prefix match. |
||||
outgoing_allow_headers is a list of exceptions to these |
||||
removals. |
||||
Default: x-object-meta-* |
||||
|
||||
outgoing_allow_headers |
||||
The headers allowed as exceptions to |
||||
outgoing_remove_headers. Simply a whitespace delimited |
||||
list of header names and names can optionally end with |
||||
'*' to indicate a prefix match. |
||||
Default: x-object-meta-public-* |
||||
|
||||
:param app: The next WSGI filter or app in the paste.deploy |
||||
chain. |
||||
:param conf: The configuration dict for the middleware. |
||||
""" |
||||
|
||||
def __init__(self, app, conf): |
||||
#: The next WSGI application/filter in the paste.deploy pipeline. |
||||
self.app = app |
||||
#: The filter configuration dict. |
||||
self.conf = conf |
||||
#: The logger to use with this middleware. |
||||
self.logger = get_logger(conf, log_route='tempurl') |
||||
|
||||
headers = DEFAULT_INCOMING_REMOVE_HEADERS |
||||
if 'incoming_remove_headers' in conf: |
||||
headers = conf['incoming_remove_headers'] |
||||
headers = \ |
||||
['HTTP_' + h.upper().replace('-', '_') for h in headers.split()] |
||||
#: Headers to remove from incoming requests. Uppercase WSGI env style, |
||||
#: like `HTTP_X_PRIVATE`. |
||||
self.incoming_remove_headers = [h for h in headers if h[-1] != '*'] |
||||
#: Header with match prefixes to remove from incoming requests. |
||||
#: Uppercase WSGI env style, like `HTTP_X_SENSITIVE_*`. |
||||
self.incoming_remove_headers_startswith = \ |
||||
[h[:-1] for h in headers if h[-1] == '*'] |
||||
|
||||
headers = DEFAULT_INCOMING_ALLOW_HEADERS |
||||
if 'incoming_allow_headers' in conf: |
||||
headers = conf['incoming_allow_headers'] |
||||
headers = \ |
||||
['HTTP_' + h.upper().replace('-', '_') for h in headers.split()] |
||||
#: Headers to allow in incoming requests. Uppercase WSGI env style, |
||||
#: like `HTTP_X_MATCHES_REMOVE_PREFIX_BUT_OKAY`. |
||||
self.incoming_allow_headers = [h for h in headers if h[-1] != '*'] |
||||
#: Header with match prefixes to allow in incoming requests. Uppercase |
||||
#: WSGI env style, like `HTTP_X_MATCHES_REMOVE_PREFIX_BUT_OKAY_*`. |
||||
self.incoming_allow_headers_startswith = \ |
||||
[h[:-1] for h in headers if h[-1] == '*'] |
||||
|
||||
headers = DEFAULT_OUTGOING_REMOVE_HEADERS |
||||
if 'outgoing_remove_headers' in conf: |
||||
headers = conf['outgoing_remove_headers'] |
||||
headers = [h.lower() for h in headers.split()] |
||||
#: Headers to remove from outgoing responses. Lowercase, like |
||||
#: `x-account-meta-temp-url-key`. |
||||
self.outgoing_remove_headers = [h for h in headers if h[-1] != '*'] |
||||
#: Header with match prefixes to remove from outgoing responses. |
||||
#: Lowercase, like `x-account-meta-private-*`. |
||||
self.outgoing_remove_headers_startswith = \ |
||||
[h[:-1] for h in headers if h[-1] == '*'] |
||||
|
||||
headers = DEFAULT_OUTGOING_ALLOW_HEADERS |
||||
if 'outgoing_allow_headers' in conf: |
||||
headers = conf['outgoing_allow_headers'] |
||||
headers = [h.lower() for h in headers.split()] |
||||
#: Headers to allow in outgoing responses. Lowercase, like |
||||
#: `x-matches-remove-prefix-but-okay`. |
||||
self.outgoing_allow_headers = [h for h in headers if h[-1] != '*'] |
||||
#: Header with match prefixes to allow in outgoing responses. |
||||
#: Lowercase, like `x-matches-remove-prefix-but-okay-*`. |
||||
self.outgoing_allow_headers_startswith = \ |
||||
[h[:-1] for h in headers if h[-1] == '*'] |
||||
|
||||
def __call__(self, env, start_response): |
||||
""" |
||||
Main hook into the WSGI paste.deploy filter/app pipeline. |
||||
|
||||
:param env: The WSGI environment dict. |
||||
:param start_response: The WSGI start_response hook. |
||||
:returns: Response as per WSGI. |
||||
""" |
||||
temp_url_sig, temp_url_expires = self._get_temp_url_info(env) |
||||
if temp_url_sig is None and temp_url_expires is None: |
||||
return self.app(env, start_response) |
||||
if not temp_url_sig or not temp_url_expires: |
||||
return self._invalid(env, start_response) |
||||
account = self._get_account(env) |
||||
if not account: |
||||
return self._invalid(env, start_response) |
||||
key = self._get_key(env, account) |
||||
if not key: |
||||
return self._invalid(env, start_response) |
||||
if env['REQUEST_METHOD'] == 'HEAD': |
||||
hmac_val = self._get_hmac(env, temp_url_expires, key, |
||||
request_method='GET') |
||||
if temp_url_sig != hmac_val: |
||||
hmac_val = self._get_hmac(env, temp_url_expires, key, |
||||
request_method='PUT') |
||||
if temp_url_sig != hmac_val: |
||||
return self._invalid(env, start_response) |
||||
else: |
||||
hmac_val = self._get_hmac(env, temp_url_expires, key) |
||||
if temp_url_sig != hmac_val: |
||||
return self._invalid(env, start_response) |
||||
self._clean_incoming_headers(env) |
||||
env['swift.authorize'] = lambda req: None |
||||
env['swift.authorize_override'] = True |
||||
|
||||
def _start_response(status, headers, exc_info=None): |
||||
headers = self._clean_outgoing_headers(headers) |
||||
if env['REQUEST_METHOD'] == 'GET': |
||||
already = False |
||||
for h, v in headers: |
||||
if h.lower() == 'content-disposition': |
||||
already = True |
||||
break |
||||
if not already: |
||||
headers.append(('Content-Disposition', |
||||
'attachment; filename=%s' % |
||||
(quote(basename(env['PATH_INFO']))))) |
||||
return start_response(status, headers, exc_info) |
||||
|
||||
return self.app(env, _start_response) |
||||
|
||||
def _get_account(self, env): |
||||
""" |
||||
Returns just the account for the request, if it's an object GET, PUT, |
||||
or HEAD request; otherwise, None is returned. |
||||
|
||||
:param env: The WSGI environment for the request. |
||||
:returns: Account str or None. |
||||
""" |
||||
account = None |
||||
if env['REQUEST_METHOD'] in ('GET', 'PUT', 'HEAD'): |
||||
parts = env['PATH_INFO'].split('/', 4) |
||||
# Must be five parts, ['', 'v1', 'a', 'c', 'o'], must be a v1 |
||||
# request, have account, container, and object values, and the |
||||
# object value can't just have '/'s. |
||||
if len(parts) == 5 and not parts[0] and parts[1] == 'v1' and \ |
||||
parts[2] and parts[3] and parts[4].strip('/'): |
||||
account = parts[2] |
||||
return account |
||||
|
||||
def _get_temp_url_info(self, env): |
||||
""" |
||||
Returns the provided temporary URL parameters (sig, expires), |
||||
if given and syntactically valid. Either sig or expires could |
||||
be None if not provided. If provided, expires is also |
||||
converted to an int if possible or 0 if not, and checked for |
||||
expiration (returns 0 if expired). |
||||
|
||||
:param env: The WSGI environment for the request. |
||||
:returns: (sig, expires) as described above. |
||||
""" |
||||
temp_url_sig = temp_url_expires = None |
||||
qs = parse_qs(env.get('QUERY_STRING', '')) |
||||
if 'temp_url_sig' in qs: |
||||
temp_url_sig = qs['temp_url_sig'][0] |
||||
if 'temp_url_expires' in qs: |
||||
try: |
||||
temp_url_expires = int(qs['temp_url_expires'][0]) |
||||
except ValueError: |
||||
temp_url_expires = 0 |
||||
if temp_url_expires < time(): |
||||
temp_url_expires = 0 |
||||
return temp_url_sig, temp_url_expires |
||||
|
||||
def _get_key(self, env, account): |
||||
""" |
||||
Returns the X-Account-Meta-Temp-URL-Key header value for the |
||||
account, or None if none is set. |
||||
|
||||
:param env: The WSGI environment for the request. |
||||
:param account: Account str. |
||||
:returns: X-Account-Meta-Temp-URL-Key str value, or None. |
||||
""" |
||||
key = None |
||||
memcache = env.get('swift.cache') |
||||
if memcache: |
||||
key = memcache.get('temp-url-key/%s' % account) |
||||
if not key: |
||||
newenv = {'REQUEST_METHOD': 'HEAD', 'SCRIPT_NAME': '', |
||||
'PATH_INFO': '/v1/' + account, 'CONTENT_LENGTH': '0', |
||||
'SERVER_PROTOCOL': 'HTTP/1.0', |
||||
'HTTP_USER_AGENT': 'TempURL', 'wsgi.version': (1, 0), |
||||
'wsgi.url_scheme': 'http', 'wsgi.input': StringIO('')} |
||||
for name in ('SERVER_NAME', 'SERVER_PORT', 'wsgi.errors', |
||||
'wsgi.multithread', 'wsgi.multiprocess', |
||||
'wsgi.run_once', 'swift.cache', 'swift.trans_id'): |
||||
if name in env: |
||||
newenv[name] = env[name] |
||||
newenv['swift.authorize'] = lambda req: None |
||||
newenv['swift.authorize_override'] = True |
||||
key = [None] |
||||
|
||||
def _start_response(status, response_headers, exc_info=None): |
||||
for h, v in response_headers: |
||||
if h.lower() == 'x-account-meta-temp-url-key': |
||||
key[0] = v |
||||
|
||||
self.app(newenv, _start_response) |
||||
key = key[0] |
||||
if key and memcache: |
||||
memcache.set('temp-url-key/%s' % account, key, timeout=60) |
||||
return key |
||||
|
||||
def _get_hmac(self, env, expires, key, request_method=None): |
||||
""" |
||||
Returns the hexdigest string of the HMAC-SHA1 (RFC 2104) for |
||||
the request. |
||||
|
||||
:param env: The WSGI environment for the request. |
||||
:param expires: Unix timestamp as an int for when the URL |
||||
expires. |
||||
:param key: Key str, from the X-Account-Meta-Temp-URL-Key of |
||||
the account. |
||||
:param request_method: Optional override of the request in |
||||
the WSGI env. For example, if a HEAD |
||||
does not match, you may wish to |
||||
override with GET to still allow the |
||||
HEAD. |
||||
:returns: hexdigest str of the HMAC-SHA1 for the request. |
||||
""" |
||||
if not request_method: |
||||
request_method = env['REQUEST_METHOD'] |
||||
return hmac.new(key, '%s\n%s\n%s' % (request_method, expires, |
||||
env['PATH_INFO']), sha1).hexdigest() |
||||
|
||||
def _invalid(self, env, start_response): |
||||
""" |
||||
Performs the necessary steps to indicate a WSGI 401 |
||||
Unauthorized response to the request. |
||||
|
||||
:param env: The WSGI environment for the request. |
||||
:param start_response: The WSGI start_response hook. |
||||
:returns: 401 response as per WSGI. |
||||
""" |
||||
self._log_request(env, 401) |
||||
body = '401 Unauthorized: Temp URL invalid\n' |
||||
start_response('401 Unauthorized', |
||||
[('Content-Type', 'text/plain'), |
||||
('Content-Length', str(len(body)))]) |
||||
if env['REQUEST_METHOD'] == 'HEAD': |
||||
return [] |
||||
return [body] |
||||
|
||||
def _clean_incoming_headers(self, env): |
||||
""" |
||||
Removes any headers from the WSGI environment as per the |
||||
middleware configuration for incoming requests. |
||||
|
||||
:param env: The WSGI environment for the request. |
||||
""" |
||||
for h in env.keys(): |
||||
remove = h in self.incoming_remove_headers |
||||
if not remove: |
||||
for p in self.incoming_remove_headers_startswith: |
||||
if h.startswith(p): |
||||
remove = True |
||||
break |
||||
if remove: |
||||
if h in self.incoming_allow_headers: |
||||
remove = False |
||||
if remove: |
||||
for p in self.incoming_allow_headers_startswith: |
||||
if h.startswith(p): |
||||
remove = False |
||||
break |
||||
if remove: |
||||
del env[h] |
||||
|
||||
def _clean_outgoing_headers(self, headers): |
||||
""" |
||||
Removes any headers as per the middleware configuration for |
||||
outgoing responses. |
||||
|
||||
:param headers: A WSGI start_response style list of headers, |
||||
[('header1', 'value), ('header2', 'value), |
||||
...] |
||||
:returns: The same headers list, but with some headers |
||||
removed as per the middlware configuration for |
||||
outgoing responses. |
||||
""" |
||||
headers = dict(headers) |
||||
for h in headers.keys(): |
||||
remove = h in self.outgoing_remove_headers |
||||
if not remove: |
||||
for p in self.outgoing_remove_headers_startswith: |
||||
if h.startswith(p): |
||||
remove = True |
||||
break |
||||
if remove: |
||||
if h in self.outgoing_allow_headers: |
||||
remove = False |
||||
if remove: |
||||
for p in self.outgoing_allow_headers_startswith: |
||||
if h.startswith(p): |
||||
remove = False |
||||
break |
||||
if remove: |
||||
del headers[h] |
||||
return headers.items() |
||||
|
||||
def _log_request(self, env, response_status_int): |
||||
""" |
||||
Used when a request might not be logged by the underlying |
||||
WSGI application, but we'd still like to record what |
||||
happened. An early 401 Unauthorized is a good example of |
||||
this. |
||||
|
||||
:param env: The WSGI environment for the request. |
||||
:param response_status_int: The HTTP status we'll be replying |
||||
to the request with. |
||||
""" |
||||
the_request = quote(unquote(env.get('PATH_INFO') or '/')) |
||||
if env.get('QUERY_STRING'): |
||||
the_request = the_request + '?' + env['QUERY_STRING'] |
||||
client = env.get('HTTP_X_CLUSTER_CLIENT_IP') |
||||
if not client and 'HTTP_X_FORWARDED_FOR' in env: |
||||
# remote host for other lbs |
||||
client = env['HTTP_X_FORWARDED_FOR'].split(',')[0].strip() |
||||
if not client: |
||||
client = env.get('REMOTE_ADDR') |
||||
self.logger.info(' '.join(quote(str(x)) for x in ( |
||||
client or '-', |
||||
env.get('REMOTE_ADDR') or '-', |
||||
strftime('%d/%b/%Y/%H/%M/%S', gmtime()), |
||||
env.get('REQUEST_METHOD') or 'GET', |
||||
the_request, |
||||
env.get('SERVER_PROTOCOL') or '1.0', |
||||
response_status_int, |
||||
env.get('HTTP_REFERER') or '-', |
||||
(env.get('HTTP_USER_AGENT') or '-') + ' TempURL', |
||||
env.get('HTTP_X_AUTH_TOKEN') or '-', |
||||
'-', |
||||
'-', |
||||
'-', |
||||
env.get('swift.trans_id') or '-', |
||||
'-', |
||||
'-', |
||||
))) |
||||
|
||||
|
||||
def filter_factory(global_conf, **local_conf): |
||||
""" Returns the WSGI filter for use with paste.deploy. """ |
||||
conf = global_conf.copy() |
||||
conf.update(local_conf) |
||||
return lambda app: TempURL(app, conf) |
File diff suppressed because it is too large
Load Diff