# Copyright (c) 2010-2012 OpenStack Foundation
#
# 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.


"""
Domain Remap Middleware

Middleware that translates container and account parts of a domain to path
parameters that the proxy server understands.

Translation is only performed when the request URL's host domain matches one of
a list of domains. This list may be configured by the option
``storage_domain``, and defaults to the single domain ``example.com``.

If not already present, a configurable ``path_root``, which defaults to ``v1``,
will be added to the start of the translated path.

For example, with the default configuration::

    container.AUTH-account.example.com/object
    container.AUTH-account.example.com/v1/object

would both be translated to::

    container.AUTH-account.example.com/v1/AUTH_account/container/object

and::

    AUTH-account.example.com/container/object
    AUTH-account.example.com/v1/container/object

would both be translated to::

    AUTH-account.example.com/v1/AUTH_account/container/object

Additionally, translation is only performed when the account name in the
translated path starts with a reseller prefix matching one of a list configured
by the option ``reseller_prefixes``, or when no match is found but a
``default_reseller_prefix`` has been configured.

The ``reseller_prefixes`` list defaults to the single prefix ``AUTH``. The
``default_reseller_prefix`` is not configured by default.

Browsers can convert a host header to lowercase, so the middleware checks that
the reseller prefix on the account name is the correct case. This is done by
comparing the items in the ``reseller_prefixes`` config option to the found
prefix. If they match except for case, the item from ``reseller_prefixes`` will
be used instead of the found reseller prefix. The middleware will also replace
any hyphen ('-') in the account name with an underscore ('_').

For example, with the default configuration::

    auth-account.example.com/container/object
    AUTH-account.example.com/container/object
    auth_account.example.com/container/object
    AUTH_account.example.com/container/object

would all be translated to::

    <unchanged>.example.com/v1/AUTH_account/container/object

When no match is found in ``reseller_prefixes``, the
``default_reseller_prefix`` config option is used. When no
``default_reseller_prefix`` is configured, any request with an account prefix
not in the ``reseller_prefixes`` list will be ignored by this middleware.

For example, with ``default_reseller_prefix = AUTH``::

    account.example.com/container/object

would be translated to::

    account.example.com/v1/AUTH_account/container/object

Note that this middleware requires that container names and account names
(except as described above) must be DNS-compatible. This means that the account
name created in the system and the containers created by users cannot exceed 63
characters or have UTF-8 characters. These are restrictions over and above what
Swift requires and are not explicitly checked. Simply put, this middleware
will do a best-effort attempt to derive account and container names from
elements in the domain name and put those derived values into the URL path
(leaving the ``Host`` header unchanged).

Also note that using :doc:`overview_container_sync` with remapped domain names
is not advised. With :doc:`overview_container_sync`, you should use the true
storage end points as sync destinations.
"""

from swift.common.middleware import RewriteContext
from swift.common.swob import Request, HTTPBadRequest, wsgi_quote
from swift.common.utils import config_true_value, list_from_csv
from swift.common.registry import register_swift_info


class _DomainRemapContext(RewriteContext):
    base_re = r'^(https?://[^/]+)%s(.*)$'


class DomainRemapMiddleware(object):
    """
    Domain Remap Middleware

    See above for a full description.

    :param app: The next WSGI filter or app in the paste.deploy
                chain.
    :param conf: The configuration dict for the middleware.
    """

    def __init__(self, app, conf):
        self.app = app
        storage_domain = conf.get('storage_domain', 'example.com')
        self.storage_domain = ['.' + s for s in
                               list_from_csv(storage_domain)
                               if not s.startswith('.')]
        self.storage_domain += [s for s in list_from_csv(storage_domain)
                                if s.startswith('.')]
        self.path_root = conf.get('path_root', 'v1').strip('/') + '/'
        prefixes = conf.get('reseller_prefixes', 'AUTH')
        self.reseller_prefixes = list_from_csv(prefixes)
        self.reseller_prefixes_lower = [x.lower()
                                        for x in self.reseller_prefixes]
        self.default_reseller_prefix = conf.get('default_reseller_prefix')
        self.mangle_client_paths = config_true_value(
            conf.get('mangle_client_paths'))

    def __call__(self, env, start_response):
        if not self.storage_domain:
            return self.app(env, start_response)
        if 'HTTP_HOST' in env:
            given_domain = env['HTTP_HOST']
        else:
            given_domain = env['SERVER_NAME']
        port = ''
        if ':' in given_domain:
            given_domain, port = given_domain.rsplit(':', 1)
        storage_domain = next((domain for domain in self.storage_domain
                               if given_domain.endswith(domain)), None)
        if storage_domain:
            parts_to_parse = given_domain[:-len(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=b'Bad domain in host header',
                                      content_type='text/plain')
                return resp(env, start_response)
            if len(self.reseller_prefixes) > 0:
                if '_' not in account and '-' in account:
                    account = account.replace('-', '_', 1)
                account_reseller_prefix = account.split('_', 1)[0].lower()

                if account_reseller_prefix in self.reseller_prefixes_lower:
                    prefix_index = self.reseller_prefixes_lower.index(
                        account_reseller_prefix)
                    real_prefix = self.reseller_prefixes[prefix_index]
                    if not account.startswith(real_prefix):
                        account_suffix = account[len(real_prefix):]
                        account = real_prefix + account_suffix
                elif self.default_reseller_prefix:
                    # account prefix is not in config list. Add default one.
                    account = "%s_%s" % (self.default_reseller_prefix, account)
                else:
                    # account prefix is not in config list. bail.
                    return self.app(env, start_response)

            requested_path = env['PATH_INFO']
            path = requested_path[1:]
            new_path_parts = ['', self.path_root[:-1], account]
            if container:
                new_path_parts.append(container)
            if self.mangle_client_paths and (path + '/').startswith(
                    self.path_root):
                path = path[len(self.path_root):]
            new_path_parts.append(path)
            new_path = '/'.join(new_path_parts)
            env['PATH_INFO'] = new_path

            context = _DomainRemapContext(
                self.app, wsgi_quote(requested_path), wsgi_quote(new_path))
            return context.handle_request(env, start_response)

        return self.app(env, start_response)


def filter_factory(global_conf, **local_conf):
    conf = global_conf.copy()
    conf.update(local_conf)

    register_swift_info(
        'domain_remap',
        default_reseller_prefix=conf.get('default_reseller_prefix'))

    def domain_filter(app):
        return DomainRemapMiddleware(app, conf)
    return domain_filter
