diff --git a/etc/proxy-server.conf-sample b/etc/proxy-server.conf-sample index aef2aec700..47e7150b53 100644 --- a/etc/proxy-server.conf-sample +++ b/etc/proxy-server.conf-sample @@ -76,3 +76,8 @@ use = egg:swift#ratelimit # container_ratelimit_0 = 100 # container_ratelimit_10 = 50 # container_ratelimit_50 = 20 + +[filter:domain_remap] +use = egg:swift#domain_remap +# storage_domain = example.com +# path_root = v1 diff --git a/setup.py b/setup.py index 66f4956cc6..90936ebff5 100644 --- a/setup.py +++ b/setup.py @@ -93,6 +93,7 @@ setup( 'healthcheck=swift.common.middleware.healthcheck:filter_factory', 'memcache=swift.common.middleware.memcache:filter_factory', 'ratelimit=swift.common.middleware.ratelimit:filter_factory', + 'domain_remap=swift.common.middleware.domain_remap:filter_factory', ], }, ) diff --git a/swift/common/middleware/domain_remap.py b/swift/common/middleware/domain_remap.py new file mode 100644 index 0000000000..e55394d84b --- /dev/null +++ b/swift/common/middleware/domain_remap.py @@ -0,0 +1,80 @@ +# Copyright (c) 2010 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. + +from webob import Request +from webob.exc import HTTPBadRequest + + +class DomainRemapMiddleware(object): + """ + Middleware that translates container and account parts of a domain to + path parameters that the proxy server understands. + + container.account.storageurl/object gets translated to + container.account.storageurl/path_root/account/container/object + + account.storageurl/path_root/container/object gets translated to + account.storageurl/path_root/account/container/object + """ + + def __init__(self, app, conf): + self.app = app + self.storage_domain = conf.get('storage_domain', 'example.com') + if self.storage_domain and self.storage_domain[0] != '.': + self.storage_domain = '.' + self.storage_domain + self.path_root = conf.get('path_root', 'v1').strip('/') + + def __call__(self, env, start_response): + if not self.storage_domain: + return self.app(env, start_response) + given_domain = env['HTTP_HOST'] + port = '' + if ':' in given_domain: + given_domain, port = given_domain.rsplit(':', 1) + if given_domain.endswith(self.storage_domain): + parts_to_parse = given_domain[:-len(self.storage_domain)] + parts_to_parse = parts_to_parse.strip('.').split('.') + len_parts_to_parse = len(parts_to_parse) + if len_parts_to_parse == 2: + container, account = parts_to_parse + elif len_parts_to_parse == 1: + container, account = None, parts_to_parse[0] + else: + resp = HTTPBadRequest(request=Request(env), + body='Bad domain in host header', + content_type='text/plain') + return resp(env, start_response) + if '_' not in account and '-' in account: + account = account.replace('-', '_', 1) + path = env['PATH_INFO'].strip('/') + new_path_parts = ['', self.path_root, account] + if container: + new_path_parts.append(container) + if path.startswith(self.path_root): + path = path[len(self.path_root):].lstrip('/') + if path: + new_path_parts.append(path) + new_path = '/'.join(new_path_parts) + env['PATH_INFO'] = new_path + return self.app(env, start_response) + + +def filter_factory(global_conf, **local_conf): + conf = global_conf.copy() + conf.update(local_conf) + + def domain_filter(app): + return DomainRemapMiddleware(app, conf) + return domain_filter diff --git a/swift/common/middleware/memcache.py b/swift/common/middleware/memcache.py index 62cd1bdf03..0eabb0fb68 100644 --- a/swift/common/middleware/memcache.py +++ b/swift/common/middleware/memcache.py @@ -1,3 +1,4 @@ +# Copyright (c) 2010 OpenStack, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/test/unit/common/middleware/test_domain_remap.py b/test/unit/common/middleware/test_domain_remap.py new file mode 100644 index 0000000000..a6beb561fb --- /dev/null +++ b/test/unit/common/middleware/test_domain_remap.py @@ -0,0 +1,110 @@ +# Copyright (c) 2010 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. + +import unittest + +from webob import Request + +from swift.common.middleware import domain_remap + + +class FakeApp(object): + + def __call__(self, env, start_response): + return env['PATH_INFO'] + + +def start_response(*args): + pass + + +class TestDomainRemap(unittest.TestCase): + + def setUp(self): + self.app = domain_remap.DomainRemapMiddleware(FakeApp(), {}) + + def test_domain_remap_passthrough(self): + req = Request.blank('/', environ={'REQUEST_METHOD': 'GET'}, + headers={'Host': 'example.com'}) + resp = self.app(req.environ, start_response) + self.assertEquals(resp, '/') + req = Request.blank('/', environ={'REQUEST_METHOD': 'GET'}, + headers={'Host': 'example.com:8080'}) + resp = self.app(req.environ, start_response) + self.assertEquals(resp, '/') + + def test_domain_remap_account(self): + req = Request.blank('/', environ={'REQUEST_METHOD': 'GET'}, + headers={'Host': 'a.example.com'}) + resp = self.app(req.environ, start_response) + self.assertEquals(resp, '/v1/a') + req = Request.blank('/', environ={'REQUEST_METHOD': 'GET'}, + headers={'Host': 'a-uuid.example.com'}) + resp = self.app(req.environ, start_response) + self.assertEquals(resp, '/v1/a_uuid') + + def test_domain_remap_account_container(self): + req = Request.blank('/', environ={'REQUEST_METHOD': 'GET'}, + headers={'Host': 'c.a.example.com'}) + resp = self.app(req.environ, start_response) + self.assertEquals(resp, '/v1/a/c') + + def test_domain_remap_extra_subdomains(self): + req = Request.blank('/', environ={'REQUEST_METHOD': 'GET'}, + headers={'Host': 'x.y.c.a.example.com'}) + resp = self.app(req.environ, start_response) + self.assertEquals(resp, ['Bad domain in host header']) + + def test_domain_remap_account_with_path_root(self): + req = Request.blank('/v1', environ={'REQUEST_METHOD': 'GET'}, + headers={'Host': 'a.example.com'}) + resp = self.app(req.environ, start_response) + self.assertEquals(resp, '/v1/a') + + def test_domain_remap_account_container_with_path_root(self): + req = Request.blank('/v1', environ={'REQUEST_METHOD': 'GET'}, + headers={'Host': 'c.a.example.com'}) + resp = self.app(req.environ, start_response) + self.assertEquals(resp, '/v1/a/c') + + def test_domain_remap_account_container_with_path(self): + req = Request.blank('/obj', environ={'REQUEST_METHOD': 'GET'}, + headers={'Host': 'c.a.example.com'}) + resp = self.app(req.environ, start_response) + self.assertEquals(resp, '/v1/a/c/obj') + + def test_domain_remap_account_container_with_path_root_and_path(self): + req = Request.blank('/v1/obj', environ={'REQUEST_METHOD': 'GET'}, + headers={'Host': 'c.a.example.com'}) + resp = self.app(req.environ, start_response) + self.assertEquals(resp, '/v1/a/c/obj') + + def test_domain_remap_account_matching_ending_not_domain(self): + req = Request.blank('/dontchange', environ={'REQUEST_METHOD': 'GET'}, + headers={'Host': 'c.aexample.com'}) + resp = self.app(req.environ, start_response) + self.assertEquals(resp, '/dontchange') + + def test_domain_remap_configured_with_empty_storage_domain(self): + self.app = domain_remap.DomainRemapMiddleware(FakeApp(), + {'storage_domain': ''}) + req = Request.blank('/test', environ={'REQUEST_METHOD': 'GET'}, + headers={'Host': 'c.a.example.com'}) + resp = self.app(req.environ, start_response) + self.assertEquals(resp, '/test') + + +if __name__ == '__main__': + unittest.main()