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_level = INFO
# set log_headers = false # set log_headers = false
# set log_address = /dev/log # 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 # Note: Put tempurl before dlo, slo and your auth filter(s) in the pipeline
[filter:tempurl] [filter:tempurl]

View File

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

View File

@ -17,6 +17,8 @@ import json
import unittest import unittest
import mock import mock
from six.moves.urllib.parse import urlparse
from swift.common.swob import Request, Response, HTTPUnauthorized from swift.common.swob import Request, Response, HTTPUnauthorized
from swift.common.middleware import staticweb from swift.common.middleware import staticweb
@ -841,5 +843,56 @@ class TestStaticWeb(unittest.TestCase):
self.assertEqual(resp.status_int, 503) # sanity 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__': if __name__ == '__main__':
unittest.main() unittest.main()