def _validate_and_prep_request_headers(req): """ Validate that the value from x-symlink-target header is well formatted and that the x-symlink-target-etag header (if present) does not contain problematic characters. We assume the caller ensures that x-symlink-target header is present in req.headers. :param req: HTTP request object :returns: a tuple, the full versioned path to the object (as a WSGI string) and the X-Symlink-Target-Etag header value which may be None :raise: HTTPPreconditionFailed if x-symlink-target value is not well formatted. :raise: HTTPBadRequest if the x-symlink-target value points to the request path. :raise: HTTPBadRequest if the x-symlink-target-etag value contains a semicolon, double-quote, or backslash. """ # N.B. check_path_header doesn't assert the leading slash and # copy middleware may accept the format. In the symlink, API # says apparently to use "container/object" format so add the # validation first, here. error_body = 'X-Symlink-Target header must be of the form ' \ '<container name>/<object name>' if wsgi_unquote(req.headers[TGT_OBJ_SYMLINK_HDR]).startswith('/'): raise HTTPPreconditionFailed(body=error_body, request=req, content_type='text/plain') # check container and object format container, obj = check_path_header(req, TGT_OBJ_SYMLINK_HDR, 2, error_body) req.headers[TGT_OBJ_SYMLINK_HDR] = wsgi_quote('%s/%s' % (container, obj)) # Check account format if it exists account = check_account_format( req, wsgi_unquote(req.headers[TGT_ACCT_SYMLINK_HDR])) \ if TGT_ACCT_SYMLINK_HDR in req.headers else None # Extract request path _junk, req_acc, req_cont, req_obj = req.split_path(4, 4, True) if account: req.headers[TGT_ACCT_SYMLINK_HDR] = wsgi_quote(account) else: account = req_acc # Check if symlink targets the symlink itself or not if (account, container, obj) == (req_acc, req_cont, req_obj): raise HTTPBadRequest(body='Symlink cannot target itself', request=req, content_type='text/plain') etag = req.headers.get(TGT_ETAG_SYMLINK_HDR, None) if etag and any(c in etag for c in ';"\\'): # See cgi.parse_header for why the above chars are problematic raise HTTPBadRequest(body='Bad %s format' % TGT_ETAG_SYMLINK_HDR.title(), request=req, content_type='text/plain') if not (etag or req.headers.get('Content-Type')): req.headers['Content-Type'] = 'application/symlink' return '/v1/%s/%s/%s' % (account, container, obj), etag
def _check_symlink_header(req): """ Validate that the value from x-symlink-target header is well formatted. We assume the caller ensures that x-symlink-target header is present in req.headers. :param req: HTTP request object :raise: HTTPPreconditionFailed if x-symlink-target value is not well formatted. :raise: HTTPBadRequest if the x-symlink-target value points to the request path. """ # N.B. check_path_header doesn't assert the leading slash and # copy middleware may accept the format. In the symlink, API # says apparently to use "container/object" format so add the # validation first, here. error_body = 'X-Symlink-Target header must be of the form ' \ '<container name>/<object name>' try: if wsgi_unquote(req.headers[TGT_OBJ_SYMLINK_HDR]).startswith('/'): raise HTTPPreconditionFailed(body=error_body, request=req, content_type='text/plain') except TypeError: raise HTTPPreconditionFailed(body=error_body, request=req, content_type='text/plain') # check container and object format container, obj = check_path_header(req, TGT_OBJ_SYMLINK_HDR, 2, error_body) req.headers[TGT_OBJ_SYMLINK_HDR] = wsgi_quote('%s/%s' % (container, obj)) # Check account format if it exists try: account = check_account_format( req, wsgi_unquote(req.headers[TGT_ACCT_SYMLINK_HDR])) \ if TGT_ACCT_SYMLINK_HDR in req.headers else None except TypeError: raise HTTPPreconditionFailed( body='Account name cannot contain slashes', request=req, content_type='text/plain') # Extract request path _junk, req_acc, req_cont, req_obj = req.split_path(4, 4, True) if account: req.headers[TGT_ACCT_SYMLINK_HDR] = wsgi_quote(account) else: account = req_acc # Check if symlink targets the symlink itself or not if (account, container, obj) == (req_acc, req_cont, req_obj): raise HTTPBadRequest(body='Symlink cannot target itself', request=req, content_type='text/plain')
def handle_COPY(self, req, start_response, account, container, obj): if not req.headers.get('Destination'): return HTTPPreconditionFailed(request=req, body='Destination header required')( req.environ, start_response) dest_account = account if 'Destination-Account' in req.headers: dest_account = wsgi_unquote(req.headers.get('Destination-Account')) dest_account = check_account_format(req, dest_account) req.headers['X-Copy-From-Account'] = wsgi_quote(account) account = dest_account del req.headers['Destination-Account'] dest_container, dest_object = _check_destination_header(req) source = '/%s/%s' % (container, obj) container = dest_container obj = dest_object # re-write the existing request as a PUT instead of creating a new one req.method = 'PUT' # As this the path info is updated with destination container, # the proxy server app will use the right object controller # implementation corresponding to the container's policy type. ver, _junk = req.split_path(1, 2, rest_with_last=True) req.path_info = '/%s/%s/%s/%s' % (ver, dest_account, dest_container, dest_object) req.headers['Content-Length'] = 0 req.headers['X-Copy-From'] = wsgi_quote(source) del req.headers['Destination'] return self.handle_PUT(req, start_response)
def handle_request(self, req, start_response): """ Take a GET or HEAD request, and if it is for a dynamic large object manifest, return an appropriate response. Otherwise, simply pass it through. """ update_ignore_range_header(req, 'X-Object-Manifest') resp_iter = self._app_call(req.environ) # make sure this response is for a dynamic large object manifest for header, value in self._response_headers: if (header.lower() == 'x-object-manifest'): content_length = self._response_header_value('content-length') if content_length is not None and int(content_length) < 1024: # Go ahead and consume small bodies drain_and_close(resp_iter) close_if_possible(resp_iter) response = self.get_or_head_response( req, wsgi_to_str(wsgi_unquote(value))) return response(req.environ, start_response) # Not a dynamic large object manifest; just pass it through. start_response(self._response_status, self._response_headers, self._response_exc_info) return resp_iter
def handle_COPY(self, req, start_response, account, container, obj): if not req.headers.get('Destination'): return HTTPPreconditionFailed(request=req, body='Destination header required' )(req.environ, start_response) dest_account = account if 'Destination-Account' in req.headers: dest_account = wsgi_unquote(req.headers.get('Destination-Account')) dest_account = check_account_format(req, dest_account) req.headers['X-Copy-From-Account'] = wsgi_quote(account) account = dest_account del req.headers['Destination-Account'] dest_container, dest_object = _check_destination_header(req) source = '/%s/%s' % (container, obj) container = dest_container obj = dest_object # re-write the existing request as a PUT instead of creating a new one req.method = 'PUT' # As this the path info is updated with destination container, # the proxy server app will use the right object controller # implementation corresponding to the container's policy type. ver, _junk = req.split_path(1, 2, rest_with_last=True) req.path_info = '/%s/%s/%s/%s' % ( ver, dest_account, dest_container, dest_object) req.headers['Content-Length'] = 0 req.headers['X-Copy-From'] = wsgi_quote(source) del req.headers['Destination'] return self.handle_PUT(req, start_response)
def parse_request(self): if not six.PY2: # request lines *should* be ascii per the RFC, but historically # we've allowed (and even have func tests that use) arbitrary # bytes. This breaks on py3 (see https://bugs.python.org/issue33973 # ) but the work-around is simple: munge the request line to be # properly quoted. py2 will do the right thing without this, but it # doesn't hurt to re-write the request line like this and it # simplifies testing. if self.raw_requestline.count(b' ') >= 2: parts = self.raw_requestline.split(b' ', 2) path, q, query = parts[1].partition(b'?') # unquote first, so we don't over-quote something # that was *correctly* quoted path = wsgi_to_bytes(wsgi_quote(wsgi_unquote( bytes_to_wsgi(path)))) query = b'&'.join( sep.join([ wsgi_to_bytes(wsgi_quote_plus(wsgi_unquote_plus( bytes_to_wsgi(key)))), wsgi_to_bytes(wsgi_quote_plus(wsgi_unquote_plus( bytes_to_wsgi(val)))) ]) for part in query.split(b'&') for key, sep, val in (part.partition(b'='), )) parts[1] = path + q + query self.raw_requestline = b' '.join(parts) # else, mangled protocol, most likely; let base class deal with it return wsgi.HttpProtocol.parse_request(self)
def get_objs_to_delete(self, req): """ Will populate objs_to_delete with data from request input. :params req: a Swob request :returns: a list of the contents of req.body when separated by newline. :raises HTTPException: on failures """ line = b'' data_remaining = True objs_to_delete = [] if req.content_length is None and \ req.headers.get('transfer-encoding', '').lower() != 'chunked': raise HTTPLengthRequired(request=req) while data_remaining: if b'\n' in line: obj_to_delete, line = line.split(b'\n', 1) if six.PY2: obj_to_delete = wsgi_unquote(obj_to_delete.strip()) else: # yeah, all this chaining is pretty terrible... # but it gets even worse trying to use UTF-8 and # errors='surrogateescape' when dealing with terrible # input like b'\xe2%98\x83' obj_to_delete = wsgi_to_str(wsgi_unquote( bytes_to_wsgi(obj_to_delete.strip()))) objs_to_delete.append({'name': obj_to_delete}) else: data = req.body_file.read(self.max_path_length) if data: line += data else: data_remaining = False if six.PY2: obj_to_delete = wsgi_unquote(line.strip()) else: obj_to_delete = wsgi_to_str(wsgi_unquote( bytes_to_wsgi(line.strip()))) if obj_to_delete: objs_to_delete.append({'name': obj_to_delete}) if len(objs_to_delete) > self.max_deletes_per_request: raise HTTPRequestEntityTooLarge( 'Maximum Bulk Deletes: %d per request' % self.max_deletes_per_request) if len(line) > self.max_path_length * 2: raise HTTPBadRequest('Invalid File Name') return objs_to_delete
def get_objs_to_delete(self, req): """ Will populate objs_to_delete with data from request input. :params req: a Swob request :returns: a list of the contents of req.body when separated by newline. :raises HTTPException: on failures """ line = b'' data_remaining = True objs_to_delete = [] if req.content_length is None and \ req.headers.get('transfer-encoding', '').lower() != 'chunked': raise HTTPLengthRequired(request=req) while data_remaining: if b'\n' in line: obj_to_delete, line = line.split(b'\n', 1) if six.PY2: obj_to_delete = wsgi_unquote(obj_to_delete.strip()) else: # yeah, all this chaining is pretty terrible... # but it gets even worse trying to use UTF-8 and # errors='surrogateescape' when dealing with terrible # input like b'\xe2%98\x83' obj_to_delete = wsgi_to_str( wsgi_unquote(bytes_to_wsgi(obj_to_delete.strip()))) objs_to_delete.append({'name': obj_to_delete}) else: data = req.body_file.read(self.max_path_length) if data: line += data else: data_remaining = False if six.PY2: obj_to_delete = wsgi_unquote(line.strip()) else: obj_to_delete = wsgi_to_str( wsgi_unquote(bytes_to_wsgi(line.strip()))) if obj_to_delete: objs_to_delete.append({'name': obj_to_delete}) if len(objs_to_delete) > self.max_deletes_per_request: raise HTTPRequestEntityTooLarge( 'Maximum Bulk Deletes: %d per request' % self.max_deletes_per_request) if len(line) > self.max_path_length * 2: raise HTTPBadRequest('Invalid File Name') return objs_to_delete
def object_request(self, req, api_version, account, container, obj, allow_versioned_writes): """ Handle request for object resource. Note that account, container, obj should be unquoted by caller if the url path is under url encoding (e.g. %FF) :param req: swift.common.swob.Request instance :param api_version: should be v1 unless swift bumps api version :param account: account name string :param container: container name string :param object: object name string """ resp = None is_enabled = config_true_value(allow_versioned_writes) container_info = get_container_info( req.environ, self.app) # To maintain backwards compatibility, container version # location could be stored as sysmeta or not, need to check both. # If stored as sysmeta, check if middleware is enabled. If sysmeta # is not set, but versions property is set in container_info, then # for backwards compatibility feature is enabled. versions_cont = container_info.get( 'sysmeta', {}).get('versions-location') versioning_mode = container_info.get( 'sysmeta', {}).get('versions-mode', 'stack') if not versions_cont: versions_cont = container_info.get('versions') # if allow_versioned_writes is not set in the configuration files # but 'versions' is configured, enable feature to maintain # backwards compatibility if not allow_versioned_writes and versions_cont: is_enabled = True if is_enabled and versions_cont: versions_cont = wsgi_unquote(str_to_wsgi( versions_cont)).split('/')[0] vw_ctx = VersionedWritesContext(self.app, self.logger) if req.method == 'PUT': resp = vw_ctx.handle_obj_versions_put( req, versions_cont, api_version, account, obj) # handle DELETE elif versioning_mode == 'history': resp = vw_ctx.handle_obj_versions_delete_push( req, versions_cont, api_version, account, container, obj) else: resp = vw_ctx.handle_obj_versions_delete_pop( req, versions_cont, api_version, account, container, obj) if resp: return resp else: return self.app
def object_request(self, req, api_version, account, container, obj, allow_versioned_writes): """ Handle request for object resource. Note that account, container, obj should be unquoted by caller if the url path is under url encoding (e.g. %FF) :param req: swift.common.swob.Request instance :param api_version: should be v1 unless swift bumps api version :param account: account name string :param container: container name string :param object: object name string """ resp = None is_enabled = config_true_value(allow_versioned_writes) container_info = get_container_info( req.environ, self.app, swift_source='VW') # To maintain backwards compatibility, container version # location could be stored as sysmeta or not, need to check both. # If stored as sysmeta, check if middleware is enabled. If sysmeta # is not set, but versions property is set in container_info, then # for backwards compatibility feature is enabled. versions_cont = container_info.get( 'sysmeta', {}).get('versions-location') versioning_mode = container_info.get( 'sysmeta', {}).get('versions-mode', 'stack') if not versions_cont: versions_cont = container_info.get('versions') # if allow_versioned_writes is not set in the configuration files # but 'versions' is configured, enable feature to maintain # backwards compatibility if not allow_versioned_writes and versions_cont: is_enabled = True if is_enabled and versions_cont: versions_cont = wsgi_unquote(str_to_wsgi( versions_cont)).split('/')[0] vw_ctx = VersionedWritesContext(self.app, self.logger) if req.method == 'PUT': resp = vw_ctx.handle_obj_versions_put( req, versions_cont, api_version, account, obj) # handle DELETE elif versioning_mode == 'history': resp = vw_ctx.handle_obj_versions_delete_push( req, versions_cont, api_version, account, container, obj) else: resp = vw_ctx.handle_obj_versions_delete_pop( req, versions_cont, api_version, account, container, obj) if resp: return resp else: return self.app
def handle_request(self, req, versions_cont, api_version, account, container, obj, is_enabled): if req.method == 'PUT': return self.handle_put(req, versions_cont, api_version, account, obj, is_enabled) elif req.method == 'POST': return self.handle_post(req, versions_cont, account) elif req.method == 'DELETE': return self.handle_delete(req, versions_cont, api_version, account, container, obj, is_enabled) # GET/HEAD/OPTIONS resp = req.get_response(self.app) resp.headers['X-Object-Version-Id'] = resp.headers[VERSION_ID_HEADER] # Check for a "real" version loc = wsgi_unquote(resp.headers.get('Content-Location', '')) if loc: _, acct, cont, version_obj = split_path(loc, 4, 4, True) if acct == account and cont == versions_cont: _, version = self._split_version_from_name(version_obj) if version is not None: resp.headers['X-Object-Version-Id'] = version.internal content_loc = wsgi_quote('/%s/%s/%s/%s' % ( api_version, account, container, obj, )) + '?version-id=%s' % (version.internal, ) resp.headers['Content-Location'] = content_loc symlink_target = wsgi_unquote(resp.headers.get('X-Symlink-Target', '')) if symlink_target: cont, version_obj = split_path('/%s' % symlink_target, 2, 2, True) if cont == versions_cont: _, version = self._split_version_from_name(version_obj) if version is not None: resp.headers['X-Object-Version-Id'] = version.internal symlink_target = wsgi_quote('%s/%s' % (container, obj)) + \ '?version-id=%s' % (version.internal,) resp.headers['X-Symlink-Target'] = symlink_target return resp
def make_subrequest(env, method=None, path=None, body=None, headers=None, agent='Swift', swift_source=None, make_env=make_env): """ Makes a new swob.Request based on the current env but with the parameters specified. :param env: The WSGI environment to base the new request on. :param method: HTTP method of new request; default is from the original env. :param path: HTTP path of new request; default is from the original env. path should be compatible with what you would send to Request.blank. path should be quoted and it can include a query string. for example: '/a%20space?unicode_str%E8%AA%9E=y%20es' :param body: HTTP body of new request; empty by default. :param headers: Extra HTTP headers of new request; None by default. :param agent: The HTTP user agent to use; default 'Swift'. You can put %(orig)s in the agent to have it replaced with the original env's HTTP_USER_AGENT, such as '%(orig)s StaticWeb'. You also set agent to None to use the original env's HTTP_USER_AGENT or '' to have no HTTP_USER_AGENT. :param swift_source: Used to mark the request as originating out of middleware. Will be logged in proxy logs. :param make_env: make_subrequest calls this make_env to help build the swob.Request. :returns: Fresh swob.Request object. """ query_string = None path = path or '' if path and '?' in path: path, query_string = path.split('?', 1) newenv = make_env(env, method, path=wsgi_unquote(path), agent=agent, query_string=query_string, swift_source=swift_source) if not headers: headers = {} if body: return Request.blank(path, environ=newenv, body=body, headers=headers) else: return Request.blank(path, environ=newenv, headers=headers)
def object_request(self, req, api_version, account, container, obj): """ Handle request for object resource. Note that account, container, obj should be unquoted by caller if the url path is under url encoding (e.g. %FF) :param req: swift.common.swob.Request instance :param api_version: should be v1 unless swift bumps api version :param account: account name string :param container: container name string :param object: object name string """ resp = None container_info = get_container_info(req.environ, self.app, swift_source='OV') versions_cont = container_info.get('sysmeta', {}).get('versions-container', '') is_enabled = config_true_value( container_info.get('sysmeta', {}).get('versions-enabled')) if versions_cont: versions_cont = wsgi_unquote( str_to_wsgi(versions_cont)).split('/')[0] if req.params.get('version-id'): vw_ctx = OioObjectContext(self.app, self.logger) resp = vw_ctx.handle_versioned_request(req, versions_cont, api_version, account, container, obj, is_enabled, req.params['version-id']) elif versions_cont: # handle object request for a enabled versioned container vw_ctx = OioObjectContext(self.app, self.logger) resp = vw_ctx.handle_request(req, versions_cont, api_version, account, container, obj, is_enabled) if resp: return resp else: return self.app
def handle_request(self, req, start_response): """ Take a GET or HEAD request, and if it is for a dynamic large object manifest, return an appropriate response. Otherwise, simply pass it through. """ resp_iter = self._app_call(req.environ) # make sure this response is for a dynamic large object manifest for header, value in self._response_headers: if (header.lower() == 'x-object-manifest'): close_if_possible(resp_iter) response = self.get_or_head_response( req, wsgi_to_str(wsgi_unquote(value))) return response(req.environ, start_response) # Not a dynamic large object manifest; just pass it through. start_response(self._response_status, self._response_headers, self._response_exc_info) return resp_iter
def check_path_header(req, name, length, error_msg): """ Validate that the value of path-like header is well formatted. We assume the caller ensures that specific header is present in req.headers. :param req: HTTP request object :param name: header name :param length: length of path segment check :param error_msg: error message for client :returns: A tuple with path parts according to length :raise: HTTPPreconditionFailed if header value is not well formatted. """ hdr = wsgi_unquote(req.headers.get(name)) if not hdr.startswith('/'): hdr = '/' + hdr try: return split_path(hdr, length, length, True) except ValueError: raise HTTPPreconditionFailed(request=req, body=error_msg)
def check_path_header(req, name, length, error_msg): """ Validate that the value of path-like header is well formatted. We assume the caller ensures that specific header is present in req.headers. :param req: HTTP request object :param name: header name :param length: length of path segment check :param error_msg: error message for client :returns: A tuple with path parts according to length :raise: HTTPPreconditionFailed if header value is not well formatted. """ hdr = wsgi_unquote(req.headers.get(name)) if not hdr.startswith('/'): hdr = '/' + hdr try: return split_path(hdr, length, length, True) except ValueError: raise HTTPPreconditionFailed( request=req, body=error_msg)
def handle_PUT(self, req, start_response): if req.content_length: return HTTPBadRequest(body='Copy requests require a zero byte ' 'body', request=req, content_type='text/plain')(req.environ, start_response) # Form the path of source object to be fetched ver, acct, _rest = req.split_path(2, 3, True) src_account_name = req.headers.get('X-Copy-From-Account') if src_account_name: src_account_name = check_account_format( req, wsgi_unquote(src_account_name)) else: src_account_name = acct src_container_name, src_obj_name = _check_copy_from_header(req) source_path = '/%s/%s/%s/%s' % (ver, src_account_name, src_container_name, src_obj_name) # GET the source object, bail out on error ssc_ctx = ServerSideCopyWebContext(self.app, self.logger) source_resp = self._get_source_object(ssc_ctx, source_path, req) if source_resp.status_int >= HTTP_MULTIPLE_CHOICES: return source_resp(source_resp.environ, start_response) # Create a new Request object based on the original request instance. # This will preserve original request environ including headers. sink_req = Request.blank(req.path_info, environ=req.environ) def is_object_sysmeta(k): return is_sys_meta('object', k) if config_true_value(req.headers.get('x-fresh-metadata', 'false')): # x-fresh-metadata only applies to copy, not post-as-copy: ignore # existing user metadata, update existing sysmeta with new copy_header_subset(source_resp, sink_req, is_object_sysmeta) copy_header_subset(req, sink_req, is_object_sysmeta) else: # First copy existing sysmeta, user meta and other headers from the # source to the sink, apart from headers that are conditionally # copied below and timestamps. exclude_headers = ('x-static-large-object', 'x-object-manifest', 'etag', 'content-type', 'x-timestamp', 'x-backend-timestamp') copy_header_subset(source_resp, sink_req, lambda k: k.lower() not in exclude_headers) # now update with original req headers sink_req.headers.update(req.headers) params = sink_req.params if params.get('multipart-manifest') == 'get': if 'X-Static-Large-Object' in source_resp.headers: params['multipart-manifest'] = 'put' if 'X-Object-Manifest' in source_resp.headers: del params['multipart-manifest'] sink_req.headers['X-Object-Manifest'] = \ source_resp.headers['X-Object-Manifest'] sink_req.params = params # Set swift.source, data source, content length and etag # for the PUT request sink_req.environ['swift.source'] = 'SSC' sink_req.environ['wsgi.input'] = FileLikeIter(source_resp.app_iter) sink_req.content_length = source_resp.content_length if (source_resp.status_int == HTTP_OK and 'X-Static-Large-Object' not in source_resp.headers and ('X-Object-Manifest' not in source_resp.headers or req.params.get('multipart-manifest') == 'get')): # copy source etag so that copied content is verified, unless: # - not a 200 OK response: source etag may not match the actual # content, for example with a 206 Partial Content response to a # ranged request # - SLO manifest: etag cannot be specified in manifest PUT; SLO # generates its own etag value which may differ from source # - SLO: etag in SLO response is not hash of actual content # - DLO: etag in DLO response is not hash of actual content sink_req.headers['Etag'] = source_resp.etag else: # since we're not copying the source etag, make sure that any # container update override values are not copied. remove_items(sink_req.headers, lambda k: k.startswith( 'X-Object-Sysmeta-Container-Update-Override-')) # We no longer need these headers sink_req.headers.pop('X-Copy-From', None) sink_req.headers.pop('X-Copy-From-Account', None) # If the copy request does not explicitly override content-type, # use the one present in the source object. if not req.headers.get('content-type'): sink_req.headers['Content-Type'] = \ source_resp.headers['Content-Type'] # Create response headers for PUT response resp_headers = self._create_response_headers(source_path, source_resp, sink_req) put_resp = ssc_ctx.send_put_req(sink_req, resp_headers, start_response) close_if_possible(source_resp.app_iter) return put_resp
def handle_PUT(self, req, start_response): if req.content_length: return HTTPBadRequest(body='Copy requests require a zero byte ' 'body', request=req, content_type='text/plain')(req.environ, start_response) # Form the path of source object to be fetched ver, acct, _rest = req.split_path(2, 3, True) src_account_name = req.headers.get('X-Copy-From-Account') if src_account_name: src_account_name = check_account_format( req, wsgi_unquote(src_account_name)) else: src_account_name = acct src_container_name, src_obj_name = _check_copy_from_header(req) source_path = '/%s/%s/%s/%s' % (ver, src_account_name, src_container_name, src_obj_name) # GET the source object, bail out on error ssc_ctx = ServerSideCopyWebContext(self.app, self.logger) source_resp = self._get_source_object(ssc_ctx, source_path, req) if source_resp.status_int >= HTTP_MULTIPLE_CHOICES: return source_resp(source_resp.environ, start_response) # Create a new Request object based on the original request instance. # This will preserve original request environ including headers. sink_req = Request.blank(req.path_info, environ=req.environ) def is_object_sysmeta(k): return is_sys_meta('object', k) if config_true_value(req.headers.get('x-fresh-metadata', 'false')): # x-fresh-metadata only applies to copy, not post-as-copy: ignore # existing user metadata, update existing sysmeta with new copy_header_subset(source_resp, sink_req, is_object_sysmeta) copy_header_subset(req, sink_req, is_object_sysmeta) else: # First copy existing sysmeta, user meta and other headers from the # source to the sink, apart from headers that are conditionally # copied below and timestamps. exclude_headers = ('x-static-large-object', 'x-object-manifest', 'etag', 'content-type', 'x-timestamp', 'x-backend-timestamp') copy_header_subset(source_resp, sink_req, lambda k: k.lower() not in exclude_headers) # now update with original req headers sink_req.headers.update(req.headers) params = sink_req.params params_updated = False if params.get('multipart-manifest') == 'get': if 'X-Static-Large-Object' in source_resp.headers: params['multipart-manifest'] = 'put' if 'X-Object-Manifest' in source_resp.headers: del params['multipart-manifest'] sink_req.headers['X-Object-Manifest'] = \ source_resp.headers['X-Object-Manifest'] params_updated = True if 'version-id' in params: del params['version-id'] params_updated = True if params_updated: sink_req.params = params # Set swift.source, data source, content length and etag # for the PUT request sink_req.environ['swift.source'] = 'SSC' sink_req.environ['wsgi.input'] = FileLikeIter(source_resp.app_iter) sink_req.content_length = source_resp.content_length if (source_resp.status_int == HTTP_OK and 'X-Static-Large-Object' not in source_resp.headers and ('X-Object-Manifest' not in source_resp.headers or req.params.get('multipart-manifest') == 'get')): # copy source etag so that copied content is verified, unless: # - not a 200 OK response: source etag may not match the actual # content, for example with a 206 Partial Content response to a # ranged request # - SLO manifest: etag cannot be specified in manifest PUT; SLO # generates its own etag value which may differ from source # - SLO: etag in SLO response is not hash of actual content # - DLO: etag in DLO response is not hash of actual content sink_req.headers['Etag'] = source_resp.etag else: # since we're not copying the source etag, make sure that any # container update override values are not copied. remove_items( sink_req.headers, lambda k: k.startswith( OBJECT_SYSMETA_CONTAINER_UPDATE_OVERRIDE_PREFIX.title())) # We no longer need these headers sink_req.headers.pop('X-Copy-From', None) sink_req.headers.pop('X-Copy-From-Account', None) # If the copy request does not explicitly override content-type, # use the one present in the source object. if not req.headers.get('content-type'): sink_req.headers['Content-Type'] = \ source_resp.headers['Content-Type'] # Create response headers for PUT response resp_headers = self._create_response_headers(source_path, source_resp, sink_req) put_resp = ssc_ctx.send_put_req(sink_req, resp_headers, start_response) close_if_possible(source_resp.app_iter) return put_resp
def handle_versioned_request(self, req, versions_cont, api_version, account, container, obj, is_enabled, version): """ Handle 'version-id' request for object resource. When a request contains a ``version-id=<id>`` parameter, the request is acted upon the actual version of that object. Version-aware operations require that the container is versioned, but do not require that the versioning is currently enabled. Users should be able to operate on older versions of an object even if versioning is currently suspended. PUT and POST requests are not allowed as that would overwrite the contents of the versioned object. :param req: The original request :param versions_cont: container holding versions of the requested obj :param api_version: should be v1 unless swift bumps api version :param account: account name string :param container: container name string :param object: object name string :param is_enabled: is versioning currently enabled :param version: version of the object to act on """ if not versions_cont and version != 'null': raise HTTPBadRequest( 'version-aware operations require that the container is ' 'versioned', request=req) req.environ['oio.query'] = {'version': version} if version != 'null': try: int(version) except ValueError: raise HTTPBadRequest('Invalid version parameter', request=req) if req.method == 'DELETE': return self.handle_delete_version(req, versions_cont, api_version, account, container, obj, is_enabled, version) elif req.method == 'PUT': return self.handle_put_version(req, versions_cont, api_version, account, container, obj, is_enabled, version) if version == 'null': resp = req.get_response(self.app) if resp.is_success: if get_reserved_name('versions', '') in wsgi_unquote( resp.headers.get('Content-Location', '')): # Have a latest version, but it's got a real version-id. # Since the user specifically asked for null, return 404 close_if_possible(resp.app_iter) raise HTTPNotFound(request=req) resp.headers['X-Object-Version-Id'] = 'null' if req.method == 'HEAD': drain_and_close(resp) return resp else: resp = req.get_response(self.app) if resp.is_success: resp.headers['X-Object-Version-Id'] = version # Well, except for some delete marker business... is_del_marker = DELETE_MARKER_CONTENT_TYPE == resp.headers.get( 'X-Backend-Content-Type', resp.headers['Content-Type']) if req.method == 'HEAD': drain_and_close(resp) if is_del_marker: hdrs = { 'X-Object-Version-Id': version, 'Content-Type': DELETE_MARKER_CONTENT_TYPE } raise HTTPNotFound(request=req, headers=hdrs) return resp
def handle_request(self, req, start_response): """ Handle request for container resource. On PUT, POST set version location and enabled flag sysmeta. For container listings of a versioned container, update the object's bytes and etag to use the target's instead of using the symlink info. """ # FIXME(FVE): this request is not needed when handling # list-object-versions requests. app_resp = self._app_call(req.environ) _, account, container, _ = req.split_path(3, 4, True) location = '' curr_bytes = 0 bytes_idx = -1 for i, (header, value) in enumerate(self._response_headers): if header == 'X-Container-Bytes-Used': curr_bytes = value bytes_idx = i if header.lower() == SYSMETA_VERSIONS_CONT: location = value if header.lower() == SYSMETA_VERSIONS_ENABLED: self._response_headers.extend([ (CLIENT_VERSIONS_ENABLED.title(), value) ]) if location: location = wsgi_unquote(location) # update bytes header if bytes_idx > -1: head_req = make_pre_authed_request( req.environ, method='HEAD', swift_source='OV', path=wsgi_quote('/v1/%s/%s' % (account, location)), headers={'X-Backend-Allow-Reserved-Names': 'true'}) vresp = head_req.get_response(self.app) if vresp.is_success: ver_bytes = vresp.headers.get('X-Container-Bytes-Used', 0) self._response_headers[bytes_idx] = ( 'X-Container-Bytes-Used', str(int(curr_bytes) + int(ver_bytes))) drain_and_close(vresp) elif is_success(self._get_status_int()): # If client is doing a version-aware listing for a container that # (as best we could tell) has never had versioning enabled, # err on the side of there being data anyway -- the metadata we # found may not be the most up-to-date. # Note that any extra listing request we make will likely 404. try: location = self._build_versions_container_name(container) except ValueError: # may be internal listing to a reserved namespace container pass # else, we won't need location anyway if req.method == 'GET' and 'versions' in req.params: return self._list_versions(req, start_response, container) start_response(self._response_status, self._response_headers, self._response_exc_info) return app_resp