middleware.py 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225
  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. SwiftS3 middleware emulates AWS S3 REST api on top of Swift.
  17. The following opperations are currently supported:
  18. * GET Service
  19. * DELETE Bucket (Delete bucket; abort running MPUs)
  20. * GET Bucket (List Objects; List in-progress multipart uploads)
  21. * PUT Bucket
  22. * DELETE Object
  23. * GET Object
  24. * HEAD Object
  25. * PUT Object
  26. * PUT Object (Copy)
  27. To add this middleware to your configuration, add the swifts3 middleware
  28. before the auth middleware and before any other middleware that
  29. waits for swift requests (like rate limiting).
  30. To set up your client, the access key will be the concatenation of the
  31. account and user strings that should look like test:tester, and the
  32. secret access key is the account password. The host should also point
  33. to the swift storage hostname and it should use the old style
  34. calling format, not the hostname based container format.
  35. An example client using the python boto library might look like the
  36. following for a SAIO setup:
  37. connection = boto.s3.Connection(
  38. aws_access_key_id='test:tester',
  39. aws_secret_access_key='testing',
  40. port=8080,
  41. host='127.0.0.1',
  42. is_secure=False,
  43. calling_format=boto.s3.connection.OrdinaryCallingFormat())
  44. Note that all the operations with multipart upload buckets are denied
  45. to user, as well as multipart buckets are not listed in all buckets list.
  46. In case of GET/DELETE - NoSuchBucket error is returned;
  47. In case of PUT/POST - InvalidBucketName error is returned.
  48. """
  49. from urllib import unquote, quote
  50. import rfc822
  51. import hmac
  52. import base64
  53. import uuid
  54. import errno
  55. from xml.sax.saxutils import escape as xml_escape
  56. import urlparse
  57. from webob import Request as WebObRequest, Response
  58. from webob.exc import HTTPNotFound
  59. from webob.multidict import MultiDict
  60. import simplejson as json
  61. from swift.common.utils import split_path, get_logger
  62. #XXX: In webob-1.9b copied environment contained link to the original
  63. # instance of a TrackableMultiDict which reflects to original
  64. # request.
  65. class Request(WebObRequest):
  66. def _remove_query_vars(self):
  67. if 'webob._parsed_query_vars' in self.environ:
  68. del self.environ['webob._parsed_query_vars']
  69. def copy(self):
  70. req = super(Request, self).copy()
  71. req._remove_query_vars()
  72. return req
  73. def copy_get(self):
  74. req = super(Request, self).copy_get()
  75. req._remove_query_vars()
  76. return req
  77. MAX_BUCKET_LISTING = 1000
  78. MAX_UPLOADS_LISTING = 1000
  79. MULTIPART_UPLOAD_PREFIX = 'MPU.'
  80. # List of Query String Arguments of Interest
  81. qsa_of_interest = ['acl', 'defaultObjectAcl', 'location', 'logging',
  82. 'partNumber', 'policy', 'requestPayment', 'torrent',
  83. 'versioning', 'versionId', 'versions', 'website',
  84. 'uploads', 'uploadId', 'response-content-type',
  85. 'response-content-language', 'response-expires',
  86. 'response-cache-control', 'response-content-disposition',
  87. 'response-content-encoding', 'delete', 'lifecycle']
  88. def get_err_response(code):
  89. """
  90. Creates a properly formatted xml error response by a
  91. given HTTP response code,
  92. :param code: error code
  93. :returns: webob.response object
  94. """
  95. error_table = {
  96. 'AccessDenied':
  97. (403, 'Access denied'),
  98. 'BucketAlreadyExists':
  99. (409, 'The requested bucket name is not available'),
  100. 'BucketNotEmpty':
  101. (409, 'The bucket you tried to delete is not empty'),
  102. 'InvalidArgument':
  103. (400, 'Invalid Argument'),
  104. 'InvalidBucketName':
  105. (400, 'The specified bucket is not valid'),
  106. 'InvalidURI':
  107. (400, 'Could not parse the specified URI'),
  108. 'NoSuchBucket':
  109. (404, 'The specified bucket does not exist'),
  110. 'SignatureDoesNotMatch':
  111. (403, 'The calculated request signature does not match '\
  112. 'your provided one'),
  113. 'NoSuchKey':
  114. (404, 'The resource you requested does not exist'),
  115. 'NoSuchUpload':
  116. (404, 'The specified multipart upload does not exist.'),
  117. }
  118. resp = Response(content_type='text/xml')
  119. resp.status = error_table[code][0]
  120. resp.body = error_table[code][1]
  121. resp.body = '<?xml version="1.0" encoding="UTF-8"?>\r\n<Error>\r\n ' \
  122. '<Code>%s</Code>\r\n <Message>%s</Message>\r\n</Error>\r\n' \
  123. % (code, error_table[code][1])
  124. return resp
  125. def get_acl(account_name):
  126. body = ('<AccessControlPolicy>'
  127. '<Owner>'
  128. '<ID>%s</ID>'
  129. '</Owner>'
  130. '<AccessControlList>'
  131. '<Grant>'
  132. '<Grantee xmlns:xsi="http://www.w3.org/2001/'\
  133. 'XMLSchema-instance" xsi:type="CanonicalUser">'
  134. '<ID>%s</ID>'
  135. '</Grantee>'
  136. '<Permission>FULL_CONTROL</Permission>'
  137. '</Grant>'
  138. '</AccessControlList>'
  139. '</AccessControlPolicy>' %
  140. (account_name, account_name))
  141. return Response(body=body, content_type="text/plain")
  142. def canonical_string(req):
  143. """
  144. Canonicalize a request to a token that can be signed.
  145. """
  146. def unquote_v(nv):
  147. if len(nv) == 1:
  148. return nv
  149. else:
  150. return (nv[0], unquote(nv[1]))
  151. amz_headers = {}
  152. buf = "%s\n%s\n%s\n" % (req.method, req.headers.get('Content-MD5', ''),
  153. req.headers.get('Content-Type') or '')
  154. for amz_header in sorted((key.lower() for key in req.headers
  155. if key.lower().startswith('x-amz-'))):
  156. amz_headers[amz_header] = req.headers[amz_header]
  157. if 'x-amz-date' in amz_headers:
  158. buf += "\n"
  159. elif 'Date' in req.headers:
  160. buf += "%s\n" % req.headers['Date']
  161. for k in sorted(key.lower() for key in amz_headers):
  162. buf += "%s:%s\n" % (k, amz_headers[k])
  163. # don't include anything after the first ? in the resource...
  164. # unless it is one of the QSA of interest, defined above
  165. parts = req.path_qs.split('?')
  166. buf += parts[0]
  167. if len(parts) > 1:
  168. qsa = parts[1].split('&')
  169. qsa = [a.split('=', 1) for a in qsa]
  170. qsa = [unquote_v(a) for a in qsa if a[0] in qsa_of_interest]
  171. if len(qsa) > 0:
  172. qsa.sort(cmp=lambda x, y: cmp(x[0], y[0]))
  173. qsa = ['='.join(a) for a in qsa]
  174. buf += '?'
  175. buf += '&'.join(qsa)
  176. return buf
  177. def check_container_name_no_such_bucket_error(container_name):
  178. """Checks that user do not tries to operate with MPU container"""
  179. if container_name.startswith(MULTIPART_UPLOAD_PREFIX):
  180. return get_err_response('NoSuchBucket')
  181. def check_container_name_invalid_bucket_name_error(container_name):
  182. """Checks that user do not tries to operate with MPU container"""
  183. if container_name.startswith(MULTIPART_UPLOAD_PREFIX):
  184. return get_err_response('InvalidBucketName')
  185. def meta_request_head(req, meta_path, app):
  186. """
  187. HEAD request to check that meta file presents and
  188. multipart upload is in progress.
  189. """
  190. meta_req = req.copy()
  191. meta_req.method = 'HEAD'
  192. meta_req.body = ''
  193. meta_req.upath_info = meta_path
  194. meta_req.GET.clear()
  195. return meta_req.get_response(app)
  196. class ServiceController(object):
  197. """
  198. Handles account level requests.
  199. """
  200. def __init__(self, env, app, account_name, token, **kwargs):
  201. self.app = app
  202. env['HTTP_X_AUTH_TOKEN'] = token
  203. env['PATH_INFO'] = '/v1/%s' % account_name
  204. def GET(self, req):
  205. """
  206. Handle GET Service request
  207. """
  208. req.GET.clear()
  209. req.GET['format'] = 'json'
  210. resp = req.get_response(self.app)
  211. status = resp.status_int
  212. body = resp.body
  213. if status != 200:
  214. if status == 401:
  215. return get_err_response('AccessDenied')
  216. else:
  217. return get_err_response('InvalidURI')
  218. containers = json.loads(body)
  219. # we don't keep the creation time of a bucket (s3cmd doesn't
  220. # work without that) so we use some bogus
  221. body = '<?xml version="1.0" encoding="UTF-8"?>' \
  222. '<ListAllMyBucketsResult ' \
  223. 'xmlns="http://s3.amazonaws.com/doc/2006-03-01/">' \
  224. '<Buckets>%s</Buckets>' \
  225. '</ListAllMyBucketsResult>' \
  226. % ("".join(['<Bucket><Name>%s</Name><CreationDate>' \
  227. '2009-02-03T16:45:09.000Z</CreationDate></Bucket>' %
  228. xml_escape(i['name']) for i in containers if \
  229. not i['name'].startswith(MULTIPART_UPLOAD_PREFIX)]))
  230. # we shold not show multipart buckets here
  231. return Response(status=200, content_type='application/xml', body=body)
  232. class BucketController(object):
  233. """
  234. Handles bucket requests.
  235. """
  236. def __init__(self, env, app, account_name, token, container_name,
  237. **kwargs):
  238. self.app = app
  239. self.container_name = unquote(container_name)
  240. self.account_name = unquote(account_name)
  241. env['HTTP_X_AUTH_TOKEN'] = token
  242. env['PATH_INFO'] = '/v1/%s/%s' % (account_name, container_name)
  243. def get_uploads(self, req):
  244. """Handles listing of in-progress multipart uploads"""
  245. acl = req.GET.get('acl')
  246. params = MultiDict([('format', 'json')])
  247. max_uploads = req.GET.get('max-uploads')
  248. if (max_uploads is not None and max_uploads.isdigit()):
  249. max_uploads = min(int(max_uploads), MAX_UPLOADS_LISTING)
  250. else:
  251. max_uploads = MAX_UPLOADS_LISTING
  252. params['limit'] = str(max_uploads + 1)
  253. for param_name in ('key-marker', 'prefix', 'delimiter',
  254. 'upload-id-marker'):
  255. if param_name in req.GET:
  256. params[param_name] = req.GET[param_name]
  257. cont_name = MULTIPART_UPLOAD_PREFIX + self.container_name
  258. cont_path = "/v1/%s/%s/" % (self.account_name, cont_name)
  259. req.upath_info = cont_path
  260. req.GET.clear()
  261. req.GET.update(params)
  262. resp = req.get_response(self.app)
  263. status = resp.status_int
  264. if status != 200:
  265. if status == 401:
  266. return get_err_response('AccessDenied')
  267. elif status == 404:
  268. return get_err_response('InvalidBucketName')
  269. else:
  270. return get_err_response('InvalidURI')
  271. if acl is not None:
  272. return get_acl(self.account_name)
  273. objects = json.loads(resp.body)
  274. uploads = ''
  275. splited_name = ''
  276. for obj in objects:
  277. if obj['name'].endswith('/meta'):
  278. splited_name = obj['name'].split('/')
  279. uploads = uploads.join(
  280. "<Upload>"
  281. "<Key>%s</Key>"
  282. "<UploadId>%s</UploadId>"
  283. "<Initiator>"
  284. "<ID>%s</ID>"
  285. "<DisplayName>%s</DisplayName>"
  286. "</Initiator>"
  287. "<Owner>"
  288. "<ID>%s</ID>"
  289. "<DisplayName>%s</DisplayName>"
  290. "</Owner>"
  291. "<StorageClass>STANDARD</StorageClass>"
  292. "<Initiated>%sZ</Initiated>"
  293. "</Upload>" % (
  294. splited_name[0],
  295. splited_name[1],
  296. self.account_name,
  297. self.account_name,
  298. self.account_name,
  299. self.account_name,
  300. obj['last_modified'][:-3]))
  301. else:
  302. objects.remove(obj)
  303. #TODO: Currently there are less then max_uploads results
  304. # in a response; Amount of uploads == amount of meta files
  305. # received in a request for a list of objects in a bucket.
  306. if len(objects) == (max_uploads + 1):
  307. is_truncated = 'true'
  308. next_key_marker = splited_name[0]
  309. next_uploadId_marker = splited_name[1]
  310. else:
  311. is_truncated = 'false'
  312. next_key_marker = next_uploadId_marker = ''
  313. body = ('<?xml version="1.0" encoding="UTF-8"?>'
  314. '<ListMultipartUploadsResult '
  315. 'xmlns="http://s3.amazonaws.com/doc/2006-03-01/">'
  316. '<Bucket>%s</Bucket>'
  317. '<KeyMarker>%s</KeyMarker>'
  318. '<UploadIdMarker>%s</UploadIdMarker>'
  319. '<NextKeyMarker>%s</NextKeyMarker>'
  320. '<NextUploadIdMarker>%s</NextUploadIdMarker>'
  321. '<MaxUploads>%s</MaxUploads>'
  322. '<IsTruncated>%s</IsTruncated>'
  323. '%s'
  324. '</ListMultipartUploadsResult>' %
  325. (
  326. xml_escape(self.container_name),
  327. xml_escape(params.get('key-marker', '')),
  328. xml_escape(params.get('upload-id-marker', '')),
  329. next_key_marker,
  330. next_uploadId_marker,
  331. max_uploads,
  332. is_truncated,
  333. uploads
  334. )
  335. )
  336. return Response(body=body, content_type='application/xml')
  337. def GET(self, req):
  338. """
  339. Handles listing of in-progress multipart uploads,
  340. handles list objects request.
  341. """
  342. # any operations with multipart buckets are not allowed to user
  343. check_container_name_no_such_bucket_error(self.container_name)
  344. if 'uploads' in req.GET:
  345. return self.get_uploads(req)
  346. else:
  347. acl = req.GET.get('acl')
  348. params = MultiDict([('format', 'json')])
  349. max_keys = req.GET.get('max-keys')
  350. if (max_keys is not None and max_keys.isdigit()):
  351. max_keys = min(int(max_keys), MAX_BUCKET_LISTING)
  352. else:
  353. max_keys = MAX_BUCKET_LISTING
  354. params['limit'] = str(max_keys + 1)
  355. for param_name in ('marker', 'prefix', 'delimiter'):
  356. if param_name in req.GET:
  357. params[param_name] = req.GET[param_name]
  358. req.GET.clear()
  359. req.GET.update(params)
  360. resp = req.get_response(self.app)
  361. status = resp.status_int
  362. body = resp.body
  363. if status != 200:
  364. if status == 401:
  365. return get_err_response('AccessDenied')
  366. elif status == 404:
  367. return get_err_response('InvalidBucketName')
  368. else:
  369. return get_err_response('InvalidURI')
  370. if acl is not None:
  371. return get_acl(self.account_name)
  372. objects = json.loads(resp.body)
  373. body = ('<?xml version="1.0" encoding="UTF-8"?>'
  374. '<ListBucketResult '
  375. 'xmlns="http://s3.amazonaws.com/doc/2006-03-01/">'
  376. '<Prefix>%s</Prefix>'
  377. '<Marker>%s</Marker>'
  378. '<Delimiter>%s</Delimiter>'
  379. '<IsTruncated>%s</IsTruncated>'
  380. '<MaxKeys>%s</MaxKeys>'
  381. '<Name>%s</Name>'
  382. '%s'
  383. '%s'
  384. '</ListBucketResult>' %
  385. (
  386. xml_escape(params.get('prefix', '')),
  387. xml_escape(params.get('marker', '')),
  388. xml_escape(params.get('delimiter', '')),
  389. 'true' if len(objects) == (max_keys + 1) else 'false',
  390. max_keys,
  391. xml_escape(self.container_name),
  392. "".join(['<Contents><Key>%s</Key><LastModified>%sZ</Last'\
  393. 'Modified><ETag>%s</ETag><Size>%s</Size><Storage'\
  394. 'Class>STANDARD</StorageClass></Contents>' %
  395. (xml_escape(i['name']), i['last_modified'][:-3],
  396. i['hash'], i['bytes'])
  397. for i in objects[:max_keys] if 'subdir' not in i]),
  398. "".join(['<CommonPrefixes><Prefix>%s</Prefix></Common'\
  399. 'Prefixes>' % xml_escape(i['subdir'])
  400. for i in objects[:max_keys] if 'subdir' in i])))
  401. return Response(body=body, content_type='application/xml')
  402. def PUT(self, req):
  403. """
  404. Handles PUT Bucket request.
  405. """
  406. # any operations with multipart buckets are not allowed to user
  407. check_container_name_invalid_bucket_name_error(self.container_name)
  408. resp = req.get_response(self.app)
  409. status = resp.status_int
  410. if status != 201:
  411. if status == 401:
  412. return get_err_response('AccessDenied')
  413. elif status == 202:
  414. return get_err_response('BucketAlreadyExists')
  415. else:
  416. return get_err_response('InvalidURI')
  417. resp = Response()
  418. resp.headers.add('Location', self.container_name)
  419. resp.status = 200
  420. return resp
  421. def mpu_bucket_deletion_list_request(self, req, cont_path):
  422. """This method returns listing of MPU bucket for deletion"""
  423. list_req = req.copy()
  424. list_req.method = 'GET'
  425. list_req.upath_info = cont_path
  426. list_req.GET.clear()
  427. list_req.GET['format'] = 'json'
  428. return list_req.get_response(self.app)
  429. def mpu_bucket_deletion(self, req):
  430. """
  431. This method checks if MPU bucket exists and
  432. if there are any active MPUs are in it.
  433. MPUs are aborted, uploaded parts are deleted.
  434. """
  435. cont_name = MULTIPART_UPLOAD_PREFIX + self.container_name
  436. cont_path = "/v1/%s/%s/" % (self.account_name, cont_name)
  437. list_resp = self.mpu_bucket_deletion_list_request(req, cont_path)
  438. status = list_resp.status_int
  439. if status != 200:
  440. if status == 401:
  441. return get_err_response('AccessDenied')
  442. elif status == 404:
  443. # there is no MPU bucket, it's OK, there is only regular bucket
  444. pass
  445. else:
  446. return get_err_response('InvalidURI')
  447. elif status == 200:
  448. # aborting multipart uploads by deleting meta and other files
  449. objects = json.loads(list_resp.body)
  450. for obj in objects:
  451. if obj['name'].endswith('/meta'):
  452. for mpu_obj in objects:
  453. if mpu_obj['name'].startswith(obj['name'][:-5]):
  454. obj_req = req.copy()
  455. obj_req.upath_info = "%s%s" % (cont_path,
  456. mpu_obj['name'])
  457. obj_req.GET.clear()
  458. obj_resp = obj_req.get_response(self.app)
  459. status = obj_resp.status_int
  460. #TODO: Add some logs here
  461. if status not in (200, 204):
  462. if status == 401:
  463. return get_err_response('AccessDenied')
  464. elif status == 404:
  465. return get_err_response('NoSuchKey')
  466. else:
  467. return get_err_response('InvalidURI')
  468. # deleting multipart bucket
  469. del_mpu_req = req.copy()
  470. del_mpu_req.upath_info = cont_path
  471. del_mpu_req.GET.clear()
  472. del_mpu_resp = del_mpu_req.get_response(self.app)
  473. status = del_mpu_resp.status_int
  474. if status != 204:
  475. if status == 401:
  476. return get_err_response('AccessDenied')
  477. elif status == 404:
  478. return get_err_response('InvalidBucketName')
  479. elif status == 409:
  480. return get_err_response('BucketNotEmpty')
  481. else:
  482. return get_err_response('InvalidURI')
  483. return Response(status=204)
  484. def DELETE(self, req):
  485. """
  486. Handles DELETE Bucket request.
  487. Also deletes multipart bucket if it exists.
  488. Aborts all multipart uploads initiated for this bucket.
  489. """
  490. # any operations with multipart buckets are not allowed to user
  491. check_container_name_no_such_bucket_error(self.container_name)
  492. # deleting regular bucket,
  493. # request is copied to save valid authorization
  494. del_req = req.copy()
  495. resp = del_req.get_response(self.app)
  496. status = resp.status_int
  497. if status != 204:
  498. if status == 401:
  499. return get_err_response('AccessDenied')
  500. elif status == 404:
  501. return get_err_response('InvalidBucketName')
  502. elif status == 409:
  503. return get_err_response('BucketNotEmpty')
  504. else:
  505. return get_err_response('InvalidURI')
  506. # check if there is a multipart bucket and
  507. # return 204 when everything is deleted
  508. return self.mpu_bucket_deletion(req)
  509. class NormalObjectController(object):
  510. """
  511. Handles requests on objects.
  512. """
  513. def __init__(self, env, app, account_name, token, container_name,
  514. object_name, **kwargs):
  515. self.app = app
  516. self.account_name = unquote(account_name)
  517. self.container_name = unquote(container_name)
  518. self.object_name = unquote(object_name)
  519. env['HTTP_X_AUTH_TOKEN'] = token
  520. env['PATH_INFO'] = '/v1/%s/%s/%s' % (account_name, container_name,
  521. object_name)
  522. def GETorHEAD(self, req):
  523. resp = req.get_response(self.app)
  524. status = resp.status_int
  525. headers = resp.headers
  526. app_iter = resp.app_iter
  527. if 200 <= status < 300:
  528. if 'acl' in req.GET:
  529. return get_acl(self.account_name)
  530. new_hdrs = {}
  531. for key, val in headers.iteritems():
  532. _key = key.lower()
  533. if _key.startswith('x-object-meta-'):
  534. new_hdrs['x-amz-meta-' + key[14:]] = val
  535. elif _key in ('content-length', 'content-type',
  536. 'content-encoding', 'etag', 'last-modified'):
  537. new_hdrs[key] = val
  538. return Response(status=status, headers=new_hdrs, app_iter=app_iter)
  539. elif status == 401:
  540. return get_err_response('AccessDenied')
  541. elif status == 404:
  542. return get_err_response('NoSuchKey')
  543. else:
  544. return get_err_response('InvalidURI')
  545. def HEAD(self, req):
  546. """
  547. Handles HEAD Object request.
  548. """
  549. return self.GETorHEAD(req)
  550. def GET(self, req):
  551. """
  552. Handles GET Object request.
  553. """
  554. return self.GETorHEAD(req)
  555. def PUT(self, req):
  556. """
  557. Handles PUT Object and PUT Object (Copy) request.
  558. """
  559. environ = req.environ
  560. for key, value in environ.items():
  561. if key.startswith('HTTP_X_AMZ_META_'):
  562. del environ[key]
  563. environ['HTTP_X_OBJECT_META_' + key[16:]] = value
  564. elif key == 'HTTP_CONTENT_MD5':
  565. environ['HTTP_ETAG'] = value.decode('base64').encode('hex')
  566. elif key == 'HTTP_X_AMZ_COPY_SOURCE':
  567. environ['HTTP_X_COPY_FROM'] = value
  568. resp = req.get_response(self.app)
  569. status = resp.status_int
  570. headers = resp.headers
  571. if status != 201:
  572. if status == 401:
  573. return get_err_response('AccessDenied')
  574. elif status == 404:
  575. return get_err_response('InvalidBucketName')
  576. else:
  577. return get_err_response('InvalidURI')
  578. if 'HTTP_X_COPY_FROM' in environ:
  579. body = '<CopyObjectResult>' \
  580. '<ETag>"%s"</ETag>' \
  581. '</CopyObjectResult>' % headers['ETag']
  582. return Response(status=200, body=body)
  583. return Response(status=200, etag=headers['ETag'])
  584. def DELETE(self, req):
  585. """
  586. Handles DELETE Object request.
  587. """
  588. resp = req.get_response(self.app)
  589. status = resp.status_int
  590. if status not in (200, 204):
  591. if status == 401:
  592. return get_err_response('AccessDenied')
  593. elif status == 404:
  594. return get_err_response('NoSuchKey')
  595. else:
  596. return get_err_response('InvalidURI')
  597. return Response(status=204)
  598. class MultiPartObjectController(object):
  599. def __init__(self, env, app, account_name, token, container_name,
  600. object_name, **kwargs):
  601. self.app = app
  602. self.account_name = unquote(account_name)
  603. self.container_name = unquote(container_name)
  604. self.object_name = unquote(object_name)
  605. self.orig_path_info = env['PATH_INFO']
  606. env['HTTP_X_AUTH_TOKEN'] = token
  607. env['PATH_INFO'] = '/v1/%s/%s/%s' % (account_name, container_name,
  608. object_name)
  609. def GET(self, req):
  610. """
  611. Lists multipart uploads by uploadId.
  612. """
  613. # any operations with multipart buckets are not allowed to user
  614. check_container_name_no_such_bucket_error(self.container_name)
  615. upload_id = req.GET.get('uploadId')
  616. max_parts = req.GET.get('max-parts', '1000')
  617. part_number_marker = req.GET.get('part-number-marker', '')
  618. try:
  619. int(upload_id, 16)
  620. max_parts = int(max_parts)
  621. if part_number_marker:
  622. part_number_marker = int(part_number_marker)
  623. except (TypeError, ValueError):
  624. return get_err_response('InvalidURI')
  625. object_name_prefix_len = len(self.object_name) + 1
  626. cont_name = MULTIPART_UPLOAD_PREFIX + self.container_name
  627. cont_path = "/v1/%s/%s/" % (self.account_name, cont_name)
  628. meta_path = "%s%s/%s/meta" % (cont_path,
  629. self.object_name,
  630. upload_id)
  631. meta_resp = meta_request_head(req, meta_path, self.app)
  632. status = meta_resp.status_int
  633. if status != 200:
  634. return get_err_response('NoSuchUpload')
  635. list_req = req.copy()
  636. list_req.upath_info = cont_path
  637. list_req.GET.clear()
  638. list_req.GET['format'] = 'json'
  639. list_req.GET['prefix'] = "%s/%s/%s/part/" % (cont_name,
  640. self.object_name,
  641. upload_id)
  642. list_req.GET['limit'] = str(max_parts + 1)
  643. if part_number_marker:
  644. list_req.GET['marker'] = "%s/%s/part/%s" % (self.object_name,
  645. upload_id,
  646. part_number_marker)
  647. resp = list_req.get_response(self.app)
  648. status = resp.status_int
  649. if status != 200:
  650. if status == 401:
  651. return get_err_response('AccessDenied')
  652. elif status == 404:
  653. return get_err_response('InvalidBucketName')
  654. else:
  655. return get_err_response('InvalidURI')
  656. objects = json.loads(resp.body)
  657. if len(objects) > max_parts:
  658. objects = objects.pop(-1)
  659. next_marker = objects[-1]['name'][object_name_prefix_len:]
  660. is_truncated = 'true'
  661. else:
  662. next_marker = ''
  663. is_truncated = 'false'
  664. if next_marker:
  665. next_marker = "<NextPartNumberMarker>%</NextPartNumberMarker>" % \
  666. next_marker
  667. if part_number_marker:
  668. part_number_marker = "<PartNumberMarker>%</PartNumberMarker>" % \
  669. part_number_marker
  670. parts = ''.join(("<Part>"
  671. "<PartNumber>%s</PartNumber>"
  672. "<LastModified>%sZ</LastModified>"
  673. "<ETag>\"%s\"</ETag>"
  674. "<Size>%s</Size>"
  675. "</Part>" % (
  676. obj['name'][object_name_prefix_len:],
  677. obj['last_modified'][:-3],
  678. obj['hash'],
  679. obj['bytes']) for obj in objects))
  680. body = (
  681. "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
  682. "<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">"
  683. "<Bucket>%s</Bucket>"
  684. "<Key>%s</Key>"
  685. "<UploadId>%s</UploadId>"
  686. "<Initiator>"
  687. "<ID>%s</ID>"
  688. "<DisplayName>%s</DisplayName>"
  689. "</Initiator>"
  690. "<Owner>"
  691. "<ID>%s</ID>"
  692. "<DisplayName>%s</DisplayName>"
  693. "</Owner>"
  694. "<StorageClass>STANDARD</StorageClass>"
  695. "%s%s"
  696. "<MaxParts>%s</MaxParts>"
  697. "<IsTruncated>%s</IsTruncated>"
  698. "%s"
  699. "</ListPartsResult>" % (
  700. self.container_name,
  701. self.object_name,
  702. upload_id,
  703. self.account_name,
  704. self.account_name,
  705. self.account_name,
  706. self.account_name,
  707. part_number_marker,
  708. next_marker,
  709. max_parts,
  710. is_truncated,
  711. parts,
  712. ))
  713. return Response(status=200,
  714. body=body,
  715. content_type='application/xml')
  716. def post_uploads_container_request(self, req, cont_path):
  717. """Method used to create a container for MPU."""
  718. cont_req = req.copy()
  719. cont_req.method = 'PUT'
  720. cont_req.upath_info = cont_path
  721. cont_req.GET.clear()
  722. return cont_req.get_response(self.app)
  723. def post_uploads_put_meta_req(self, req, cont_path, upload_id):
  724. """Method to create a MPU metafile."""
  725. meta_req = req.copy()
  726. meta_req.method = 'PUT'
  727. meta_req.upath_info = "%s%s/%s/meta" % (cont_path,
  728. self.object_name,
  729. upload_id)
  730. for header, value in meta_req.headers.items():
  731. if header.lower().startswith('x-amz-meta-'):
  732. meta_req.headers['X-Object-Meta-Amz-' + header[11:]] = \
  733. value
  734. return meta_req.get_response(self.app)
  735. def post_uploads(self, req):
  736. """
  737. Called if POST with 'uploads' query string was received.
  738. Creates metafile which is used as a flag on uncompleted MPU.
  739. Initiates multipart upload.
  740. """
  741. cont_name = MULTIPART_UPLOAD_PREFIX + self.container_name
  742. cont_path = "/v1/%s/%s/" % (self.account_name, cont_name)
  743. cont_req = req.copy()
  744. cont_req.method = 'HEAD'
  745. cont_req.upath_info = cont_path
  746. cont_req.GET.clear()
  747. cont_resp = cont_req.get_response(self.app)
  748. status = cont_resp.status_int
  749. if status == 404:
  750. # creating container for MPU
  751. cont_resp = self.post_uploads_container_request(req, cont_path)
  752. status = cont_resp.status_int
  753. if status not in (201, 204):
  754. if status == 401:
  755. return get_err_response('AccessDenied')
  756. elif status == 404:
  757. return get_err_response('InvalidBucketName')
  758. else:
  759. return get_err_response('InvalidURI')
  760. upload_id = uuid.uuid4().hex
  761. meta_resp = self.post_uploads_put_meta_req(req, cont_path, upload_id)
  762. status = meta_resp.status_int
  763. if status != 201:
  764. if status == 401:
  765. return get_err_response('AccessDenied')
  766. elif status == 404:
  767. return get_err_response('InvalidBucketName')
  768. else:
  769. return get_err_response('InvalidURI')
  770. body = ('<?xml version="1.0" encoding="UTF-8"?>'
  771. '<InitiateMultipartUploadResult '\
  772. 'xmlns="http://s3.amazonaws.com/doc/2006-03-01/">'
  773. '<Bucket>%s</Bucket>'
  774. '<Key>%s</Key>'
  775. '<UploadId>%s</UploadId>'
  776. '</InitiateMultipartUploadResult>' %
  777. (self.container_name, self.object_name, upload_id))
  778. return Response(status=200,
  779. body=body,
  780. content_type='application/xml')
  781. def post_uploadId(self, req):
  782. """
  783. Called if POST with 'uploadId' query string was received.
  784. Deletes metafile after completion of MPU.
  785. Completes multipart upload.
  786. """
  787. upload_id = req.GET.get('uploadId')
  788. try:
  789. int(upload_id, 16)
  790. except (TypeError, ValueError):
  791. return get_err_response('InvalidURI')
  792. cont_name = MULTIPART_UPLOAD_PREFIX + self.container_name
  793. cont_path = "/v1/%s/%s/" % (self.account_name, cont_name)
  794. meta_path = "%s%s/%s/meta" % (cont_path,
  795. self.object_name,
  796. upload_id)
  797. meta_resp = meta_request_head(req, meta_path, self.app)
  798. status = meta_resp.status_int
  799. if status != 200:
  800. if status == 401:
  801. return get_err_response('AccessDenied')
  802. elif status == 404:
  803. return get_err_response('NoSuchUpload')
  804. else:
  805. return get_err_response('InvalidURI')
  806. # TODO: Validate uploaded parts.
  807. manifest_path = MULTIPART_UPLOAD_PREFIX + \
  808. "%s/%s/%s/part/" % (self.container_name,
  809. self.object_name,
  810. upload_id)
  811. manifest_req = req.copy()
  812. manifest_req.method = 'PUT'
  813. manifest_req.GET.clear()
  814. manifest_req.headers['X-Object-Manifest'] = manifest_path
  815. for header, value in meta_resp.headers.iteritems():
  816. if header.lower().startswith('x-object-meta-amz-'):
  817. manifest_req.headers['x-amz-meta-' + header[18:]] = value
  818. manifest_resp = manifest_req.get_response(self.app)
  819. status = manifest_resp.status_int
  820. if status == 201:
  821. finish_req = req.copy()
  822. finish_req.method = 'DELETE'
  823. finish_req.upath_info = meta_path
  824. finish_req.body = ''
  825. finish_req.GET.clear()
  826. finish_resp = finish_req.get_response(self.app)
  827. status = finish_resp.status_int
  828. if status not in (201, 204):
  829. if status == 401:
  830. return get_err_response('AccessDenied')
  831. elif status == 404:
  832. return get_err_response('InvalidBucketName')
  833. else:
  834. return get_err_response('InvalidURI')
  835. body = ('<?xml version="1.0" encoding="UTF-8"?>'
  836. '<CompleteMultipartUploadResult '\
  837. 'xmlns="http://s3.amazonaws.com/doc/2006-03-01/">'
  838. '<Location>%s</Location>'
  839. '<Bucket>%s</Bucket>'
  840. '<Key>%s</Key>'
  841. '<ETag>%s</ETag>'
  842. '</CompleteMultipartUploadResult>' %
  843. (self.orig_path_info,
  844. self.container_name,
  845. self.object_name,
  846. manifest_resp.headers['ETag']))
  847. return Response(status=200,
  848. body=body,
  849. content_type='application/xml')
  850. def POST(self, req):
  851. """
  852. Initiate and complete multipart upload.
  853. """
  854. # any operations with multipart buckets are not allowed to user
  855. check_container_name_invalid_bucket_name_error(self.container_name)
  856. if 'uploads' in req.GET:
  857. return self.post_uploads(req)
  858. elif 'uploadId' in req.GET:
  859. return self.post_uploadId(req)
  860. return get_err_response('InvalidURI')
  861. def PUT(self, req):
  862. """
  863. Upload part of a multipart upload.
  864. """
  865. upload_id = req.GET.get('uploadId')
  866. part_number = req.GET.get('partNumber', '')
  867. try:
  868. int(upload_id, 16)
  869. except (TypeError, ValueError):
  870. return get_err_response('InvalidURI')
  871. if not part_number.isdigit():
  872. return get_err_response('InvalidURI')
  873. # any operations with multipart buckets are not allowed to user
  874. check_container_name_invalid_bucket_name_error(self.container_name)
  875. cont_name = MULTIPART_UPLOAD_PREFIX + self.container_name
  876. cont_path = "/v1/%s/%s/" % (self.account_name, cont_name)
  877. meta_path = "%s%s/%s/meta" % (cont_path, self.object_name, upload_id)
  878. meta_resp = meta_request_head(req, meta_path, self.app)
  879. status = meta_resp.status_int
  880. if status != 200:
  881. return get_err_response('NoSuchUpload')
  882. req = req.copy()
  883. req.upath_info = "%s%s/%s/part/%s" % (cont_path, self.object_name,
  884. upload_id, part_number)
  885. req.GET.clear()
  886. resp = req.get_response(self.app)
  887. status = resp.status_int
  888. headers = resp.headers
  889. if status != 201:
  890. if status == 401:
  891. return get_err_response('AccessDenied')
  892. elif status == 404:
  893. return get_err_response('InvalidBucketName')
  894. else:
  895. return get_err_response('InvalidURI')
  896. if 'HTTP_X_COPY_FROM' in req.environ:
  897. body = '<CopyObjectResult>' \
  898. '<ETag>"%s"</ETag>' \
  899. '</CopyObjectResult>' % resp.headers['ETag']
  900. return Response(status=200, body=body)
  901. return Response(status=200, etag=resp.headers['ETag'])
  902. def DELETE(self, req):
  903. """
  904. Aborts multipart upload by uploadId.
  905. """
  906. upload_id = req.GET.get('uploadId')
  907. try:
  908. int(upload_id, 16)
  909. except (TypeError, ValueError):
  910. return get_err_response('InvalidURI')
  911. # any operations with multipart buckets are not allowed to user
  912. check_container_name_no_such_bucket_error(self.container_name)
  913. cont_name = MULTIPART_UPLOAD_PREFIX + self.container_name
  914. cont_path = "/v1/%s/%s/" % (self.account_name, cont_name)
  915. prefix = "%s/%s/" % (self.object_name, upload_id)
  916. list_req = req.copy_get()
  917. list_req.upath_info = cont_path
  918. list_req.GET.clear()
  919. list_req.GET['format'] = 'json'
  920. list_req.GET['prefix'] = prefix
  921. list_resp = list_req.get_response(self.app)
  922. status = list_resp.status_int
  923. if status != 200:
  924. if status == 401:
  925. return get_err_response('AccessDenied')
  926. elif status == 404:
  927. return get_err_response('InvalidBucketName')
  928. else:
  929. return get_err_response('InvalidURI')
  930. objects = json.loads(list_resp.body)
  931. for obj in objects:
  932. obj_req = req.copy()
  933. obj_req.method = 'DELETE'
  934. obj_req.upath_info = "%s%s" % (cont_path, obj['name'])
  935. obj_req.GET.clear()
  936. obj_resp = obj_req.get_response(self.app)
  937. status = obj_resp.status_int
  938. if status not in (200, 204):
  939. if status == 401:
  940. return get_err_response('AccessDenied')
  941. elif status == 404:
  942. return get_err_response('NoSuchKey')
  943. else:
  944. return get_err_response('InvalidURI')
  945. return Response(status=204)
  946. class ObjectController(NormalObjectController, MultiPartObjectController):
  947. """Manages requests on normal and multipart objects"""
  948. def __init__(self, *args, **kwargs):
  949. MultiPartObjectController.__init__(self, *args, **kwargs)
  950. def GET(self, req):
  951. if 'uploadId' in req.GET:
  952. return MultiPartObjectController.GET(self, req)
  953. return NormalObjectController.GET(self, req)
  954. def PUT(self, req):
  955. if 'uploadId' in req.GET:
  956. return MultiPartObjectController.PUT(self, req)
  957. return NormalObjectController.PUT(self, req)
  958. def POST(self, req):
  959. if 'uploadId' in req.GET or 'uploads' in req.GET:
  960. return MultiPartObjectController.POST(self, req)
  961. return NormalObjectController.POST(self, req)
  962. def DELETE(self, req):
  963. if 'uploadId' in req.GET:
  964. return MultiPartObjectController.DELETE(self, req)
  965. obj_req = req.copy_get()
  966. obj_req.method = 'HEAD'
  967. obj_req.GET.clear()
  968. obj_resp = obj_req.get_response(self.app)
  969. status = obj_resp.status_int
  970. if status == 200 and 'X-Object-Manifest' in obj_resp.headers:
  971. manifest = obj_resp.headers['X-Object-Manifest']
  972. upload_id = manifest.split('/')[2]
  973. del_req = req.copy()
  974. del_req.GET['uploadId'] = upload_id
  975. MultiPartObjectController.DELETE(self, del_req)
  976. return NormalObjectController.DELETE(self, req)
  977. class Swift3Middleware(object):
  978. """Swift3 S3 compatibility midleware"""
  979. def __init__(self, app, conf, *args, **kwargs):
  980. self.app = app
  981. def get_controller(self, path, params):
  982. container, obj = split_path(path, 0, 2, True)
  983. d = dict(container_name=container, object_name=obj)
  984. if container and obj:
  985. return ObjectController, d
  986. elif container:
  987. return BucketController, d
  988. return ServiceController, d
  989. def __call__(self, env, start_response):
  990. req = Request(env)
  991. if 'AWSAccessKeyId' in req.GET:
  992. try:
  993. req.headers['Date'] = req.GET['Expires']
  994. req.headers['Authorization'] = \
  995. 'AWS %(AWSAccessKeyId)s:%(Signature)s' % req.GET
  996. except KeyError:
  997. return get_err_response('InvalidArgument')(env, start_response)
  998. if not 'Authorization' in req.headers:
  999. return self.app(env, start_response)
  1000. try:
  1001. account, signature = \
  1002. req.headers['Authorization'].split(' ')[-1].rsplit(':', 1)
  1003. except Exception:
  1004. return get_err_response('InvalidArgument')(env, start_response)
  1005. try:
  1006. controller, path_parts = self.get_controller(req.path, req.GET)
  1007. except ValueError:
  1008. return get_err_response('InvalidURI')(env, start_response)
  1009. token = base64.urlsafe_b64encode(canonical_string(req))
  1010. controller = controller(req.environ,
  1011. self.app,
  1012. account,
  1013. token,
  1014. **path_parts)
  1015. if hasattr(controller, req.method):
  1016. res = getattr(controller, req.method)(req)
  1017. else:
  1018. return get_err_response('InvalidURI')(env, start_response)
  1019. return res(env, start_response)
  1020. def filter_factory(global_conf, **local_conf):
  1021. """Standard filter factory to use the middleware with paste.deploy"""
  1022. conf = global_conf.copy()
  1023. conf.update(local_conf)
  1024. def swifts3_filter(app):
  1025. return Swift3Middleware(app, conf)
  1026. return swifts3_filter