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:
parent
486fb23447
commit
8c4e65a6b5
@ -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 = '?' + '&'.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"> </td>\n' \
|
||||
' <td class="coldate"> </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"> </td>\n' \
|
||||
' <td class="coldate"> </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)
|
||||
|
@ -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
|
||||
|
@ -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&temp_url_expires=%s&'
|
||||
'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)
|
||||
|
@ -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(
|
||||
|
@ -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/&'
|
||||
b'temp_url_expires=2024-12-31T00%3A00%3A00&'
|
||||
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/&'
|
||||
b'temp_url_expires=2024-12-31T00%3A00%3A00&'
|
||||
b'temp_url_sig=the-sig&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/&'
|
||||
b'temp_url_expires=2024-12-31T00%3A00%3A00&'
|
||||
b'temp_url_sig=the-sig&temp_url_ip_range='
|
||||
b'127.0.0.1&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)
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user