Added swift3.Request

This patch introduces S3 request object, swift3.Request, and moves request
manipulation code like header validation or translation into request.py.

The main purpose of this change is to improve code maintainability.  After this
patch, Swift requests and responses are completely distinguished from S3's
ones, and middleware.py no longer touch swob.Request and swob.Response.  All
the controllers in middleware.py can focus on how requests flow into Swift.

Change-Id: I7545f1ebcb5f36a7cb78b812ac6f0124beee5cb8
This commit is contained in:
MORITA Kazutaka
2014-05-27 10:14:12 +09:00
parent 6448a43ef8
commit aceaad2b75
5 changed files with 348 additions and 233 deletions

22
swift3/exception.py Normal file
View File

@@ -0,0 +1,22 @@
# Copyright (c) 2014 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.
class S3Exception(Exception):
pass
class NotS3Request(S3Exception):
pass

View File

@@ -53,14 +53,10 @@ following for an SAIO setup::
""" """
from urllib import quote from urllib import quote
import base64
from simplejson import loads from simplejson import loads
import email.utils
import datetime
import re import re
from swift.common.utils import get_logger from swift.common.utils import get_logger
from swift.common.swob import Request
from swift.common.http import HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, \ from swift.common.http import HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, \
HTTP_NO_CONTENT, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, \ HTTP_NO_CONTENT, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, \
HTTP_CONFLICT, HTTP_UNPROCESSABLE_ENTITY, is_success, \ HTTP_CONFLICT, HTTP_UNPROCESSABLE_ENTITY, is_success, \
@@ -68,11 +64,12 @@ from swift.common.http import HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, \
from swift.common.middleware.acl import parse_acl, referrer_allowed from swift.common.middleware.acl import parse_acl, referrer_allowed
from swift3.etree import fromstring, tostring, Element, SubElement from swift3.etree import fromstring, tostring, Element, SubElement
from swift3.response import Response, HTTPNoContent, HTTPOk, ErrorResponse, \ from swift3.exception import NotS3Request
from swift3.request import Request
from swift3.response import HTTPNoContent, HTTPOk, ErrorResponse, \
AccessDenied, BucketAlreadyExists, BucketNotEmpty, EntityTooLarge, \ AccessDenied, BucketAlreadyExists, BucketNotEmpty, EntityTooLarge, \
InternalError, InvalidArgument, InvalidDigest, InvalidURI, \ InternalError, InvalidArgument, InvalidDigest, MalformedACLError, \
MalformedACLError, MethodNotAllowed, NoSuchBucket, NoSuchKey, \ MethodNotAllowed, NoSuchBucket, NoSuchKey, S3NotImplemented
S3NotImplemented, RequestTimeTooSkewed, SignatureDoesNotMatch
XMLNS_XSI = 'http://www.w3.org/2001/XMLSchema-instance' XMLNS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
@@ -140,45 +137,6 @@ def get_acl(account_name, headers):
return HTTPOk(body=body, content_type="text/plain") return HTTPOk(body=body, content_type="text/plain")
def canonical_string(req):
"""
Canonicalize a request to a token that can be signed.
"""
amz_headers = {}
buf = "%s\n%s\n%s\n" % (req.method, req.headers.get('Content-MD5', ''),
req.headers.get('Content-Type') or '')
for amz_header in sorted((key.lower() for key in req.headers
if key.lower().startswith('x-amz-'))):
amz_headers[amz_header] = req.headers[amz_header]
if 'x-amz-date' in amz_headers:
buf += "\n"
elif 'Date' in req.headers:
buf += "%s\n" % req.headers['Date']
for k in sorted(key.lower() for key in amz_headers):
buf += "%s:%s\n" % (k, amz_headers[k])
# RAW_PATH_INFO is enabled in later version than eventlet 0.9.17.
# When using older version, swift3 uses req.path of swob instead
# of it.
path = req.environ.get('RAW_PATH_INFO', req.path)
if req.query_string:
path += '?' + req.query_string
if '?' in path:
path, args = path.split('?', 1)
params = []
for key, value in sorted(req.params.items()):
if key in ALLOWED_SUB_RESOURCES:
params.append('%s=%s' % (key, value) if value else key)
if params:
return '%s%s?%s' % (buf, path, '&'.join(params))
return buf + path
def swift_acl_translate(acl, group='', user='', xml=False): def swift_acl_translate(acl, group='', user='', xml=False):
""" """
Takes an S3 style ACL and returns a list of header/value pairs that Takes an S3 style ACL and returns a list of header/value pairs that
@@ -255,21 +213,9 @@ class Controller(object):
""" """
Base WSGI controller class for the middleware Base WSGI controller class for the middleware
""" """
def __init__(self, req, app, account_name, token, conf, def __init__(self, app, conf, **kwargs):
container_name=None, object_name=None, **kwargs):
self.app = app self.app = app
self.conf = conf self.conf = conf
self.account_name = account_name
self.container_name = container_name
self.object_name = object_name
req.environ['HTTP_X_AUTH_TOKEN'] = token
if object_name:
req.path_info = '/v1/%s/%s/%s' % (account_name, container_name,
object_name)
elif container_name:
req.path_info = '/v1/%s/%s' % (account_name, container_name)
else:
req.path_info = '/v1/%s' % (account_name)
class ServiceController(Controller): class ServiceController(Controller):
@@ -318,12 +264,10 @@ class BucketController(Controller):
req.query_string = '' req.query_string = ''
resp = req.get_response(self.app) resp = req.get_response(self.app)
status = resp.status_int if resp.status_int == HTTP_NO_CONTENT:
headers = resp.headers resp.status_int = HTTP_OK
if status == HTTP_NO_CONTENT:
status = HTTP_OK
return Response(status=status, headers=headers, app_iter=resp.app_iter) return resp
def GET(self, req): def GET(self, req):
""" """
@@ -351,7 +295,7 @@ class BucketController(Controller):
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
raise AccessDenied() raise AccessDenied()
elif status == HTTP_NOT_FOUND: elif status == HTTP_NOT_FOUND:
raise NoSuchBucket(self.container_name) raise NoSuchBucket(req.container_name)
else: else:
raise InternalError() raise InternalError()
@@ -367,7 +311,7 @@ class BucketController(Controller):
is_truncated = 'false' is_truncated = 'false'
SubElement(elem, 'IsTruncated').text = is_truncated SubElement(elem, 'IsTruncated').text = is_truncated
SubElement(elem, 'MaxKeys').text = str(max_keys) SubElement(elem, 'MaxKeys').text = str(max_keys)
SubElement(elem, 'Name').text = self.container_name SubElement(elem, 'Name').text = req.container_name
for o in objects[:max_keys]: for o in objects[:max_keys]:
if 'subdir' not in o: if 'subdir' not in o:
@@ -377,7 +321,7 @@ class BucketController(Controller):
o['last_modified'] + 'Z' o['last_modified'] + 'Z'
SubElement(contents, 'ETag').text = o['hash'] SubElement(contents, 'ETag').text = o['hash']
SubElement(contents, 'Size').text = str(o['bytes']) SubElement(contents, 'Size').text = str(o['bytes'])
add_canonical_user(contents, 'Owner', self.account_name) add_canonical_user(contents, 'Owner', req.access_key)
for o in objects[:max_keys]: for o in objects[:max_keys]:
if 'subdir' in o: if 'subdir' in o:
@@ -410,14 +354,6 @@ class BucketController(Controller):
for header, acl in translated_acl: for header, acl in translated_acl:
req.headers[header] = acl req.headers[header] = acl
if 'CONTENT_LENGTH' in req.environ:
try:
if req.content_length < 0:
raise InvalidArgument('Content-Length', req.content_length)
except (ValueError, TypeError):
raise InvalidArgument('Content-Length',
req.environ['CONTENT_LENGTH'])
resp = req.get_response(self.app) resp = req.get_response(self.app)
status = resp.status_int status = resp.status_int
@@ -425,11 +361,11 @@ class BucketController(Controller):
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
raise AccessDenied() raise AccessDenied()
elif status == HTTP_ACCEPTED: elif status == HTTP_ACCEPTED:
raise BucketAlreadyExists(self.container_name) raise BucketAlreadyExists(req.container_name)
else: else:
raise InternalError() raise InternalError()
return HTTPOk(headers={'Location': self.container_name}) return HTTPOk(headers={'Location': req.container_name})
def DELETE(self, req): def DELETE(self, req):
""" """
@@ -442,7 +378,7 @@ class BucketController(Controller):
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
raise AccessDenied() raise AccessDenied()
elif status == HTTP_NOT_FOUND: elif status == HTTP_NOT_FOUND:
raise NoSuchBucket(self.container_name) raise NoSuchBucket(req.container_name)
elif status == HTTP_CONFLICT: elif status == HTTP_CONFLICT:
raise BucketNotEmpty() raise BucketNotEmpty()
else: else:
@@ -464,18 +400,16 @@ class ObjectController(Controller):
def GETorHEAD(self, req): def GETorHEAD(self, req):
resp = req.get_response(self.app) resp = req.get_response(self.app)
status = resp.status_int status = resp.status_int
headers = resp.headers
if req.method == 'HEAD': if req.method == 'HEAD':
resp.app_iter = None resp.app_iter = None
if is_success(status): if is_success(status):
return Response(status=status, headers=headers, return resp
app_iter=resp.app_iter)
elif status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): elif status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
raise AccessDenied() raise AccessDenied()
elif status == HTTP_NOT_FOUND: elif status == HTTP_NOT_FOUND:
raise NoSuchKey(self.object_name) raise NoSuchKey(req.object_name)
else: else:
raise InternalError() raise InternalError()
@@ -495,23 +429,6 @@ class ObjectController(Controller):
""" """
Handle PUT Object and PUT Object (Copy) request Handle PUT Object and PUT Object (Copy) request
""" """
for key, value in req.environ.items():
if key.startswith('HTTP_X_AMZ_META_'):
del req.environ[key]
req.environ['HTTP_X_OBJECT_META_' + key[16:]] = value
elif key == 'HTTP_CONTENT_MD5':
if value == '':
raise InvalidDigest()
try:
req.environ['HTTP_ETAG'] = \
value.decode('base64').encode('hex')
except Exception:
raise InvalidDigest()
if req.environ['HTTP_ETAG'] == '':
raise SignatureDoesNotMatch()
elif key == 'HTTP_X_AMZ_COPY_SOURCE':
req.environ['HTTP_X_COPY_FROM'] = value
resp = req.get_response(self.app) resp = req.get_response(self.app)
status = resp.status_int status = resp.status_int
@@ -519,7 +436,7 @@ class ObjectController(Controller):
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
raise AccessDenied() raise AccessDenied()
elif status == HTTP_NOT_FOUND: elif status == HTTP_NOT_FOUND:
raise NoSuchBucket(self.container_name) raise NoSuchBucket(req.container_name)
elif status == HTTP_UNPROCESSABLE_ENTITY: elif status == HTTP_UNPROCESSABLE_ENTITY:
raise InvalidDigest() raise InvalidDigest()
elif status == HTTP_REQUEST_ENTITY_TOO_LARGE: elif status == HTTP_REQUEST_ENTITY_TOO_LARGE:
@@ -553,7 +470,7 @@ class ObjectController(Controller):
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
raise AccessDenied() raise AccessDenied()
elif status == HTTP_NOT_FOUND: elif status == HTTP_NOT_FOUND:
raise NoSuchKey(self.object_name) raise NoSuchKey(req.object_name)
else: else:
raise InternalError() raise InternalError()
@@ -575,7 +492,7 @@ class AclController(Controller):
""" """
Handles GET Bucket acl and GET Object acl. Handles GET Bucket acl and GET Object acl.
""" """
if self.object_name: if req.object_name:
# Handle Object ACL # Handle Object ACL
# ACL requests need to make a HEAD call rather than GET # ACL requests need to make a HEAD call rather than GET
@@ -590,11 +507,11 @@ class AclController(Controller):
if is_success(status): if is_success(status):
# Method must be GET or the body wont be returned to the caller # Method must be GET or the body wont be returned to the caller
req.environ['REQUEST_METHOD'] = 'GET' req.environ['REQUEST_METHOD'] = 'GET'
return get_acl(self.account_name, headers) return get_acl(req.access_key, headers)
elif status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): elif status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
raise AccessDenied() raise AccessDenied()
elif status == HTTP_NOT_FOUND: elif status == HTTP_NOT_FOUND:
raise NoSuchKey(self.object_name) raise NoSuchKey(req.object_name)
else: else:
raise InternalError() raise InternalError()
@@ -605,12 +522,12 @@ class AclController(Controller):
headers = resp.headers headers = resp.headers
if is_success(status): if is_success(status):
return get_acl(self.account_name, headers) return get_acl(req.access_key, headers)
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
raise AccessDenied() raise AccessDenied()
elif status == HTTP_NOT_FOUND: elif status == HTTP_NOT_FOUND:
raise NoSuchBucket(self.container_name) raise NoSuchBucket(req.container_name)
else: else:
raise InternalError() raise InternalError()
@@ -618,7 +535,7 @@ class AclController(Controller):
""" """
Handles PUT Bucket acl and PUT Object acl. Handles PUT Bucket acl and PUT Object acl.
""" """
if self.object_name: if req.object_name:
# Handle Object ACL # Handle Object ACL
raise S3NotImplemented() raise S3NotImplemented()
else: else:
@@ -643,7 +560,7 @@ class AclController(Controller):
else: else:
raise InternalError() raise InternalError()
return HTTPOk(headers={'Location': self.container_name}) return HTTPOk(headers={'Location': req.container_name})
class LocationController(Controller): class LocationController(Controller):
@@ -662,7 +579,7 @@ class LocationController(Controller):
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
raise AccessDenied() raise AccessDenied()
elif status == HTTP_NOT_FOUND: elif status == HTTP_NOT_FOUND:
raise NoSuchBucket(self.container_name) raise NoSuchBucket(req.container_name)
else: else:
raise InternalError() raise InternalError()
@@ -694,7 +611,7 @@ class LoggingStatusController(Controller):
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
raise AccessDenied() raise AccessDenied()
elif status == HTTP_NOT_FOUND: elif status == HTTP_NOT_FOUND:
raise NoSuchBucket(self.container_name) raise NoSuchBucket(req.container_name)
else: else:
raise InternalError() raise InternalError()
@@ -741,9 +658,9 @@ class MultiObjectDeleteController(Controller):
sub_req.query_string = '' sub_req.query_string = ''
sub_req.content_length = 0 sub_req.content_length = 0
sub_req.method = 'DELETE' sub_req.method = 'DELETE'
controller = ObjectController(sub_req, self.app, self.account_name, sub_req.object_name = key
req.environ['HTTP_X_AUTH_TOKEN'],
self.container_name, key) controller = ObjectController(self.app, self.conf)
try: try:
controller.DELETE(sub_req) controller.DELETE(sub_req)
except NoSuchKey: except NoSuchKey:
@@ -777,7 +694,7 @@ class PartController(Controller):
Handles Upload Part and Upload Part Copy. Handles Upload Part and Upload Part Copy.
""" """
# Pass it through, the s3multi upload helper will handle it. # Pass it through, the s3multi upload helper will handle it.
return self.app return req.get_response(self.app)
class UploadsController(Controller): class UploadsController(Controller):
@@ -794,14 +711,14 @@ class UploadsController(Controller):
Handles List Multipart Uploads Handles List Multipart Uploads
""" """
# Pass it through, the s3multi upload helper will handle it. # Pass it through, the s3multi upload helper will handle it.
return self.app return req.get_response(self.app)
def POST(self, req): def POST(self, req):
""" """
Handles Initiate Multipart Upload. Handles Initiate Multipart Upload.
""" """
# Pass it through, the s3multi upload helper will handle it. # Pass it through, the s3multi upload helper will handle it.
return self.app return req.get_response(self.app)
class UploadController(Controller): class UploadController(Controller):
@@ -819,21 +736,21 @@ class UploadController(Controller):
Handles List Parts. Handles List Parts.
""" """
# Pass it through, the s3multi upload helper will handle it. # Pass it through, the s3multi upload helper will handle it.
return self.app return req.get_response(self.app)
def DELETE(self, req): def DELETE(self, req):
""" """
Handles Abort Multipart Upload. Handles Abort Multipart Upload.
""" """
# Pass it through, the s3multi upload helper will handle it. # Pass it through, the s3multi upload helper will handle it.
return self.app return req.get_response(self.app)
def POST(self, req): def POST(self, req):
""" """
Handles Complete Multipart Upload. Handles Complete Multipart Upload.
""" """
# Pass it through, the s3multi upload helper will handle it. # Pass it through, the s3multi upload helper will handle it.
return self.app return req.get_response(self.app)
class VersioningController(Controller): class VersioningController(Controller):
@@ -856,7 +773,7 @@ class VersioningController(Controller):
if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
raise AccessDenied() raise AccessDenied()
elif status == HTTP_NOT_FOUND: elif status == HTTP_NOT_FOUND:
raise NoSuchBucket(self.container_name) raise NoSuchBucket(req.container_name)
else: else:
raise InternalError() raise InternalError()
@@ -880,41 +797,12 @@ class Swift3Middleware(object):
self.conf = conf self.conf = conf
self.logger = get_logger(self.conf, log_route='swift3') self.logger = get_logger(self.conf, log_route='swift3')
def get_controller(self, req):
container, obj = req.split_path(0, 2, True)
d = dict(container_name=container, object_name=obj)
if 'acl' in req.params:
return AclController, d
if 'delete' in req.params:
return MultiObjectDeleteController, d
if 'location' in req.params:
return LocationController, d
if 'logging' in req.params:
return LoggingStatusController, d
if 'partNumber' in req.params:
return PartController, d
if 'uploadId' in req.params:
return UploadController, d
if 'uploads' in req.params:
return UploadsController, d
if 'versioning' in req.params:
return VersioningController, d
if container and obj:
if req.method == 'POST':
if 'uploads' in req.params or 'uploadId' in req.params:
return BucketController, d
return ObjectController, d
elif container:
return BucketController, d
return ServiceController, d
def __call__(self, env, start_response): def __call__(self, env, start_response):
req = Request(env)
try: try:
req = Request(env)
resp = self.handle_request(req) resp = self.handle_request(req)
except NotS3Request:
resp = self.app
except ErrorResponse as err_resp: except ErrorResponse as err_resp:
if isinstance(err_resp, InternalError): if isinstance(err_resp, InternalError):
self.logger.exception(err_resp) self.logger.exception(err_resp)
@@ -928,71 +816,7 @@ class Swift3Middleware(object):
self.logger.debug('Calling Swift3 Middleware') self.logger.debug('Calling Swift3 Middleware')
self.logger.debug(req.__dict__) self.logger.debug(req.__dict__)
if 'AWSAccessKeyId' in req.params: controller = req.controller(self.app, self.conf)
try:
req.headers['Date'] = req.params['Expires']
req.headers['Authorization'] = \
'AWS %(AWSAccessKeyId)s:%(Signature)s' % req.params
except KeyError:
raise AccessDenied()
if 'Authorization' not in req.headers:
return self.app
try:
keyword, info = req.headers['Authorization'].split(' ')
except Exception:
raise AccessDenied()
if keyword != 'AWS':
raise AccessDenied()
try:
account, signature = info.rsplit(':', 1)
except Exception:
err_msg = 'AWS authorization header is invalid. ' \
'Expected AwsAccessKeyId:signature'
raise InvalidArgument('Authorization',
req.headers['Authorization'], err_msg)
try:
controller, path_parts = self.get_controller(req)
except ValueError:
raise InvalidURI(req.path)
if 'Date' in req.headers:
now = datetime.datetime.utcnow()
date = email.utils.parsedate(req.headers['Date'])
if 'Expires' in req.params:
try:
d = email.utils.formatdate(float(req.params['Expires']))
except ValueError:
raise AccessDenied()
# check expiration
expdate = email.utils.parsedate(d)
ex = datetime.datetime(*expdate[0:6])
if now > ex:
raise AccessDenied()
elif date is not None:
epoch = datetime.datetime(1970, 1, 1, 0, 0, 0, 0)
d1 = datetime.datetime(*date[0:6])
if d1 < epoch:
raise AccessDenied()
# If the standard date is too far ahead or behind, it is an
# error
delta = datetime.timedelta(seconds=60 * 5)
if abs(d1 - now) > delta:
raise RequestTimeTooSkewed()
else:
raise AccessDenied()
token = base64.urlsafe_b64encode(canonical_string(req))
controller = controller(req, self.app, account, token, self.conf,
**path_parts)
if hasattr(controller, req.method): if hasattr(controller, req.method):
res = getattr(controller, req.method)(req) res = getattr(controller, req.method)(req)

