Add a configurable URL base to staticweb

This came to light because someone ran Tempest against a standard
installation of RDO, which helpfuly terminates SSL for Swift in
a pre-configured load-balancer. In such a case, staticweb has no
way to know what scheme to use and guesses wrong, causing Tempest
to fail.

Related upstream bug:
 https://bugs.launchpad.net/mos/+bug/1537071

Change-Id: Ie15cf2aff4f7e6bcf68b67ae733c77bb9353587a
Closes-Bug: 1572011
This commit is contained in:
Pete Zaitcev 2016-10-03 21:08:15 -06:00
parent 32bc272634
commit f62df7b80c
3 changed files with 92 additions and 10 deletions

View File

@ -550,6 +550,18 @@ use = egg:swift#staticweb
# set log_level = INFO
# set log_headers = false
# set log_address = /dev/log
#
# At times when it's impossible for staticweb to guess the outside
# endpoint correctly, the url_base may be used to supply the URL
# scheme and/or the host name (and port number) in order to generate
# redirects.
# Example values:
# http://www.example.com - redirect to www.example.com
# https: - changes the schema only
# https:// - same, changes the schema only
# //www.example.com:8080 - redirect www.example.com on port 8080
# (schema unchanged)
# url_base =
# Note: Put tempurl before dlo, slo and your auth filter(s) in the pipeline
[filter:tempurl]

View File

@ -128,7 +128,7 @@ import json
import time
from swift.common.utils import human_readable, split_path, config_true_value, \
quote, register_swift_info, get_logger
quote, register_swift_info, get_logger, urlparse
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, \
@ -154,6 +154,8 @@ class _StaticWebContext(WSGIContext):
self.container = container
self.obj = obj
self.app = staticweb.app
self.url_scheme = staticweb.url_scheme
self.url_host = staticweb.url_host
self.agent = '%(orig)s StaticWeb'
# Results from the last call to self._get_container_info.
self._index = self._error = self._listings = self._listings_css = \
@ -353,6 +355,16 @@ class _StaticWebContext(WSGIContext):
css_path = '../' * prefix.count('/') + quote(self._listings_css)
return css_path
def _redirect_with_slash(self, env_, start_response):
env = {}
env.update(env_)
if self.url_scheme:
env['wsgi.url_scheme'] = self.url_scheme
if self.url_host:
env['HTTP_HOST'] = self.url_host
resp = HTTPMovedPermanently(location=(env['PATH_INFO'] + '/'))
return resp(env, start_response)
def handle_container(self, env, start_response):
"""
Handles a possible static web request for a container.
@ -374,9 +386,7 @@ class _StaticWebContext(WSGIContext):
return HTTPNotFound()(env, start_response)
return self.app(env, start_response)
if not env['PATH_INFO'].endswith('/'):
resp = HTTPMovedPermanently(
location=(env['PATH_INFO'] + '/'))
return resp(env, start_response)
return self._redirect_with_slash(env, start_response)
if not self._index:
return self._listing(env, start_response)
tmp_env = dict(env)
@ -445,9 +455,7 @@ class _StaticWebContext(WSGIContext):
status_int = self._get_status_int()
if is_success(status_int) or is_redirection(status_int):
if not env['PATH_INFO'].endswith('/'):
resp = HTTPMovedPermanently(
location=env['PATH_INFO'] + '/')
return resp(env, start_response)
return self._redirect_with_slash(env, start_response)
start_response(self._response_status, self._response_headers,
self._response_exc_info)
return resp
@ -465,8 +473,7 @@ class _StaticWebContext(WSGIContext):
not json.loads(body):
resp = HTTPNotFound()(env, self._start_response)
return self._error_response(resp, env, start_response)
resp = HTTPMovedPermanently(location=env['PATH_INFO'] + '/')
return resp(env, start_response)
return self._redirect_with_slash(env, start_response)
return self._listing(env, start_response, self.obj)
@ -485,10 +492,20 @@ class StaticWeb(object):
def __init__(self, app, conf):
#: The next WSGI application/filter in the paste.deploy pipeline.
self.app = app
#: The filter configuration dict.
#: The filter configuration dict. Only used in tests.
self.conf = conf
self.logger = get_logger(conf, log_route='staticweb')
# We expose a more general "url_base" parameter in case we want
# to incorporate the path prefix later. Currently it is discarded.
url_base = conf.get('url_base', None)
self.url_scheme = None
self.url_host = None
if url_base:
parsed = urlparse(url_base)
self.url_scheme = parsed.scheme
self.url_host = parsed.netloc
def __call__(self, env, start_response):
"""
Main hook into the WSGI paste.deploy filter/app pipeline.

View File

@ -17,6 +17,8 @@ import json
import unittest
import mock
from six.moves.urllib.parse import urlparse
from swift.common.swob import Request, Response, HTTPUnauthorized
from swift.common.middleware import staticweb
@ -841,5 +843,56 @@ class TestStaticWeb(unittest.TestCase):
self.assertEqual(resp.status_int, 503) # sanity
class TestStaticWebUrlBase(unittest.TestCase):
def setUp(self):
self.app = FakeApp()
self._orig_get_container_info = staticweb.get_container_info
staticweb.get_container_info = mock_get_container_info
def tearDown(self):
staticweb.get_container_info = self._orig_get_container_info
def test_container3subdirz_scheme(self):
path = '/v1/a/c3/subdirz'
scheme = 'https'
test_staticweb = FakeAuthFilter(
staticweb.filter_factory({'url_base': 'https://'})(self.app))
resp = Request.blank(path).get_response(test_staticweb)
self.assertEqual(resp.status_int, 301)
parsed = urlparse(resp.location)
self.assertEqual(parsed.scheme, scheme)
# We omit comparing netloc here, because swob is free to add port.
self.assertEqual(parsed.path, path + '/')
def test_container3subdirz_host(self):
path = '/v1/a/c3/subdirz'
netloc = 'example.com'
test_staticweb = FakeAuthFilter(
staticweb.filter_factory({
'url_base': '//%s' % (netloc,)})(self.app))
resp = Request.blank(path).get_response(test_staticweb)
self.assertEqual(resp.status_int, 301)
parsed = urlparse(resp.location)
# We compare scheme with the default. This may change, but unlikely.
self.assertEqual(parsed.scheme, 'http')
self.assertEqual(parsed.netloc, netloc)
self.assertEqual(parsed.path, path + '/')
def test_container3subdirz_both(self):
path = '/v1/a/c3/subdirz'
scheme = 'http'
netloc = 'example.com'
test_staticweb = FakeAuthFilter(
staticweb.filter_factory({
'url_base': 'http://example.com'})(self.app))
resp = Request.blank(path).get_response(test_staticweb)
self.assertEqual(resp.status_int, 301)
parsed = urlparse(resp.location)
self.assertEqual(parsed.scheme, scheme)
self.assertEqual(parsed.netloc, netloc)
self.assertEqual(parsed.path, path + '/')
if __name__ == '__main__':
unittest.main()