178 lines
7.4 KiB
Python
178 lines
7.4 KiB
Python
# Copyright (c) 2013 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.
|
|
|
|
import os
|
|
|
|
from swift.common.constraints import valid_api_version
|
|
from swift.common.container_sync_realms import ContainerSyncRealms
|
|
from swift.common.swob import HTTPBadRequest, HTTPUnauthorized, wsgify
|
|
from swift.common.utils import (
|
|
config_true_value, get_logger, register_swift_info, streq_const_time)
|
|
from swift.proxy.controllers.base import get_container_info
|
|
|
|
|
|
class ContainerSync(object):
|
|
"""
|
|
WSGI middleware that validates an incoming container sync request
|
|
using the container-sync-realms.conf style of container sync.
|
|
"""
|
|
|
|
def __init__(self, app, conf, logger=None):
|
|
self.app = app
|
|
self.conf = conf
|
|
self.logger = logger or get_logger(conf, log_route='container_sync')
|
|
self.realms_conf = ContainerSyncRealms(
|
|
os.path.join(
|
|
conf.get('swift_dir', '/etc/swift'),
|
|
'container-sync-realms.conf'),
|
|
self.logger)
|
|
self.allow_full_urls = config_true_value(
|
|
conf.get('allow_full_urls', 'true'))
|
|
# configure current realm/cluster for /info
|
|
self.realm = self.cluster = None
|
|
current = conf.get('current', None)
|
|
if current:
|
|
try:
|
|
self.realm, self.cluster = (p.upper() for p in
|
|
current.strip('/').split('/'))
|
|
except ValueError:
|
|
self.logger.error('Invalid current //REALM/CLUSTER (%s)',
|
|
current)
|
|
self.register_info()
|
|
|
|
def register_info(self):
|
|
dct = {}
|
|
for realm in self.realms_conf.realms():
|
|
clusters = self.realms_conf.clusters(realm)
|
|
if clusters:
|
|
dct[realm] = {'clusters': dict((c, {}) for c in clusters)}
|
|
if self.realm and self.cluster:
|
|
try:
|
|
dct[self.realm]['clusters'][self.cluster]['current'] = True
|
|
except KeyError:
|
|
self.logger.error('Unknown current //REALM/CLUSTER (%s)',
|
|
'//%s/%s' % (self.realm, self.cluster))
|
|
register_swift_info('container_sync', realms=dct)
|
|
|
|
@wsgify
|
|
def __call__(self, req):
|
|
if req.path == '/info':
|
|
# Ensure /info requests get the freshest results
|
|
self.register_info()
|
|
return self.app
|
|
|
|
try:
|
|
(version, acc, cont, obj) = req.split_path(3, 4, True)
|
|
bad_path = False
|
|
except ValueError:
|
|
bad_path = True
|
|
|
|
# use of bad_path bool is to avoid recursive tracebacks
|
|
if bad_path or not valid_api_version(version):
|
|
return self.app
|
|
|
|
# validate container-sync metdata update
|
|
info = get_container_info(
|
|
req.environ, self.app, swift_source='CS')
|
|
sync_to = req.headers.get('x-container-sync-to')
|
|
if req.method in ('PUT', 'POST') and cont and not obj:
|
|
versions_cont = info.get(
|
|
'sysmeta', {}).get('versions-container')
|
|
if sync_to and versions_cont:
|
|
raise HTTPBadRequest(
|
|
'Cannot configure container sync on a container '
|
|
'with object versioning configured.',
|
|
request=req)
|
|
|
|
if not self.allow_full_urls:
|
|
if sync_to and not sync_to.startswith('//'):
|
|
raise HTTPBadRequest(
|
|
body='Full URLs are not allowed for X-Container-Sync-To '
|
|
'values. Only realm values of the format '
|
|
'//realm/cluster/account/container are allowed.\n',
|
|
request=req)
|
|
auth = req.headers.get('x-container-sync-auth')
|
|
if auth:
|
|
valid = False
|
|
auth = auth.split()
|
|
if len(auth) != 3:
|
|
req.environ.setdefault('swift.log_info', []).append(
|
|
'cs:not-3-args')
|
|
else:
|
|
realm, nonce, sig = auth
|
|
realm_key = self.realms_conf.key(realm)
|
|
realm_key2 = self.realms_conf.key2(realm)
|
|
if not realm_key:
|
|
req.environ.setdefault('swift.log_info', []).append(
|
|
'cs:no-local-realm-key')
|
|
else:
|
|
user_key = info.get('sync_key')
|
|
if not user_key:
|
|
req.environ.setdefault('swift.log_info', []).append(
|
|
'cs:no-local-user-key')
|
|
else:
|
|
# x-timestamp headers get shunted by gatekeeper
|
|
if 'x-backend-inbound-x-timestamp' in req.headers:
|
|
req.headers['x-timestamp'] = req.headers.pop(
|
|
'x-backend-inbound-x-timestamp')
|
|
|
|
expected = self.realms_conf.get_sig(
|
|
req.method, req.path,
|
|
req.headers.get('x-timestamp', '0'), nonce,
|
|
realm_key, user_key)
|
|
expected2 = self.realms_conf.get_sig(
|
|
req.method, req.path,
|
|
req.headers.get('x-timestamp', '0'), nonce,
|
|
realm_key2, user_key) if realm_key2 else expected
|
|
if not streq_const_time(sig, expected) and \
|
|
not streq_const_time(sig, expected2):
|
|
req.environ.setdefault(
|
|
'swift.log_info', []).append('cs:invalid-sig')
|
|
else:
|
|
req.environ.setdefault(
|
|
'swift.log_info', []).append('cs:valid')
|
|
valid = True
|
|
if not valid:
|
|
exc = HTTPUnauthorized(
|
|
body='X-Container-Sync-Auth header not valid; '
|
|
'contact cluster operator for support.',
|
|
headers={'content-type': 'text/plain'},
|
|
request=req)
|
|
exc.headers['www-authenticate'] = ' '.join([
|
|
'SwiftContainerSync',
|
|
exc.www_authenticate().split(None, 1)[1]])
|
|
raise exc
|
|
else:
|
|
req.environ['swift.authorize_override'] = True
|
|
# An SLO manifest will already be in the internal manifest
|
|
# syntax and might be synced before its segments, so stop SLO
|
|
# middleware from performing the usual manifest validation.
|
|
req.environ['swift.slo_override'] = True
|
|
# Similar arguments for static symlinks
|
|
req.environ['swift.symlink_override'] = True
|
|
|
|
return self.app
|
|
|
|
|
|
def filter_factory(global_conf, **local_conf):
|
|
conf = global_conf.copy()
|
|
conf.update(local_conf)
|
|
register_swift_info('container_sync')
|
|
|
|
def cache_filter(app):
|
|
return ContainerSync(app, conf)
|
|
|
|
return cache_filter
|