234
swift3/request.py Normal file
View File

@@ -0,0 +1,234 @@
# Copyright (c) 2014 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.
from urllib import quote
import base64
import email.utils
import datetime
from swift.common import swob
from swift3.response import AccessDenied, InvalidArgument, InvalidDigest, \
RequestTimeTooSkewed, Response, SignatureDoesNotMatch
from swift3.exception import NotS3Request
# List of sub-resources that must be maintained as part of the HMAC
# signature string.
ALLOWED_SUB_RESOURCES = sorted([
'acl', 'delete', 'lifecycle', 'location', 'logging', 'notification',
'partNumber', 'policy', 'requestPayment', 'torrent', 'uploads', 'uploadId',
'versionId', 'versioning', 'versions ', 'website'
])
class Request(swob.Request):
"""
S3 request object.
"""
def __init__(self, env):
swob.Request.__init__(self, env)
self.access_key, self.signature = self._parse_authorization()
self.container_name, self.object_name = self.split_path(0, 2, True)
self._validate_headers()
self.token = base64.urlsafe_b64encode(self._canonical_string())
def _parse_authorization(self):
if 'AWSAccessKeyId' in self.params:
try:
self.headers['Date'] = self.params['Expires']
self.headers['Authorization'] = \
'AWS %(AWSAccessKeyId)s:%(Signature)s' % self.params
except KeyError:
raise AccessDenied()
if 'Authorization' not in self.headers:
raise NotS3Request
try:
keyword, info = self.headers['Authorization'].split(' ')
except Exception:
raise AccessDenied()
if keyword != 'AWS':
raise AccessDenied()
try:
access_key, signature = info.rsplit(':', 1)
except Exception:
err_msg = 'AWS authorization header is invalid. ' \
'Expected AwsAccessKeyId:signature'
raise InvalidArgument('Authorization',
self.headers['Authorization'], err_msg)
return access_key, signature
def _validate_headers(self):
if 'CONTENT_LENGTH' in self.environ:
try:
if self.content_length < 0:
raise InvalidArgument('Content-Length',
self.content_length)
except (ValueError, TypeError):
raise InvalidArgument('Content-Length',
self.environ['CONTENT_LENGTH'])
if 'Date' in self.headers:
now = datetime.datetime.utcnow()
date = email.utils.parsedate(self.headers['Date'])
if 'Expires' in self.params:
try:
d = email.utils.formatdate(float(self.params['Expires']))
except ValueError:
raise AccessDenied()
# check expiration
expdate = email.utils.parsedate(d)
ex = datetime.datetime(*expdate[0:6])
if now > ex:
raise AccessDenied('Request has expired')
elif date is not None:
epoch = datetime.datetime(1970, 1, 1, 0, 0, 0, 0)
d1 = datetime.datetime(*date[0:6])
if d1 < epoch:
raise AccessDenied()
# If the standard date is too far ahead or behind, it is an
# error
delta = datetime.timedelta(seconds=60 * 5)
if abs(d1 - now) > delta:
raise RequestTimeTooSkewed()
else:
raise AccessDenied()
if 'Content-MD5' in self.headers:
value = self.headers['Content-MD5']
if value == '':
raise InvalidDigest()
try:
self.headers['ETag'] = value.decode('base64').encode('hex')
except Exception:
raise InvalidDigest()
if self.headers['ETag'] == '':
raise SignatureDoesNotMatch()
def _canonical_string(self):
"""
Canonicalize a request to a token that can be signed.
"""
amz_headers = {}
buf = "%s\n%s\n%s\n" % (self.method,
self.headers.get('Content-MD5', ''),
self.headers.get('Content-Type') or '')
for amz_header in sorted((key.lower() for key in self.headers
if key.lower().startswith('x-amz-'))):
amz_headers[amz_header] = self.headers[amz_header]
if 'x-amz-date' in amz_headers:
buf += "\n"
elif 'Date' in self.headers:
buf += "%s\n" % self.headers['Date']
for k in sorted(key.lower() for key in amz_headers):
buf += "%s:%s\n" % (k, amz_headers[k])
path = self.environ.get('RAW_PATH_INFO', self.path)
if self.query_string:
path += '?' + self.query_string
if '?' in path:
path, args = path.split('?', 1)
params = []
for key, value in sorted(self.params.items()):
if key in ALLOWED_SUB_RESOURCES:
params.append('%s=%s' % (key, value) if value else key)
if params:
return '%s%s?%s' % (buf, path, '&'.join(params))
return buf + path
@property
def controller(self):
from swift3.middleware import ServiceController, BucketController, \
ObjectController, AclController, MultiObjectDeleteController, \
LocationController, LoggingStatusController, PartController, \
UploadController, UploadsController, VersioningController
if 'acl' in self.params:
return AclController
if 'delete' in self.params:
return MultiObjectDeleteController
if 'location' in self.params:
return LocationController
if 'logging' in self.params:
return LoggingStatusController
if 'partNumber' in self.params:
return PartController
if 'uploadId' in self.params:
return UploadController
if 'uploads' in self.params:
return UploadsController
if 'versioning' in self.params:
return VersioningController
if self.container_name and self.object_name:
return ObjectController
elif self.container_name:
return BucketController
return ServiceController
def to_swift_req(self):
"""
Create a Swift request based on this request's environment.
"""
env = self.environ.copy()
for key in env:
if key.startswith('HTTP_X_AMZ_META_'):
env['HTTP_X_OBJECT_META_' + key[16:]] = env[key]
del env[key]
if key == 'HTTP_X_AMZ_COPY_SOURCE':
env['HTTP_X_COPY_FROM'] = env[key]
del env[key]
env['swift.source'] = 'S3'
env['HTTP_X_AUTH_TOKEN'] = self.token
if self.object_name:
path = '/v1/%s/%s/%s' % (self.access_key, self.container_name,
self.object_name)
elif self.container_name:
path = '/v1/%s/%s' % (self.access_key, self.container_name)
else:
path = '/v1/%s' % (self.access_key)
env['PATH_INFO'] = path
env['QUERY_STRING'] = self.query_string
return swob.Request.blank(quote(path), environ=env)
def get_response(self, app):
"""
Calls the application with this request's environment. Returns a
Response object that wraps up the application's result.
"""
sw_req = self.to_swift_req()
sw_resp = sw_req.get_response(app)
return Response.from_swift_resp(sw_resp)

