Guest User

swift3 middleware

a guest
Dec 5th, 2012
47
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 29.07 KB | None | 0 0
  1. # Copyright (c) 2010 OpenStack, LLC.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. #    http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  12. # implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15.  
  16. """
  17. The swift3 middleware will emulate the S3 REST api on top of swift.
  18.  
  19. The following opperations are currently supported:
  20.  
  21.    * GET Service
  22.    * DELETE Bucket
  23.    * GET Bucket (List Objects)
  24.    * PUT Bucket
  25.    * DELETE Object
  26.    * GET Object
  27.    * HEAD Object
  28.    * PUT Object
  29.    * PUT Object (Copy)
  30.  
  31. To add this middleware to your configuration, add the swift3 middleware
  32. in front of the auth middleware, and before any other middleware that
  33. look at swift requests (like rate limiting).
  34.  
  35. To set up your client, the access key will be the concatenation of the
  36. account and user strings that should look like test:tester, and the
  37. secret access key is the account password.  The host should also point
  38. to the swift storage hostname.  It also will have to use the old style
  39. calling format, and not the hostname based container format.
  40.  
  41. An example client using the python boto library might look like the
  42. following for an SAIO setup::
  43.  
  44.    connection = boto.s3.Connection(
  45.        aws_access_key_id='test:tester',
  46.        aws_secret_access_key='testing',
  47.        port=8080,
  48.        host='127.0.0.1',
  49.        is_secure=False,
  50.        calling_format=boto.s3.connection.OrdinaryCallingFormat())
  51. """
  52.  
  53. from urllib import unquote, quote
  54. import base64
  55. from xml.sax.saxutils import escape as xml_escape
  56. import urlparse
  57. from xml.dom.minidom import parseString
  58.  
  59. from webob import Request, Response
  60. from simplejson import loads
  61. import email.utils
  62. import datetime
  63.  
  64. from swift.common.utils import split_path, get_logger
  65. from swift.common.utils import split_path
  66. from swift.common.wsgi import WSGIContext
  67. from swift.common.http import HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, \
  68.     HTTP_NO_CONTENT, HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, \
  69.     HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_UNPROCESSABLE_ENTITY, is_success
  70.  
  71.  
  72. MAX_BUCKET_LISTING = 1000
  73.  
  74.  
  75. def get_err_response(code):
  76.     """
  77.    Given an HTTP response code, create a properly formatted xml error response
  78.  
  79.    :param code: error code
  80.    :returns: webob.response object
  81.    """
  82.     error_table = {
  83.         'AccessDenied':
  84.             (HTTP_FORBIDDEN, 'Access denied'),
  85.         'BucketAlreadyExists':
  86.             (HTTP_CONFLICT, 'The requested bucket name is not available'),
  87.         'BucketNotEmpty':
  88.             (HTTP_CONFLICT, 'The bucket you tried to delete is not empty'),
  89.         'InvalidArgument':
  90.             (HTTP_BAD_REQUEST, 'Invalid Argument'),
  91.         'InvalidBucketName':
  92.             (HTTP_BAD_REQUEST, 'The specified bucket is not valid'),
  93.         'InvalidURI':
  94.             (HTTP_BAD_REQUEST, 'Could not parse the specified URI'),
  95.         'InvalidDigest':
  96.             (HTTP_BAD_REQUEST, 'The Content-MD5 you specified was invalid'),
  97.         'NoSuchBucket':
  98.             (HTTP_NOT_FOUND, 'The specified bucket does not exist'),
  99.         'SignatureDoesNotMatch':
  100.             (HTTP_FORBIDDEN, 'The calculated request signature does not '\
  101.             'match your provided one'),
  102.         'RequestTimeTooSkewed':
  103.             (HTTP_FORBIDDEN, 'The difference between the request time and '\
  104.                  ' the current time is too large'),
  105.         'NoSuchKey':
  106.             (HTTP_NOT_FOUND, 'The resource you requested does not exist')}
  107.  
  108.     resp = Response(content_type='text/xml')
  109.     resp.status = error_table[code][0]
  110.     resp.body = error_table[code][1]
  111.     resp.body = '<?xml version="1.0" encoding="UTF-8"?>\r\n<Error>\r\n  ' \
  112.                 '<Code>%s</Code>\r\n  <Message>%s</Message>\r\n</Error>\r\n' \
  113.                  % (code, error_table[code][1])
  114.     return resp
  115.  
  116.  
  117. def get_acl(account_name, headers):
  118.     """
  119.    Attempts to construct an S3 ACL based on what is found in the swift headers
  120.    """
  121.  
  122.     acl = 'private'  # default to private
  123.  
  124.     if 'x-container-read' in headers:
  125.         if headers['x-container-read'] == ".r:*" or\
  126.             ".r:*," in headers['x-container-read'] or \
  127.                 ",*," in headers['x-container-read']:
  128.             acl = 'public-read'
  129.     if 'x-container-write' in headers:
  130.         if headers['x-container-write'] == ".r:*" or\
  131.             ".r:*," in headers['x-container-write'] or \
  132.                 ",*," in headers['x-container-write']:
  133.             if acl == 'public-read':
  134.                 acl = 'public-read-write'
  135.             else:
  136.                 acl = 'public-write'
  137.  
  138.     if acl == 'private':
  139.         body = ('<AccessControlPolicy>'
  140.                 '<Owner>'
  141.                 '<ID>%s</ID>'
  142.                 '<DisplayName>%s</DisplayName>'
  143.                 '</Owner>'
  144.                 '<AccessControlList>'
  145.                 '<Grant>'
  146.                 '<Grantee xmlns:xsi="http://www.w3.org/2001/'
  147.                 'XMLSchema-instance" xsi:type="CanonicalUser">'
  148.                 '<ID>%s</ID>'
  149.                 '<DisplayName>%s</DisplayName>'
  150.                 '</Grantee>'
  151.                 '<Permission>FULL_CONTROL</Permission>'
  152.                 '</Grant>'
  153.                 '</AccessControlList>'
  154.                 '</AccessControlPolicy>' %
  155.                 (account_name, account_name, account_name, account_name))
  156.     elif acl == 'public-read':
  157.         body = ('<AccessControlPolicy>'
  158.                 '<Owner>'
  159.                 '<ID>%s</ID>'
  160.                 '<DisplayName>%s</DisplayName>'
  161.                 '</Owner>'
  162.                 '<AccessControlList>'
  163.                 '<Grant>'
  164.                 '<Grantee xmlns:xsi="http://www.w3.org/2001/'
  165.                 'XMLSchema-instance" xsi:type="CanonicalUser">'
  166.                 '<ID>%s</ID>'
  167.                 '<DisplayName>%s</DisplayName>'
  168.                 '</Grantee>'
  169.                 '<Permission>FULL_CONTROL</Permission>'
  170.                 '</Grant>'
  171.                 '<Grant>'
  172.                 '<Grantee xmlns:xsi="http://www.w3.org/2001/'
  173.                 'XMLSchema-instance" xsi:type="Group">'
  174.                 '<URI>http://acs.amazonaws.com/groups/global/AllUsers</URI>'
  175.                 '</Grantee>'
  176.                 '<Permission>READ</Permission>'
  177.                 '</Grant>'
  178.                 '</AccessControlList>'
  179.                 '</AccessControlPolicy>' %
  180.                 (account_name, account_name, account_name, account_name))
  181.     elif acl == 'public-read-write':
  182.         body = ('<AccessControlPolicy>'
  183.                 '<Owner>'
  184.                 '<ID>%s</ID>'
  185.                 '<DisplayName>%s</DisplayName>'
  186.                 '</Owner>'
  187.                 '<AccessControlList>'
  188.                 '<Grant>'
  189.                 '<Grantee xmlns:xsi="http://www.w3.org/2001/'
  190.                 'XMLSchema-instance" xsi:type="CanonicalUser">'
  191.                 '<ID>%s</ID>'
  192.                 '<DisplayName>%s</DisplayName>'
  193.                 '</Grantee>'
  194.                 '<Permission>FULL_CONTROL</Permission>'
  195.                 '</Grant>'
  196.                 '<Grant>'
  197.                 '<Grantee xmlns:xsi="http://www.w3.org/2001/'
  198.                 'XMLSchema-instance" xsi:type="Group">'
  199.                 '<URI>http://acs.amazonaws.com/groups/global/AllUsers</URI>'
  200.                 '</Grantee>'
  201.                 '<Permission>READ</Permission>'
  202.                 '</Grant>'
  203.                 '</AccessControlList>'
  204.                 '<AccessControlList>'
  205.                 '<Grant>'
  206.                 '<Grantee xmlns:xsi="http://www.w3.org/2001/'
  207.                 'XMLSchema-instance" xsi:type="Group">'
  208.                 '<URI>http://acs.amazonaws.com/groups/global/AllUsers</URI>'
  209.                 '</Grantee>'
  210.                 '<Permission>WRITE</Permission>'
  211.                 '</Grant>'
  212.                 '</AccessControlList>'
  213.                 '</AccessControlPolicy>' %
  214.                 (account_name, account_name, account_name, account_name))
  215.     else:
  216.         body = ('<AccessControlPolicy>'
  217.                 '<Owner>'
  218.                 '<ID>%s</ID>'
  219.                 '<DisplayName>%s</DisplayName>'
  220.                 '</Owner>'
  221.                 '<AccessControlList>'
  222.                 '<Grant>'
  223.                 '<Grantee xmlns:xsi="http://www.w3.org/2001/'
  224.                 'XMLSchema-instance" xsi:type="CanonicalUser">'
  225.                 '<ID>%s</ID>'
  226.                 '<DisplayName>%s</DisplayName>'
  227.                 '</Grantee>'
  228.                 '<Permission>FULL_CONTROL</Permission>'
  229.                 '</Grant>'
  230.                 '</AccessControlList>'
  231.                 '</AccessControlPolicy>' %
  232.                 (account_name, account_name, account_name, account_name))
  233.     return Response(body=body, content_type="text/plain")
  234.  
  235. def swift_acl_translate(acl, group='', user='', xml=False):
  236.     """
  237.    Takes an S3 style ACL and returns a list of header/value pairs that
  238.    implement that ACL in Swift, or "Unsupported" if there isn't a way to do
  239.    that yet.
  240.    """
  241.     swift_acl = {}
  242.     swift_acl['public-read'] = [['HTTP_X_CONTAINER_READ', '.r:*,.rlistings']]
  243.     # Swift does not support public write:
  244.     # https://answers.launchpad.net/swift/+question/169541
  245.     swift_acl['public-read-write'] = [['HTTP_X_CONTAINER_WRITE', '.r:*'],
  246.                                       ['HTTP_X_CONTAINER_READ',
  247.                                        '.r:*,.rlistings']]
  248.  
  249.     #TODO: if there's a way to get group and user, this should work for
  250.     # private:
  251.     #swift_acl['private'] = [['HTTP_X_CONTAINER_WRITE',  group + ':' + user], \
  252.     #                  ['HTTP_X_CONTAINER_READ', group + ':' + user]]
  253.     swift_acl['private'] = [['HTTP_X_CONTAINER_WRITE', '.'],
  254.                             ['HTTP_X_CONTAINER_READ', '.']]
  255.     if xml:
  256.         # We are working with XML and need to parse it
  257.         dom = parseString(acl)
  258.         acl = 'unknown'
  259.         for grant in dom.getElementsByTagName('Grant'):
  260.             permission = grant.getElementsByTagName('Permission')[0]\
  261.                 .firstChild.data
  262.             grantee = grant.getElementsByTagName('Grantee')[0]\
  263.                 .getAttributeNode('xsi:type').nodeValue
  264.             if permission == "FULL_CONTROL" and grantee == 'CanonicalUser' and\
  265.                     acl != 'public-read' and acl != 'public-read-write':
  266.                 acl = 'private'
  267.             elif permission == "READ" and grantee == 'Group' and\
  268.                     acl != 'public-read-write':
  269.                 acl = 'public-read'
  270.             elif permission == "WRITE" and grantee == 'Group':
  271.                 acl = 'public-read-write'
  272.             else:
  273.                 acl = 'unsupported'
  274.     if acl == 'authenticated-read':
  275.         return "Unsupported"
  276.     elif acl not in swift_acl:
  277.         return "InvalidArgument"
  278.  
  279.     return swift_acl[acl]
  280.  
  281. def canonical_string(req, slogger, alt=False):
  282.     """
  283.    Canonicalize a request to a token that can be signed.
  284.    """
  285.     amz_headers = {}
  286.  
  287.     buf = "%s\n%s\n%s\n" % (req.method, req.headers.get('Content-MD5', ''),
  288.             req.headers.get('Content-Type') or '')
  289.  
  290.     for amz_header in sorted((key.lower() for key in req.headers
  291.                               if key.lower().startswith('x-amz-'))):
  292.         amz_headers[amz_header] = req.headers[amz_header]
  293.  
  294.     if 'x-amz-date' in amz_headers:
  295.         buf += "\n"
  296.     elif 'Date' in req.headers:
  297.         buf += "%s\n" % req.headers['Date']
  298.  
  299.     for k in sorted(key.lower() for key in amz_headers):
  300.         buf += "%s:%s\n" % (k, amz_headers[k])
  301.  
  302.     path = req.path_qs
  303.     args = ''
  304.  
  305.     slogger.debug("QUERY STRING PATH: %s" % (path))
  306.  
  307.     if '?' in path:
  308.         path, args = path.split('?', 1)
  309.     segs = path.split('/')
  310.     if len(segs) > 2 and segs[2]:  # segs[2] is object name
  311.         # We doing this for replace '/' with %2F, because by default quote
  312.         # don't replace '/' with %2F
  313.         object_name = quote(unquote('/'.join(segs[2:])), safe='')
  314.         # Generate alternate token
  315.         if (alt):
  316.           path = '/'.join(segs[:2] + [object_name])
  317.  
  318.     if args:
  319.         for key in urlparse.parse_qs(args, keep_blank_values=True):
  320.              if key in ('acl', 'logging', 'torrent', 'location',
  321.                         'requestPayment'):
  322.                 return "%s%s?%s" % (buf, path, key)
  323.  
  324.     slogger.debug("Canonical_string: %s%s" % (buf,path))
  325.     return (buf + path)
  326.  
  327.  
  328. class ServiceController(WSGIContext):
  329.     """
  330.    Handles account level requests.
  331.    """
  332.     def __init__(self, env, app, conf, account_name, token, **kwargs):
  333.         WSGIContext.__init__(self, app)
  334.         self.conf = conf
  335.         self.logger = get_logger(self.conf, log_route='swift3')
  336.         env['HTTP_X_AUTH_TOKEN'] = token[0]
  337.         env['HTTP_X_AUTH_TOKEN_ALT'] = token[1]
  338.         env['PATH_INFO'] = '/v1/%s' % account_name
  339.  
  340.     def GET(self, env, start_response):
  341.         """
  342.        Handle GET Service request
  343.        """
  344.         env['QUERY_STRING'] = 'format=json'
  345.         body_iter = self._app_call(env)
  346.         status = self._get_status_int()
  347.  
  348.         if status != HTTP_OK:
  349.             if status == HTTP_UNAUTHORIZED:
  350.                 return get_err_response('AccessDenied')
  351.             else:
  352.                 return get_err_response('InvalidURI')
  353.  
  354.         containers = loads(''.join(list(body_iter)))
  355.         # we don't keep the creation time of a backet (s3cmd doesn't
  356.         # work without that) so we use something bogus.
  357.         body = '<?xml version="1.0" encoding="UTF-8"?>' \
  358.             '<ListAllMyBucketsResult ' \
  359.               'xmlns="http://doc.s3.amazonaws.com/2006-03-01">' \
  360.             '<Buckets>%s</Buckets>' \
  361.             '</ListAllMyBucketsResult>' \
  362.             % ("".join(['<Bucket><Name>%s</Name><CreationDate>' \
  363.                          '2009-02-03T16:45:09.000Z</CreationDate></Bucket>' %
  364.                          xml_escape(i['name']) for i in containers]))
  365.         resp = Response(status=HTTP_OK, content_type='application/xml',
  366.             body=body)
  367.         return resp
  368.  
  369.  
  370. class BucketController(WSGIContext):
  371.     """
  372.     Handles bucket request.
  373.    """
  374.     def __init__(self, env, app, conf, account_name, token, container_name,
  375.                     **kwargs):
  376.         WSGIContext.__init__(self, app)
  377.         self.container_name = unquote(container_name)
  378.         self.account_name = unquote(account_name)
  379.         self.conf = conf
  380.         self.logger = get_logger(self.conf, log_route='swift3')
  381.         env['HTTP_X_AUTH_TOKEN'] = token[0]
  382.         env['HTTP_X_AUTH_TOKEN_ALT'] = token[1]
  383.         env['PATH_INFO'] = '/v1/%s/%s' % (account_name, container_name)
  384.  
  385.     def GET(self, env, start_response):
  386.         """
  387.        Handle GET Bucket (List Objects) request
  388.        """
  389.         if 'QUERY_STRING' in env:
  390.             args = dict(urlparse.parse_qsl(env['QUERY_STRING'], 1))
  391.         else:
  392.             args = {}
  393.         max_keys = min(int(args.get('max-keys', MAX_BUCKET_LISTING)),
  394.                         MAX_BUCKET_LISTING)
  395.         env['QUERY_STRING'] = 'format=json&limit=%s' % (max_keys + 1)
  396.         if 'marker' in args:
  397.             env['QUERY_STRING'] += '&marker=%s' % quote(args['marker'])
  398.         if 'prefix' in args:
  399.             env['QUERY_STRING'] += '&prefix=%s' % quote(args['prefix'])
  400.         if 'delimiter' in args:
  401.             env['QUERY_STRING'] += '&delimiter=%s' % quote(args['delimiter'])
  402.         body_iter = self._app_call(env)
  403.         status = self._get_status_int()
  404.         headers = dict(self._response_headers)
  405.  
  406.         if status != HTTP_OK:
  407.             if status == HTTP_UNAUTHORIZED:
  408.                 return get_err_response('AccessDenied')
  409.             elif status == HTTP_NOT_FOUND:
  410.                 return get_err_response('NoSuchBucket')
  411.             else:
  412.                 return get_err_response('InvalidURI')
  413.  
  414.         if 'acl' in args:
  415.             return get_acl(self.account_name, headers)
  416.  
  417.         objects = loads(''.join(list(body_iter)))
  418.         body = ('<?xml version="1.0" encoding="UTF-8"?>'
  419.             '<ListBucketResult '
  420.                 'xmlns="http://s3.amazonaws.com/doc/2006-03-01">'
  421.             '<Prefix>%s</Prefix>'
  422.             '<Marker>%s</Marker>'
  423.             '<Delimiter>%s</Delimiter>'
  424.             '<IsTruncated>%s</IsTruncated>'
  425.             '<MaxKeys>%s</MaxKeys>'
  426.             '<Name>%s</Name>'
  427.             '%s'
  428.             '%s'
  429.             '</ListBucketResult>' %
  430.             (
  431.                 xml_escape(args.get('prefix', '')),
  432.                 xml_escape(args.get('marker', '')),
  433.                 xml_escape(args.get('delimiter', '')),
  434.                 'true' if len(objects) == (max_keys + 1) else 'false',
  435.                 max_keys,
  436.                 xml_escape(self.container_name),
  437.                 "".join(['<Contents><Key>%s</Key><LastModified>%sZ</LastModif'\
  438.                         'ied><ETag>%s</ETag><Size>%s</Size><StorageClass>STA'\
  439.                         'NDARD</StorageClass></Contents>' %
  440.                         (xml_escape(i['name']), i['last_modified'], i['hash'],
  441.                            i['bytes'])
  442.                            for i in objects[:max_keys] if 'subdir' not in i]),
  443.                 "".join(['<CommonPrefixes><Prefix>%s</Prefix></CommonPrefixes>'
  444.                          % xml_escape(i['subdir'])
  445.                          for i in objects[:max_keys] if 'subdir' in i])))
  446.         return Response(body=body, content_type='application/xml')
  447.  
  448.     def PUT(self, env, start_response):
  449.         """
  450.        Handle PUT Bucket request
  451.        """
  452.         if 'HTTP_X_AMZ_ACL' in env:
  453.             amz_acl = env['HTTP_X_AMZ_ACL']
  454.             # Translate the Amazon ACL to something that can be
  455.             # implemented in Swift, 501 otherwise. Swift uses POST
  456.             # for ACLs, whereas S3 uses PUT.
  457.             del env['HTTP_X_AMZ_ACL']
  458.             if 'QUERY_STRING' in env:
  459.                 del env['QUERY_STRING']
  460.  
  461.             translated_acl = swift_acl_translate(amz_acl)
  462.             if translated_acl == 'Unsupported':
  463.                 return get_err_response('Unsupported')
  464.             elif translated_acl == 'InvalidArgument':
  465.                 return get_err_response('InvalidArgument')
  466.  
  467.             for header, acl in translated_acl:
  468.                 env[header] = acl
  469.  
  470.         if 'CONTENT_LENGTH' in env:
  471.             content_length = env['CONTENT_LENGTH']
  472.             try:
  473.                 content_length = int(content_length)
  474.             except (ValueError, TypeError):
  475.                 return get_err_response('InvalidArgument')
  476.             if content_length < 0:
  477.                 return get_err_response('InvalidArgument')
  478.  
  479.         if 'QUERY_STRING' in env:
  480.             args = dict(urlparse.parse_qsl(env['QUERY_STRING'], 1))
  481.             if 'acl' in args:
  482.                 # We very likely have an XML-based ACL request.
  483.                 body = env['wsgi.input'].readline().decode()
  484.                 # Some ACL debugging
  485.                 translated_acl = swift_acl_translate(body, xml=True)
  486.                 if translated_acl == 'Unsupported':
  487.                     return get_err_response('Unsupported')
  488.                 elif translated_acl == 'InvalidArgument':
  489.                     return get_err_response('InvalidArgument')
  490.                 for header, acl in translated_acl:
  491.                     env[header] = acl
  492.                 env['REQUEST_METHOD'] = 'POST'
  493.  
  494.         body_iter = self._app_call(env)
  495.         status = self._get_status_int()
  496.  
  497.         # Changing an ACL on a bucket requires a PUT that swift responds with a HTTP_NO_CONTENT
  498.         # This is a legitimate response when just changing the perms on a bucket that already
  499.         # exists and should respond with a HTTP_OK according to the S3 API.
  500.         if status != HTTP_CREATED and status != HTTP_NO_CONTENT and status != HTTP_ACCEPTED:
  501.             if status == HTTP_UNAUTHORIZED:
  502.                 return get_err_response('AccessDenied')
  503. #            elif status == HTTP_ACCEPTED:
  504. #                return get_err_response('BucketAlreadyExists')
  505.             else:
  506.                 return get_err_response('InvalidURI: %s' % HTTP_ACCEPTED)
  507.  
  508.         resp = Response()
  509.         resp.headers.add('Location', self.container_name)
  510.         resp.status = HTTP_OK
  511.         return resp
  512.  
  513.     def DELETE(self, env, start_response):
  514.         """
  515.        Handle DELETE Bucket request
  516.        """
  517.         body_iter = self._app_call(env)
  518.         status = self._get_status_int()
  519.  
  520.         if status != HTTP_NO_CONTENT:
  521.             if status == HTTP_UNAUTHORIZED:
  522.                 return get_err_response('AccessDenied')
  523.             elif status == HTTP_NOT_FOUND:
  524.                 return get_err_response('NoSuchBucket')
  525.             elif status == HTTP_CONFLICT:
  526.                 return get_err_response('BucketNotEmpty')
  527.             else:
  528.                 return get_err_response('InvalidURI')
  529.  
  530.         resp = Response()
  531.         resp.status = HTTP_NO_CONTENT
  532.         return resp
  533.  
  534.  
  535. class ObjectController(WSGIContext):
  536.     """
  537.    Handles requests on objects
  538.    """
  539.     def __init__(self, env, app, conf, account_name, token, container_name,
  540.                     object_name, **kwargs):
  541.         WSGIContext.__init__(self, app)
  542.         self.account_name = unquote(account_name)
  543.         self.container_name = unquote(container_name)
  544.         self.object_name = unquote(object_name)
  545.         self.conf = conf
  546.         self.logger = get_logger(self.conf, log_route='swift3')
  547.         env['HTTP_X_AUTH_TOKEN'] = token[0]
  548.         env['HTTP_X_AUTH_TOKEN_ALT'] = token[1]
  549.         env['PATH_INFO'] = '/v1/%s/%s/%s' % (account_name, container_name,
  550.                                              self.object_name)
  551.         self.logger.debug("Object env: %s" % (env))
  552.  
  553.     def GETorHEAD(self, env, start_response):
  554.         if env['REQUEST_METHOD'] == 'HEAD':
  555.             head = True
  556.             env['REQUEST_METHOD'] = 'GET'
  557.         else:
  558.             head = False
  559.  
  560.         #env['HTTP_ACCEPT_ENCODING'] = 'gzip,deflate'
  561.         app_iter = self._app_call(env)
  562.  
  563.         if head:
  564.             app_iter = None
  565.  
  566.         status = self._get_status_int()
  567.         headers = dict(self._response_headers)
  568.  
  569.         self.logger.debug("status: %s, headers: %s, body: %s" % (status, headers, dir(self._start_response)))
  570.  
  571.         if is_success(status):
  572.             if 'QUERY_STRING' in env:
  573.                 args = dict(urlparse.parse_qsl(env['QUERY_STRING'], 1))
  574.             else:
  575.                 args = {}
  576.             if 'acl' in args:
  577.                 return get_acl(self.account_name, headers)
  578.  
  579.             new_hdrs = {}
  580.             for key, val in headers.iteritems():
  581.                 _key = key.lower()
  582.                 if _key.startswith('x-object-meta-'):
  583.                     new_hdrs['x-amz-meta-' + key[14:]] = val
  584.                 elif _key in ('content-length', 'content-type',
  585.                               'content-range', 'content-encoding',
  586.                               'etag', 'last-modified'):
  587.                     new_hdrs[key] = val
  588.             return Response(status=status, headers=new_hdrs, app_iter=app_iter)
  589.         elif status == HTTP_UNAUTHORIZED:
  590.             return get_err_response('AccessDenied')
  591.         elif status == HTTP_NOT_FOUND:
  592.             return get_err_response('NoSuchKey')
  593.         else:
  594.             return get_err_response('InvalidURI')
  595.  
  596.     def HEAD(self, env, start_response):
  597.         """
  598.        Handle HEAD Object request
  599.        """
  600.         return self.GETorHEAD(env, start_response)
  601.  
  602.     def GET(self, env, start_response):
  603.         """
  604.        Handle GET Object request
  605.        """
  606.         return self.GETorHEAD(env, start_response)
  607.  
  608.     def PUT(self, env, start_response):
  609.         """
  610.        Handle PUT Object and PUT Object (Copy) request
  611.        """
  612.         for key, value in env.items():
  613.             if key.startswith('HTTP_X_AMZ_META_'):
  614.                 del env[key]
  615.                 env['HTTP_X_OBJECT_META_' + key[16:]] = value
  616.             elif key == 'HTTP_CONTENT_MD5':
  617.                 env['HTTP_ETAG'] = value.decode('base64').encode('hex')
  618.             elif key == 'HTTP_X_AMZ_COPY_SOURCE':
  619.                 env['HTTP_X_COPY_FROM'] = value
  620.  
  621.         body_iter = self._app_call(env)
  622.         status = self._get_status_int()
  623.  
  624.         if status != HTTP_CREATED:
  625.             if status == HTTP_UNAUTHORIZED:
  626.                 return get_err_response('AccessDenied')
  627.             elif status == HTTP_NOT_FOUND:
  628.                 return get_err_response('NoSuchBucket')
  629.             elif status == HTTP_UNPROCESSABLE_ENTITY:
  630.                 return get_err_response('InvalidDigest')
  631.             else:
  632.                 return get_err_response('InvalidURI')
  633.  
  634.         if 'HTTP_X_COPY_FROM' in env:
  635.             last_modified = datetime.datetime.strptime(self._response_header_value('Last-Modified'), "%a, %d %b %Y %H:%M:%S GMT")
  636.             body = '<CopyObjectResult>' \
  637.                    '<ETag>"%s"</ETag>' \
  638.                    '<LastModified>%s</LastModified>' \
  639.                    '</CopyObjectResult>' % (self._response_header_value('etag'), last_modified.isoformat())
  640.             return Response(status=HTTP_OK, body=body)
  641.  
  642.         return Response(status=200, etag=self._response_header_value('etag'))
  643.  
  644.     def DELETE(self, env, start_response):
  645.         """
  646.        Handle DELETE Object request
  647.        """
  648.         body_iter = self._app_call(env)
  649.         status = self._get_status_int()
  650.  
  651.         if status != HTTP_NO_CONTENT:
  652.             if status == HTTP_UNAUTHORIZED:
  653.                 return get_err_response('AccessDenied')
  654.             elif status == HTTP_NOT_FOUND:
  655.                 return get_err_response('NoSuchKey')
  656.             else:
  657.                 return get_err_response('InvalidURI')
  658.  
  659.         resp = Response()
  660.         resp.status = HTTP_NO_CONTENT
  661.         return resp
  662.  
  663.  
  664. class Swift3Middleware(object):
  665.     """Swift3 S3 compatibility midleware"""
  666.     def __init__(self, app, conf, *args, **kwargs):
  667.         self.app = app
  668.         self.conf = conf
  669.         self.logger = get_logger(self.conf, log_route='swift3')
  670.  
  671.     def get_controller(self, path):
  672.         container, obj = split_path(path, 0, 2, True)
  673.         d = dict(container_name=container, object_name=obj)
  674.  
  675.         if container and obj:
  676.             return ObjectController, d
  677.         elif container:
  678.             return BucketController, d
  679.         return ServiceController, d
  680.  
  681.     def __call__(self, env, start_response):
  682.         req = Request(env)
  683.  
  684.         if 'AWSAccessKeyId' in req.GET:
  685.             try:
  686.                 req.headers['Date'] = req.GET['Expires']
  687.                 req.headers['Authorization'] = \
  688.                     'AWS %(AWSAccessKeyId)s:%(Signature)s' % req.GET
  689.             except KeyError:
  690.                 return get_err_response('InvalidArgument')(env, start_response)
  691.  
  692.         if not 'Authorization' in req.headers:
  693.             return self.app(env, start_response)
  694.  
  695.         try:
  696.             keyword, info = req.headers['Authorization'].split(' ')
  697.         except:
  698.             return get_err_response('AccessDenied')(env, start_response)
  699.  
  700.         if keyword != 'AWS':
  701.             return get_err_response('AccessDenied')(env, start_response)
  702.  
  703.         try:
  704.             account, signature = info.rsplit(':', 1)
  705.         except:
  706.             return get_err_response('InvalidArgument')(env, start_response)
  707.  
  708.         try:
  709.             controller, path_parts = self.get_controller(req.path)
  710.             #controller, path_parts = self.get_controller(env['PATH_INFO'])
  711.         except ValueError:
  712.             return get_err_response('InvalidURI')(env, start_response)
  713.  
  714.         if 'Date' in req.headers:
  715.             date = email.utils.parsedate(req.headers['Date'])
  716.             if date == None:
  717.                 return get_err_response('AccessDenied')(env, start_response)
  718.  
  719.             d1 = datetime.datetime(*date[0:6])
  720.             d2 = datetime.datetime.utcnow()
  721.             epoch = datetime.datetime(1970, 1, 1, 0, 0, 0, 0)
  722.  
  723.             if d1 < epoch:
  724.                 return get_err_response('AccessDenied')(env, start_response)
  725.  
  726.             delta = datetime.timedelta(seconds=60 * 10)
  727.             if d1 - d2 > delta or d2 - d1 > delta:
  728.                 return get_err_response('RequestTimeTooSkewed')(env, start_response)
  729.  
  730.         token = (base64.urlsafe_b64encode(canonical_string(req, self.logger)),
  731.                   base64.urlsafe_b64encode(canonical_string(req, self.logger, alt=True)))
  732.  
  733.         self.logger.debug("s3 tokens: %s %s" % (token[0], token[1]))
  734.  
  735.         controller = controller(env, self.app, self.conf, account, token, **path_parts)
  736.  
  737.         if hasattr(controller, req.method):
  738.             res = getattr(controller, req.method)(env, start_response)
  739.         else:
  740.             return get_err_response('InvalidURI')(env, start_response)
  741.  
  742.         return res(env, start_response)
  743.  
  744.  
  745. def filter_factory(global_conf, **local_conf):
  746.     """Standard filter factory to use the middleware with paste.deploy"""
  747.     conf = global_conf.copy()
  748.     conf.update(local_conf)
  749.  
  750.     def swift3_filter(app):
  751.         return Swift3Middleware(app, conf)
  752.  
  753.     return swift3_filter
Add Comment
Please, Sign In to add comment