staticweb: Work with prefix-based tempurls

Note that there's a bit of a privilege escalation as prefix-based
tempurls can now be used to perform listings -- but only on containers
with staticweb enabled. Since having staticweb enabled was previously
pretty useless unless the container was both public and
publicly-listable, I think it's probably fine.

This also allows tempurls to be used at the container level, but only
for staticweb responses.

Change-Id: I7949185fdd3b64b882df01d54a8bc158ce2d7032
This commit is contained in:
Tim Burke 2021-09-23 10:31:42 -07:00
parent 486fb23447
commit 8c4e65a6b5
6 changed files with 550 additions and 146 deletions

View File

@ -59,7 +59,8 @@ requests for paths not found.
For pseudo paths that have no <index.name>, this middleware can serve HTML file
listings if you set the ``X-Container-Meta-Web-Listings: true`` metadata item
on the container.
on the container. Note that the listing must be authorized; you may want a
container ACL like ``X-Container-Read: .r:*,.rlistings``.
If listings are enabled, the listings can have a custom style sheet by setting
the X-Container-Meta-Web-Listings-CSS header. For instance, setting
@ -68,6 +69,17 @@ the .../listing.css style sheet. If you "view source" in your browser on a
listing page, you will see the well defined document structure that can be
styled.
Additionally, prefix-based :ref:`tempurl` parameters may be used to authorize
requests instead of making the whole container publicly readable. This gives
clients dynamic discoverability of the objects available within that prefix.
.. note::
``temp_url_prefix`` values should typically end with a slash (``/``) when
used with StaticWeb. StaticWeb's redirects will not carry over any TempURL
parameters, as they likely indicate that the user created an overly-broad
TempURL.
By default, the listings will be rendered with a label of
"Listing of /v1/account/container/path". This can be altered by
setting a ``X-Container-Meta-Web-Listings-Label: <label>``. For example,
@ -137,6 +149,7 @@ from swift.common.wsgi import make_env, WSGIContext
from swift.common.http import is_success, is_redirection, HTTP_NOT_FOUND
from swift.common.swob import Response, HTTPMovedPermanently, HTTPNotFound, \
Request, wsgi_quote, wsgi_to_str, str_to_wsgi
from swift.common.middleware.tempurl import get_temp_url_info
from swift.proxy.controllers.base import get_container_info
@ -225,7 +238,7 @@ class _StaticWebContext(WSGIContext):
self._dir_type = meta.get('web-directory-type', '').strip()
return container_info
def _listing(self, env, start_response, prefix=None):
def _listing(self, env, start_response, prefix=''):
"""
Sends an HTML object listing to the remote client.
@ -284,7 +297,27 @@ class _StaticWebContext(WSGIContext):
if prefix and not listing:
resp = HTTPNotFound()(env, self._start_response)
return self._error_response(resp, env, start_response)
headers = {'Content-Type': 'text/html; charset=UTF-8'}
tempurl_qs = tempurl_prefix = ''
if env.get('REMOTE_USER') == '.wsgi.tempurl':
sig, expires, tempurl_prefix, _filename, inline, ip_range = \
get_temp_url_info(env)
if tempurl_prefix is None:
tempurl_prefix = ''
else:
parts = [
'temp_url_prefix=%s' % quote(tempurl_prefix),
'temp_url_expires=%s' % quote(str(expires)),
'temp_url_sig=%s' % sig,
]
if ip_range:
parts.append('temp_url_ip_range=%s' % quote(ip_range))
if inline:
parts.append('inline')
tempurl_qs = '?' + '&amp;'.join(parts)
headers = {'Content-Type': 'text/html; charset=UTF-8',
'X-Backend-Content-Generator': 'staticweb'}
body = '<!DOCTYPE html>\n' \
'<html>\n' \
' <head>\n' \
@ -309,12 +342,12 @@ class _StaticWebContext(WSGIContext):
' <th class="colsize">Size</th>\n' \
' <th class="coldate">Date</th>\n' \
' </tr>\n' % html_escape(label)
if prefix:
if len(prefix) > len(tempurl_prefix):
body += ' <tr id="parent" class="item">\n' \
' <td class="colname"><a href="../">../</a></td>\n' \
' <td class="colname"><a href="../%s">../</a></td>\n' \
' <td class="colsize">&nbsp;</td>\n' \
' <td class="coldate">&nbsp;</td>\n' \
' </tr>\n'
' </tr>\n' % tempurl_qs
for item in listing:
if 'subdir' in item:
subdir = item['subdir'] if six.PY3 else \
@ -326,7 +359,7 @@ class _StaticWebContext(WSGIContext):
' <td class="colsize">&nbsp;</td>\n' \
' <td class="coldate">&nbsp;</td>\n' \
' </tr>\n' % \
(quote(subdir), html_escape(subdir))
(quote(subdir) + tempurl_qs, html_escape(subdir))
for item in listing:
if 'name' in item:
name = item['name'] if six.PY3 else \
@ -347,7 +380,7 @@ class _StaticWebContext(WSGIContext):
' </tr>\n' % \
(' '.join('type-' + html_escape(t.lower())
for t in content_type.split('/')),
quote(name), html_escape(name),
quote(name) + tempurl_qs, html_escape(name),
bytes, last_modified)
body += ' </table>\n' \
' </body>\n' \
@ -540,8 +573,8 @@ class StaticWeb(object):
return self.app(env, start_response)
if env['REQUEST_METHOD'] not in ('HEAD', 'GET'):
return self.app(env, start_response)
if env.get('REMOTE_USER') and \
not config_true_value(env.get('HTTP_X_WEB_MODE', 'f')):
if env.get('REMOTE_USER') and env['REMOTE_USER'] != '.wsgi.tempurl' \
and not config_true_value(env.get('HTTP_X_WEB_MODE', 'f')):
return self.app(env, start_response)
if not container:
return self.app(env, start_response)

View File

@ -309,13 +309,15 @@ from six.moves.urllib.parse import urlencode
from swift.proxy.controllers.base import get_account_info, get_container_info
from swift.common.header_key_dict import HeaderKeyDict
from swift.common.http import is_success
from swift.common.digest import get_allowed_digests, \
extract_digest_and_algorithm, DEFAULT_ALLOWED_DIGESTS, get_hmac
from swift.common.swob import header_to_environ_key, HTTPUnauthorized, \
HTTPBadRequest, wsgi_to_str
from swift.common.utils import split_path, get_valid_utf8_str, \
streq_const_time, quote, get_logger
streq_const_time, quote, get_logger, close_if_possible
from swift.common.registry import register_swift_info, register_sensitive_param
from swift.common.wsgi import WSGIContext
DISALLOWED_INCOMING_HEADERS = 'x-object-manifest x-symlink-target'
@ -364,6 +366,55 @@ def get_tempurl_keys_from_metadata(meta):
if key.lower() in ('temp-url-key', 'temp-url-key-2')]
def normalize_temp_url_expires(value):
"""
Returns the normalized expiration value as an int
If not None, the value is converted to an int if possible or 0
if not, and checked for expiration (returns 0 if expired).
"""
if value is None:
return value
try:
temp_url_expires = int(value)
except ValueError:
try:
temp_url_expires = timegm(strptime(
value, EXPIRES_ISO8601_FORMAT))
except ValueError:
temp_url_expires = 0
if temp_url_expires < time():
temp_url_expires = 0
return temp_url_expires
def get_temp_url_info(env):
"""
Returns the provided temporary URL parameters (sig, expires, prefix,
temp_url_ip_range), if given and syntactically valid.
Either sig, expires or prefix could be None if not provided.
:param env: The WSGI environment for the request.
:returns: (sig, expires, prefix, filename, inline,
temp_url_ip_range) as described above.
"""
sig = expires = prefix = ip_range = filename = inline = None
qs = parse_qs(env.get('QUERY_STRING', ''), keep_blank_values=True)
if 'temp_url_ip_range' in qs:
ip_range = qs['temp_url_ip_range'][0]
if 'temp_url_sig' in qs:
sig = qs['temp_url_sig'][0]
if 'temp_url_expires' in qs:
expires = qs['temp_url_expires'][0]
if 'temp_url_prefix' in qs:
prefix = qs['temp_url_prefix'][0]
if 'filename' in qs:
filename = qs['filename'][0]
if 'inline' in qs:
inline = True
return (sig, expires, prefix, filename, inline, ip_range)
def disposition_format(disposition_type, filename):
# Content-Disposition in HTTP is defined in
# https://tools.ietf.org/html/rfc6266 and references
@ -495,9 +546,10 @@ class TempURL(object):
"""
if env['REQUEST_METHOD'] == 'OPTIONS':
return self.app(env, start_response)
info = self._get_temp_url_info(env)
temp_url_sig, temp_url_expires, temp_url_prefix, filename, \
info = get_temp_url_info(env)
temp_url_sig, client_temp_url_expires, temp_url_prefix, filename, \
inline_disposition, temp_url_ip_range = info
temp_url_expires = normalize_temp_url_expires(client_temp_url_expires)
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:
@ -511,7 +563,10 @@ class TempURL(object):
if hash_algorithm not in self.allowed_digests:
return self._invalid(env, start_response)
account, container, obj = self._get_path_parts(env)
account, container, obj = self._get_path_parts(
env, allow_container_root=(
env['REQUEST_METHOD'] in ('GET', 'HEAD') and
temp_url_prefix == ""))
if not account:
return self._invalid(env, start_response)
@ -577,116 +632,102 @@ class TempURL(object):
env['swift.authorize_override'] = True
env['REMOTE_USER'] = '.wsgi.tempurl'
qs = {'temp_url_sig': temp_url_sig,
'temp_url_expires': temp_url_expires}
'temp_url_expires': client_temp_url_expires}
if temp_url_prefix is not None:
qs['temp_url_prefix'] = temp_url_prefix
if filename:
qs['filename'] = filename
env['QUERY_STRING'] = urlencode(qs)
def _start_response(status, headers, exc_info=None):
headers = self._clean_outgoing_headers(headers)
if env['REQUEST_METHOD'] in ('GET', 'HEAD') and status[0] == '2':
# figure out the right value for content-disposition
# 1) use the value from the query string
# 2) use the value from the object metadata
# 3) use the object name (default)
out_headers = []
existing_disposition = None
for h, v in headers:
if h.lower() != 'content-disposition':
out_headers.append((h, v))
else:
existing_disposition = v
if inline_disposition:
if filename:
disposition_value = disposition_format('inline',
filename)
else:
disposition_value = 'inline'
elif filename:
disposition_value = disposition_format('attachment',
filename)
elif existing_disposition:
disposition_value = existing_disposition
ctx = WSGIContext(self.app)
app_iter = ctx._app_call(env)
ctx._response_headers = self._clean_outgoing_headers(
ctx._response_headers)
if env['REQUEST_METHOD'] in ('GET', 'HEAD') and \
is_success(ctx._get_status_int()):
# figure out the right value for content-disposition
# 1) use the value from the query string
# 2) use the value from the object metadata
# 3) use the object name (default)
out_headers = []
existing_disposition = None
content_generator = None
for h, v in ctx._response_headers:
if h.lower() == 'x-backend-content-generator':
content_generator = v
if h.lower() != 'content-disposition':
out_headers.append((h, v))
else:
name = basename(wsgi_to_str(env['PATH_INFO']).rstrip('/'))
disposition_value = disposition_format('attachment',
name)
# this is probably just paranoia, I couldn't actually get a
# newline into existing_disposition
value = disposition_value.replace('\n', '%0A')
out_headers.append(('Content-Disposition', value))
existing_disposition = v
if content_generator == 'staticweb':
inline_disposition = True
elif obj == "":
# Generally, tempurl requires an object. We carved out an
# exception to allow GETs at the container root for the sake
# of staticweb, but we can't tell whether we'll have a
# staticweb response or not until after we call the app
close_if_possible(app_iter)
return self._invalid(env, start_response)
# include Expires header for better cache-control
out_headers.append(('Expires', strftime(
"%a, %d %b %Y %H:%M:%S GMT",
gmtime(temp_url_expires))))
headers = out_headers
return start_response(status, headers, exc_info)
if inline_disposition:
if filename:
disposition_value = disposition_format('inline',
filename)
else:
disposition_value = 'inline'
elif filename:
disposition_value = disposition_format('attachment',
filename)
elif existing_disposition:
disposition_value = existing_disposition
else:
name = basename(wsgi_to_str(env['PATH_INFO']).rstrip('/'))
disposition_value = disposition_format('attachment',
name)
# this is probably just paranoia, I couldn't actually get a
# newline into existing_disposition
value = disposition_value.replace('\n', '%0A')
out_headers.append(('Content-Disposition', value))
return self.app(env, _start_response)
# include Expires header for better cache-control
out_headers.append(('Expires', strftime(
"%a, %d %b %Y %H:%M:%S GMT",
gmtime(temp_url_expires))))
ctx._response_headers = out_headers
start_response(
ctx._response_status,
ctx._response_headers,
ctx._response_exc_info)
return app_iter
def _get_path_parts(self, env):
def _get_path_parts(self, env, allow_container_root=False):
"""
Return the account, container and object name for the request,
if it's an object request and one of the configured methods;
otherwise, None is returned.
If it's a container request and allow_root_container is true,
the object name returned will be the empty string.
:param env: The WSGI environment for the request.
:param allow_container_root: Whether requests to the root of a
container should be allowed.
:returns: (Account str, container str, object str) or
(None, None, None).
"""
if env['REQUEST_METHOD'] in self.conf['methods']:
try:
ver, acc, cont, obj = split_path(env['PATH_INFO'], 4, 4, True)
ver, acc, cont, obj = split_path(
env['PATH_INFO'], 3 if allow_container_root else 4,
4, True)
except ValueError:
return (None, None, None)
if ver == 'v1' and obj.strip('/'):
return (wsgi_to_str(acc), wsgi_to_str(cont), wsgi_to_str(obj))
if ver == 'v1' and (allow_container_root or obj.strip('/')):
return (wsgi_to_str(acc), wsgi_to_str(cont),
wsgi_to_str(obj) if obj else '')
return (None, None, None)
def _get_temp_url_info(self, env):
"""
Returns the provided temporary URL parameters (sig, expires, prefix,
temp_url_ip_range), if given and syntactically valid.
Either sig, expires or prefix 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, prefix, filename, inline,
temp_url_ip_range) as described above.
"""
temp_url_sig = temp_url_expires = temp_url_prefix = filename =\
inline = None
temp_url_ip_range = None
qs = parse_qs(env.get('QUERY_STRING', ''), keep_blank_values=True)
if 'temp_url_ip_range' in qs:
temp_url_ip_range = qs['temp_url_ip_range'][0]
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:
try:
temp_url_expires = timegm(strptime(
qs['temp_url_expires'][0],
EXPIRES_ISO8601_FORMAT))
except ValueError:
temp_url_expires = 0
if temp_url_expires < time():
temp_url_expires = 0
if 'temp_url_prefix' in qs:
temp_url_prefix = qs['temp_url_prefix'][0]
if 'filename' in qs:
filename = qs['filename'][0]
if 'inline' in qs:
inline = True
return (temp_url_sig, temp_url_expires, temp_url_prefix, filename,
inline, temp_url_ip_range)
def _get_keys(self, env):
"""
Returns the X-[Account|Container]-Meta-Temp-URL-Key[-2] header values

View File

@ -15,13 +15,17 @@
# limitations under the License.
import functools
import hashlib
import six
import time
from unittest import SkipTest
from six.moves.urllib.parse import unquote
from swift.common.middleware import tempurl
from swift.common.utils import quote
from swift.common.swob import str_to_wsgi
import test.functional as tf
from test.functional.tests import Utils, Base, Base2, BaseEnv
from test.functional.test_tempurl import tempurl_parms
from test.functional.swift_test_client import Account, Connection, \
ResponseError
@ -424,3 +428,213 @@ class TestStaticWebUTF8(Base2, TestStaticWeb):
def test_redirect_slash_anon_remap_cont(self):
self.skipTest("Can't remap UTF8 containers")
class TestStaticWebTempurlEnv(BaseEnv):
static_web_enabled = None # tri-state: None initially, then True/False
tempurl_enabled = None # tri-state: None initially, then True/False
@classmethod
def setUp(cls):
cls.conn = Connection(tf.config)
cls.conn.authenticate()
if cls.static_web_enabled is None:
cls.static_web_enabled = 'staticweb' in tf.cluster_info
if not cls.static_web_enabled:
return
if cls.tempurl_enabled is None:
cls.tempurl_enabled = 'tempurl' in tf.cluster_info
if not cls.tempurl_enabled:
return
cls.account = Account(
cls.conn, tf.config.get('account', tf.config['username']))
cls.account.delete_containers()
cls.container = cls.account.container(Utils.create_name())
cls.tempurl_key = Utils.create_name()
if not cls.container.create(
hdrs={'X-Container-Meta-Web-Listings': 'true',
'X-Container-Meta-Temp-URL-Key': cls.tempurl_key}):
raise ResponseError(cls.conn.response)
objects = ['index',
'error',
'listings_css',
'dir/',
'dir/obj',
'dir/subdir/',
'dir/subdir/obj']
cls.objects = {}
for item in sorted(objects):
if '/' in item.rstrip('/'):
parent, _ = item.rstrip('/').rsplit('/', 1)
path = '%s/%s' % (cls.objects[parent + '/'].name,
Utils.create_name())
else:
path = Utils.create_name()
if item[-1] == '/':
cls.objects[item] = cls.container.file(path)
cls.objects[item].write(hdrs={
'Content-Type': 'application/directory'})
else:
cls.objects[item] = cls.container.file(path)
cls.objects[item].write(('%s contents' % item).encode('utf8'))
class TestStaticWebTempurl(Base):
env = TestStaticWebTempurlEnv
set_up = False
def setUp(self):
super(TestStaticWebTempurl, self).setUp()
if self.env.static_web_enabled is False:
raise SkipTest("Static Web not enabled")
elif self.env.static_web_enabled is not True:
# just some sanity checking
raise Exception(
"Expected static_web_enabled to be True/False, got %r" %
(self.env.static_web_enabled,))
if self.env.tempurl_enabled is False:
raise SkipTest("Temp URL not enabled")
elif self.env.tempurl_enabled is not True:
# just some sanity checking
raise Exception(
"Expected tempurl_enabled to be True/False, got %r" %
(self.env.tempurl_enabled,))
self.whole_container_parms = dict(tempurl_parms(
'GET', int(time.time() + 60),
'prefix:%s' % self.env.conn.make_path(
self.env.container.path + ['']),
self.env.tempurl_key, hashlib.sha256,
), temp_url_prefix='')
def link(self, virtual_name, parms=None):
name = self.env.objects[virtual_name].name.rsplit('/', 1)[-1]
if parms is None:
parms = self.whole_container_parms
return (
'<a href="%s?temp_url_prefix=%s&amp;temp_url_expires=%s&amp;'
'temp_url_sig=%s">%s</a>' % (
name,
parms['temp_url_prefix'],
quote(str(parms['temp_url_expires'])),
parms['temp_url_sig'],
name))
def test_unauthed(self):
status = self.env.conn.make_request(
'GET', self.env.container.path, cfg={'no_auth_token': True})
self.assertEqual(status, 401)
def test_staticweb_off(self):
self.env.container.update_metadata(
{'X-Remove-Container-Meta-Web-Listings': 'true'})
status = self.env.conn.make_request(
'GET', self.env.container.path, parms=self.whole_container_parms,
cfg={'no_auth_token': True})
self.assertEqual(status, 401, self.env.conn.response.read())
status = self.env.conn.make_request(
'GET', self.env.container.path + [''],
parms=self.whole_container_parms,
cfg={'no_auth_token': True})
self.assertEqual(status, 401)
status = self.env.conn.make_request(
'GET',
self.env.container.path + [self.env.objects['dir/'].name, ''],
parms=self.whole_container_parms,
cfg={'no_auth_token': True})
self.assertEqual(status, 404)
def test_get_root(self):
status = self.env.conn.make_request(
'GET', self.env.container.path, parms=self.whole_container_parms,
cfg={'no_auth_token': True})
self.assertEqual(status, 301)
status = self.env.conn.make_request(
'GET', self.env.container.path + [''],
parms=self.whole_container_parms,
cfg={'no_auth_token': True})
self.assertEqual(status, 200)
body = self.env.conn.response.read()
if not six.PY2:
body = body.decode('utf-8')
self.assertIn('Listing of /v1/', body)
self.assertNotIn('href="..', body)
self.assertIn(self.link('dir/'), body)
def test_get_dir(self):
status = self.env.conn.make_request(
'GET',
self.env.container.path + [self.env.objects['dir/'].name, ''],
parms=self.whole_container_parms,
cfg={'no_auth_token': True})
self.assertEqual(status, 200)
body = self.env.conn.response.read()
if not six.PY2:
body = body.decode('utf-8')
self.assertIn('Listing of /v1/', body)
self.assertIn('href="..', body)
self.assertIn(self.link('dir/obj'), body)
self.assertIn(self.link('dir/subdir/'), body)
def test_get_dir_with_iso_expiry(self):
iso_expiry = time.strftime(
tempurl.EXPIRES_ISO8601_FORMAT,
time.gmtime(int(self.whole_container_parms['temp_url_expires'])))
iso_parms = dict(self.whole_container_parms,
temp_url_expires=iso_expiry)
status = self.env.conn.make_request(
'GET',
self.env.container.path + [self.env.objects['dir/'].name, ''],
parms=iso_parms,
cfg={'no_auth_token': True})
self.assertEqual(status, 200)
body = self.env.conn.response.read()
if not six.PY2:
body = body.decode('utf-8')
self.assertIn('Listing of /v1/', body)
self.assertIn('href="..', body)
self.assertIn(self.link('dir/obj', iso_parms), body)
self.assertIn(self.link('dir/subdir/', iso_parms), body)
def test_get_limited_dir(self):
parms = dict(tempurl_parms(
'GET', int(time.time() + 60),
'prefix:%s' % self.env.conn.make_path(
self.env.container.path + [self.env.objects['dir/'].name, '']),
self.env.tempurl_key, hashlib.sha256,
), temp_url_prefix=self.env.objects['dir/'].name + '/')
status = self.env.conn.make_request(
'GET',
self.env.container.path + [self.env.objects['dir/'].name, ''],
parms=parms, cfg={'no_auth_token': True})
self.assertEqual(status, 200)
body = self.env.conn.response.read()
if not six.PY2:
body = body.decode('utf-8')
self.assertIn('Listing of /v1/', body)
self.assertNotIn('href="..', body)
self.assertIn(self.link('dir/obj', parms), body)
self.assertIn(self.link('dir/subdir/', parms), body)
status = self.env.conn.make_request(
'GET', self.env.container.path + [
self.env.objects['dir/subdir/'].name, ''],
parms=parms, cfg={'no_auth_token': True})
self.assertEqual(status, 200)
body = self.env.conn.response.read()
if not six.PY2:
body = body.decode('utf-8')
self.assertIn('Listing of /v1/', body)
self.assertIn('href="..', body)
self.assertIn(self.link('dir/subdir/obj', parms), body)

View File

@ -34,6 +34,19 @@ from test.functional.swift_test_client import Account, Connection, \
ResponseError
def tempurl_parms(method, expires, path, key, digest=None):
path = urllib.parse.unquote(path)
if not six.PY2:
method = method.encode('utf8')
path = path.encode('utf8')
key = key.encode('utf8')
sig = hmac.new(
key,
b'%s\n%d\n%s' % (method, expires, path),
digest or hashlib.sha256).hexdigest()
return {'temp_url_sig': sig, 'temp_url_expires': str(expires)}
def setUpModule():
tf.setup_package()
@ -119,16 +132,7 @@ class TestTempurl(Base):
self.env.tempurl_key)
def tempurl_parms(self, method, expires, path, key):
path = urllib.parse.unquote(path)
if not six.PY2:
method = method.encode('utf8')
path = path.encode('utf8')
key = key.encode('utf8')
sig = hmac.new(
key,
b'%s\n%d\n%s' % (method, expires, path),
self.digest).hexdigest()
return {'temp_url_sig': sig, 'temp_url_expires': str(expires)}
return tempurl_parms(method, expires, path, key, self.digest)
def test_GET(self):
for e in (str(self.expires), self.expires_8601):
@ -350,17 +354,12 @@ class TestTempurlPrefix(TestTempurl):
else:
prefix = path_parts[4][:4]
prefix_to_hash = '/'.join(path_parts[0:4]) + '/' + prefix
if not six.PY2:
method = method.encode('utf8')
prefix_to_hash = prefix_to_hash.encode('utf8')
key = key.encode('utf8')
sig = hmac.new(
key,
b'%s\n%d\nprefix:%s' % (method, expires, prefix_to_hash),
self.digest).hexdigest()
return {
'temp_url_sig': sig, 'temp_url_expires': str(expires),
'temp_url_prefix': prefix}
parms = tempurl_parms(
method, expires,
'prefix:' + prefix_to_hash,
key, self.digest)
parms['temp_url_prefix'] = prefix
return parms
def test_empty_prefix(self):
parms = self.tempurl_parms(

View File

@ -539,6 +539,7 @@ class TestStaticWeb(unittest.TestCase):
resp = Request.blank('/v1/a/c3/').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200)
self.assertIn(b'Test main index.html file.', resp.body)
self.assertNotIn('X-Backend-Content-Generator', resp.headers)
def test_container3subsubdir(self):
resp = Request.blank(
@ -591,21 +592,86 @@ class TestStaticWeb(unittest.TestCase):
self.assertEqual(resp.status_int, 200)
self.assertIn(b'Listing of /v1/a/c4/', resp.body)
self.assertIn(b'href="listing.css"', resp.body)
self.assertIn('X-Backend-Content-Generator', resp.headers)
self.assertEqual(resp.headers['X-Backend-Content-Generator'],
'staticweb')
def test_container4indexhtmlauthed(self):
# anonymous access gets staticweb
resp = Request.blank('/v1/a/c4').get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 301)
# authed access doesn't (by default)
resp = Request.blank(
'/v1/a/c4',
environ={'REMOTE_USER': 'authed'}).get_response(
self.test_staticweb)
self.assertEqual(resp.status_int, 200)
# it can opt-in, though!
resp = Request.blank(
'/v1/a/c4', headers={'x-web-mode': 't'},
environ={'REMOTE_USER': 'authed'}).get_response(
self.test_staticweb)
self.assertEqual(resp.status_int, 301)
# and there's an exclusion for authed-via-tempurl
resp = Request.blank(
'/v1/a/c4',
environ={'REMOTE_USER': '.wsgi.tempurl'}).get_response(
self.test_staticweb)
self.assertEqual(resp.status_int, 301)
def test_container4tempurl(self):
parts = [
'temp_url_prefix=subdir/',
'temp_url_sig=the-sig',
'temp_url_expires=2024-12-31T00:00:00'
]
resp = Request.blank(
'/v1/a/c4/subdir/?' + '&'.join(parts),
environ={'REMOTE_USER': '.wsgi.tempurl'},
).get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200)
self.assertIn(b'Listing of /v1/a/c4/subdir/', resp.body)
self.assertIn(b'<a href="2.txt?temp_url_prefix=subdir/&amp;'
b'temp_url_expires=2024-12-31T00%3A00%3A00&amp;'
b'temp_url_sig=the-sig">2.txt</a>', resp.body)
parts.append('temp_url_ip_range=127.0.0.1')
resp = Request.blank(
'/v1/a/c4/subdir/?' + '&'.join(parts),
environ={'REMOTE_USER': '.wsgi.tempurl'},
).get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200)
self.assertIn(b'Listing of /v1/a/c4/subdir/', resp.body)
self.assertIn(b'<a href="2.txt?temp_url_prefix=subdir/&amp;'
b'temp_url_expires=2024-12-31T00%3A00%3A00&amp;'
b'temp_url_sig=the-sig&amp;temp_url_ip_range='
b'127.0.0.1">2.txt</a>', resp.body)
parts.append('inline')
resp = Request.blank(
'/v1/a/c4/subdir/?' + '&'.join(parts),
environ={'REMOTE_USER': '.wsgi.tempurl'},
).get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200)
self.assertIn(b'Listing of /v1/a/c4/subdir/', resp.body)
self.assertIn(b'<a href="2.txt?temp_url_prefix=subdir/&amp;'
b'temp_url_expires=2024-12-31T00%3A00%3A00&amp;'
b'temp_url_sig=the-sig&amp;temp_url_ip_range='
b'127.0.0.1&amp;inline">2.txt</a>', resp.body)
# no prefix => you get normal links (which will almost certainly 401)
resp = Request.blank(
'/v1/a/c4/subdir/?' + '&'.join(parts[1:]),
environ={'REMOTE_USER': '.wsgi.tempurl'},
).get_response(self.test_staticweb)
self.assertEqual(resp.status_int, 200)
self.assertIn(b'Listing of /v1/a/c4/subdir/', resp.body)
self.assertIn(b'<a href="2.txt">2.txt</a>', resp.body)
def test_container4unknown(self):
resp = Request.blank(
'/v1/a/c4/unknown').get_response(self.test_staticweb)

View File

@ -369,6 +369,30 @@ class TestTempURL(unittest.TestCase):
sig = hmac.new(key, hmac_body, hashlib.sha256).hexdigest()
self.assert_valid_sig(expires, query_path, [key], sig, prefix=prefix)
def test_get_valid_with_prefix_and_staticweb(self):
method = 'GET'
expires = int(time() + 86400)
prefix = 'p1/p2/'
sig_path = 'prefix:/v1/a/c/' + prefix
query_path = '/v1/a/c/' + prefix + 'o'
key = b'abc'
hmac_body = ('%s\n%i\n%s' %
(method, expires, sig_path)).encode('utf-8')
sig = hmac.new(key, hmac_body, hashlib.sha512).hexdigest()
req = self._make_request(query_path, keys=[key], environ={
'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&'
'temp_url_prefix=%s' % (sig, expires, prefix)})
self.tempurl.app = FakeApp(iter([('200 Ok', {
'X-Backend-Content-Generator': 'staticweb'}, b'123')]))
resp = req.get_response(self.tempurl)
self.assertEqual(resp.status_int, 200)
# This is the key thing: if the response came from staticweb, assume
# the client is a browser and doesn't want a download prompt
self.assertEqual(resp.headers['content-disposition'], 'inline')
self.assertIn('expires', resp.headers)
self.assertEqual(req.environ['swift.authorize_override'], True)
self.assertEqual(req.environ['REMOTE_USER'], '.wsgi.tempurl')
def test_get_valid_with_prefix_empty(self):
method = 'GET'
expires = int(time() + 86400)
@ -1198,6 +1222,16 @@ class TestTempURL(unittest.TestCase):
self.assertEqual(self.tempurl._get_path_parts({
'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/'}),
(None, None, None))
self.assertEqual(
self.tempurl._get_path_parts(
{'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/'},
allow_container_root=True),
('a', 'c', ''))
self.assertEqual(
self.tempurl._get_path_parts(
{'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/'},
allow_container_root=False),
(None, None, None))
self.assertEqual(self.tempurl._get_path_parts({
'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c//////'}),
(None, None, None))
@ -1224,71 +1258,88 @@ class TestTempURL(unittest.TestCase):
s = 'f5d5051bddf5df7e27c628818738334f'
e_ts = int(time() + 86400)
e_8601 = strftime(tempurl.EXPIRES_ISO8601_FORMAT, gmtime(e_ts))
for e in (e_ts, e_8601):
for e in (str(e_ts), e_8601):
self.assertEqual(
self.tempurl._get_temp_url_info(
tempurl.get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (
s, e)}),
(s, e_ts, None, None, None, None))
(s, e, None, None, None, None))
self.assertEqual(
self.tempurl._get_temp_url_info(
tempurl.get_temp_url_info(
{'QUERY_STRING':
'temp_url_sig=%s&temp_url_expires=%s&temp_url_prefix=%s'
% (s, e, 'prefix')}),
(s, e_ts, 'prefix', None, None, None))
(s, e, 'prefix', None, None, None))
self.assertEqual(
self.tempurl._get_temp_url_info(
tempurl.get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&'
'filename=bobisyouruncle' % (s, e)}),
(s, e_ts, None, 'bobisyouruncle', None, None))
(s, e, None, 'bobisyouruncle', None, None))
self.assertEqual(
self.tempurl._get_temp_url_info({}),
tempurl.get_temp_url_info({}),
(None, None, None, None, None, None))
self.assertEqual(
self.tempurl._get_temp_url_info(
tempurl.get_temp_url_info(
{'QUERY_STRING': 'temp_url_expires=%s' % e}),
(None, e_ts, None, None, None, None))
(None, e, None, None, None, None))
self.assertEqual(
self.tempurl._get_temp_url_info(
tempurl.get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s' % s}),
(s, None, None, None, None, None))
self.assertEqual(
self.tempurl._get_temp_url_info(
tempurl.get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=bad' % (
s)}),
(s, 0, None, None, None, None))
(s, 'bad', None, None, None, None))
self.assertEqual(
self.tempurl._get_temp_url_info(
tempurl.get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&'
'inline=' % (s, e)}),
(s, e_ts, None, None, True, None))
(s, e, None, None, True, None))
self.assertEqual(
self.tempurl._get_temp_url_info(
tempurl.get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&'
'filename=bobisyouruncle&inline=' % (s, e)}),
(s, e_ts, None, 'bobisyouruncle', True, None))
(s, e, None, 'bobisyouruncle', True, None))
self.assertEqual(
self.tempurl._get_temp_url_info(
tempurl.get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&'
'filename=bobisyouruncle&inline='
'&temp_url_ip_range=127.0.0.1' % (s, e)}),
(s, e_ts, None, 'bobisyouruncle', True, '127.0.0.1'))
(s, e, None, 'bobisyouruncle', True, '127.0.0.1'))
e_ts = int(time() - 1)
e_8601 = strftime(tempurl.EXPIRES_ISO8601_FORMAT, gmtime(e_ts))
for e in (e_ts, e_8601):
for e in (str(e_ts), e_8601):
self.assertEqual(
self.tempurl._get_temp_url_info(
tempurl.get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (
s, e)}),
(s, 0, None, None, None, None))
# Offsets not supported (yet?).
(s, e, None, None, None, None))
e_8601 = strftime('%Y-%m-%dT%H:%M:%S+0000', gmtime(e_ts))
self.assertEqual(
self.tempurl._get_temp_url_info(
tempurl.get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (
s, e_8601)}),
(s, 0, None, None, None, None))
s, e_8601.replace('+', '%2B'))}),
(s, e_8601, None, None, None, None))
def test_normalize_temp_url_expires(self):
e_ts = int(time() + 86400)
self.assertEqual(e_ts, tempurl.normalize_temp_url_expires(e_ts))
self.assertEqual(e_ts, tempurl.normalize_temp_url_expires(str(e_ts)))
e_8601 = strftime(tempurl.EXPIRES_ISO8601_FORMAT, gmtime(e_ts))
self.assertEqual(e_ts, tempurl.normalize_temp_url_expires(e_8601))
# Offsets not supported (yet?).
e_8601 = strftime('%Y-%m-%dT%H:%M:%S+0000', gmtime(e_ts))
self.assertEqual(0, tempurl.normalize_temp_url_expires(e_8601))
self.assertEqual(None, tempurl.normalize_temp_url_expires(None))
self.assertEqual(0, tempurl.normalize_temp_url_expires('bad'))
e_ts = int(time() - 1)
self.assertEqual(0, tempurl.normalize_temp_url_expires(e_ts))
e_8601 = strftime(tempurl.EXPIRES_ISO8601_FORMAT, gmtime(e_ts))
self.assertEqual(0, tempurl.normalize_temp_url_expires(e_8601))
def test_get_hmacs(self):
self.assertEqual(