View File

@@ -90,6 +90,25 @@ class Response(swob.Response):
self.headers = headers self.headers = headers
@classmethod
def from_swift_resp(cls, sw_resp):
"""
Create a new S3 response object based on the given Swift response.
"""
if sw_resp.app_iter:
body = None
app_iter = sw_resp.app_iter
else:
body = sw_resp.body
app_iter = None
resp = Response(status=sw_resp.status, headers=sw_resp.headers,
request=sw_resp.request, body=body, app_iter=app_iter,
conditional_response=sw_resp.conditional_response)
resp.environ.update(sw_resp.environ)
return resp
class StatusMap(object): class StatusMap(object):
""" """

View File

@@ -28,6 +28,7 @@ from swift.common.swob import Request
from swift3 import middleware as swift3 from swift3 import middleware as swift3
from swift3.test.unit.helpers import FakeSwift from swift3.test.unit.helpers import FakeSwift
from swift3.etree import fromstring, tostring, Element, SubElement from swift3.etree import fromstring, tostring, Element, SubElement
from swift3.request import Request as S3Request
XMLNS_XSI = 'http://www.w3.org/2001/XMLSchema-instance' XMLNS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
@@ -187,7 +188,7 @@ class TestSwift3(unittest.TestCase):
environ={'REQUEST_METHOD': 'GET'}, environ={'REQUEST_METHOD': 'GET'},
headers={'Authorization': 'AWS test:tester:hmac'}) headers={'Authorization': 'AWS test:tester:hmac'})
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
raw_path_info = "/v1/AUTH_test/%s/%s" % (bucket_name, object_name) raw_path_info = "/%s/%s" % (bucket_name, object_name)
path_info = req.environ['PATH_INFO'] path_info = req.environ['PATH_INFO']
self.assertEquals(path_info, unquote(raw_path_info)) self.assertEquals(path_info, unquote(raw_path_info))
self.assertEquals(req.path, quote(path_info)) self.assertEquals(req.path, quote(path_info))
@@ -569,10 +570,24 @@ class TestSwift3(unittest.TestCase):
The hashes here were generated by running the same requests against The hashes here were generated by running the same requests against
boto.utils.canonical_string boto.utils.canonical_string
""" """
def canonical_string(path, headers):
if '?' in path:
path, query_string = path.split('?', 1)
else:
query_string = ''
req = S3Request({
'REQUEST_METHOD': 'GET',
'PATH_INFO': path,
'QUERY_STRING': query_string,
'HTTP_AUTHORIZATION': 'AWS X:Y:Z',
})
req.headers.update(headers)
return req._canonical_string()
def verify(hash, path, headers): def verify(hash, path, headers):
req = Request.blank(path, headers=headers) s = canonical_string(path, headers)
self.assertEquals(hash, hashlib.md5( self.assertEquals(hash, hashlib.md5(s).hexdigest())
swift3.canonical_string(req)).hexdigest())
verify('6dd08c75e42190a1ce9468d1fd2eb787', '/bucket/object', verify('6dd08c75e42190a1ce9468d1fd2eb787', '/bucket/object',
{'Content-Type': 'text/plain', 'X-Amz-Something': 'test', {'Content-Type': 'text/plain', 'X-Amz-Something': 'test',
@@ -586,7 +601,7 @@ class TestSwift3(unittest.TestCase):
verify('be01bd15d8d47f9fe5e2d9248cc6f180', '/bucket/object', {}) verify('be01bd15d8d47f9fe5e2d9248cc6f180', '/bucket/object', {})
verify('8d28cc4b8322211f6cc003256cd9439e', 'bucket/object', verify('e9ec7dca45eef3e2c7276af23135e896', '/bucket/object',
{'Content-MD5': 'somestuff'}) {'Content-MD5': 'somestuff'})
verify('a822deb31213ad09af37b5a7fe59e55e', '/bucket/object?acl', {}) verify('a822deb31213ad09af37b5a7fe59e55e', '/bucket/object?acl', {})
@@ -608,16 +623,16 @@ class TestSwift3(unittest.TestCase):
{'Content-Type': None, {'Content-Type': None,
'Date': 'Tue, 12 Jul 2011 10:52:57 +0000'}) 'Date': 'Tue, 12 Jul 2011 10:52:57 +0000'})
req1 = Request.blank('/', headers= str1 = canonical_string('/', headers=
{'Content-Type': None, 'X-Amz-Something': 'test'}) {'Content-Type': None,
req2 = Request.blank('/', headers= 'X-Amz-Something': 'test'})
{'Content-Type': '', 'X-Amz-Something': 'test'}) str2 = canonical_string('/', headers=
req3 = Request.blank('/', headers={'X-Amz-Something': 'test'}) {'Content-Type': '',
'X-Amz-Something': 'test'})
str3 = canonical_string('/', headers={'X-Amz-Something': 'test'})
self.assertEquals(swift3.canonical_string(req1), self.assertEquals(str1, str2)
swift3.canonical_string(req2)) self.assertEquals(str2, str3)
self.assertEquals(swift3.canonical_string(req2),
swift3.canonical_string(req3))
def test_signed_urls_expired(self): def test_signed_urls_expired(self):
expire = '1000000000' expire = '1000000000'
@@ -668,8 +683,9 @@ class TestSwift3(unittest.TestCase):
environ={'REQUEST_METHOD': 'PUT'}) environ={'REQUEST_METHOD': 'PUT'})
req.headers['Authorization'] = 'AWS test:tester:hmac' req.headers['Authorization'] = 'AWS test:tester:hmac'
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
_, _, headers = self.swift.calls_with_headers[-1]
self.assertEquals(base64.urlsafe_b64decode( self.assertEquals(base64.urlsafe_b64decode(
req.headers['X-Auth-Token']), headers['X-Auth-Token']),
'PUT\n\n\n/bucket/object?partNumber=1&uploadId=123456789abcdef') 'PUT\n\n\n/bucket/object?partNumber=1&uploadId=123456789abcdef')
def test_xml_namespace(self): def test_xml_namespace(self):