def _perform_subrequest(self, env, start_response, attributes, fp, key): """ Performs the subrequest and returns a new response. :param env: The WSGI environment dict. :param start_response: The WSGI start_response hook. :param attributes: dict of the attributes of the form so far. :param fp: The file-like object containing the request body. :param key: The account key to validate the signature with. :returns: Response as per WSGI. """ if not key: return '401 Unauthorized', 'invalid signature' try: max_file_size = int(attributes.get('max_file_size') or 0) except ValueError: raise FormInvalid('max_file_size not an integer') subenv = { 'REQUEST_METHOD': 'PUT', 'SCRIPT_NAME': '', 'SERVER_NAME': env['SERVER_NAME'], 'SERVER_PORT': env['SERVER_PORT'], 'SERVER_PROTOCOL': env['SERVER_PROTOCOL'], 'HTTP_TRANSFER_ENCODING': 'chunked', 'wsgi.input': _CappedFileLikeObject(fp, max_file_size), 'swift.cache': env['swift.cache'] } subenv['PATH_INFO'] = env['PATH_INFO'] if subenv['PATH_INFO'][-1] != '/' and \ subenv['PATH_INFO'].count('/') < 4: subenv['PATH_INFO'] += '/' subenv['PATH_INFO'] += attributes['filename'] or 'filename' if 'content-type' in attributes: subenv['CONTENT_TYPE'] = \ attributes['content-type'] or 'application/octet-stream' try: if int(attributes.get('expires') or 0) < time(): return '401 Unauthorized', 'form expired' except ValueError: raise FormInvalid('expired not an integer') hmac_body = '%s\n%s\n%s\n%s\n%s' % ( env['PATH_INFO'], attributes.get('redirect') or '', attributes.get('max_file_size') or '0', attributes.get('max_file_count') or '0', attributes.get('expires') or '0') sig = hmac.new(key, hmac_body, sha1).hexdigest() if not streq_const_time(sig, (attributes.get('signature') or 'invalid')): return '401 Unauthorized', 'invalid signature' subenv['swift.authorize'] = lambda req: None subenv['swift.authorize_override'] = True subenv['REMOTE_USER'] = '******' substatus = [None] def _start_response(status, headers, exc_info=None): substatus[0] = status self.app(subenv, _start_response) return substatus[0], ''
def _perform_subrequest(self, orig_env, attributes, fp, key): """ Performs the subrequest and returns the response. :param orig_env: The WSGI environment dict; will only be used to form a new env for the subrequest. :param attributes: dict of the attributes of the form so far. :param fp: The file-like object containing the request body. :param key: The account key to validate the signature with. :returns: (status_line, message) """ if not key: return '401 Unauthorized', 'invalid signature' try: max_file_size = int(attributes.get('max_file_size') or 0) except ValueError: raise FormInvalid('max_file_size not an integer') subenv = make_pre_authed_env(orig_env, 'PUT', agent=None, swift_source='FP') if 'QUERY_STRING' in subenv: del subenv['QUERY_STRING'] subenv['HTTP_TRANSFER_ENCODING'] = 'chunked' subenv['wsgi.input'] = _CappedFileLikeObject(fp, max_file_size) if subenv['PATH_INFO'][-1] != '/' and \ subenv['PATH_INFO'].count('/') < 4: subenv['PATH_INFO'] += '/' subenv['PATH_INFO'] += attributes['filename'] or 'filename' if 'content-type' in attributes: subenv['CONTENT_TYPE'] = \ attributes['content-type'] or 'application/octet-stream' elif 'CONTENT_TYPE' in subenv: del subenv['CONTENT_TYPE'] try: if int(attributes.get('expires') or 0) < time(): return '401 Unauthorized', 'form expired' except ValueError: raise FormInvalid('expired not an integer') hmac_body = '%s\n%s\n%s\n%s\n%s' % ( orig_env['PATH_INFO'], attributes.get('redirect') or '', attributes.get('max_file_size') or '0', attributes.get('max_file_count') or '0', attributes.get('expires') or '0') sig = hmac.new(key, hmac_body, sha1).hexdigest() if not streq_const_time(sig, (attributes.get('signature') or 'invalid')): return '401 Unauthorized', 'invalid signature' substatus = [None] def _start_response(status, headers, exc_info=None): substatus[0] = status i = iter(self.app(subenv, _start_response)) try: i.next() except StopIteration: pass return substatus[0], ''
def _perform_subrequest(self, env, start_response, attributes, fp, key): """ Performs the subrequest and returns a new response. :param env: The WSGI environment dict. :param start_response: The WSGI start_response hook. :param attributes: dict of the attributes of the form so far. :param fp: The file-like object containing the request body. :param key: The account key to validate the signature with. :returns: Response as per WSGI. """ if not key: return '401 Unauthorized', 'invalid signature' try: max_file_size = int(attributes.get('max_file_size') or 0) except ValueError: raise FormInvalid('max_file_size not an integer') subenv = {'REQUEST_METHOD': 'PUT', 'SCRIPT_NAME': '', 'SERVER_NAME': env['SERVER_NAME'], 'SERVER_PORT': env['SERVER_PORT'], 'SERVER_PROTOCOL': env['SERVER_PROTOCOL'], 'HTTP_TRANSFER_ENCODING': 'chunked', 'wsgi.input': _CappedFileLikeObject(fp, max_file_size), 'swift.cache': env['swift.cache']} subenv['PATH_INFO'] = env['PATH_INFO'] if subenv['PATH_INFO'][-1] != '/' and \ subenv['PATH_INFO'].count('/') < 4: subenv['PATH_INFO'] += '/' subenv['PATH_INFO'] += attributes['filename'] or 'filename' if 'content-type' in attributes: subenv['CONTENT_TYPE'] = \ attributes['content-type'] or 'application/octet-stream' try: if int(attributes.get('expires') or 0) < time(): return '401 Unauthorized', 'form expired' except ValueError: raise FormInvalid('expired not an integer') hmac_body = '%s\n%s\n%s\n%s\n%s' % ( env['PATH_INFO'], attributes.get('redirect') or '', attributes.get('max_file_size') or '0', attributes.get('max_file_count') or '0', attributes.get('expires') or '0' ) sig = hmac.new(key, hmac_body, sha1).hexdigest() if not streq_const_time(sig, (attributes.get('signature') or 'invalid')): return '401 Unauthorized', 'invalid signature' subenv['swift.authorize'] = lambda req: None subenv['swift.authorize_override'] = True subenv['REMOTE_USER'] = '******' substatus = [None] def _start_response(status, headers, exc_info=None): substatus[0] = status self.app(subenv, _start_response) return substatus[0], ''
def GETorHEAD(self, req): """Handler for HTTP GET/HEAD requests.""" """ Handles requests to /info Should return a WSGI-style callable (such as swob.Response). :param req: swob.Request object """ if not self.expose_info: return HTTPForbidden(request=req) admin_request = False sig = req.params.get('swiftinfo_sig', '') expires = req.params.get('swiftinfo_expires', '') if sig != '' or expires != '': admin_request = True if not self.admin_key: return HTTPForbidden(request=req) try: expires = int(expires) except ValueError: return HTTPUnauthorized(request=req) if expires < time(): return HTTPUnauthorized(request=req) valid_sigs = [] for method in self.allowed_hmac_methods[req.method]: valid_sigs.append(get_hmac(method, '/info', expires, self.admin_key)) # While it's true that any() will short-circuit, this doesn't # affect the timing-attack resistance since the only way this will # short-circuit is when a valid signature is passed in. is_valid_hmac = any(streq_const_time(valid_sig, sig) for valid_sig in valid_sigs) if not is_valid_hmac: return HTTPUnauthorized(request=req) headers = {} if 'Origin' in req.headers: headers['Access-Control-Allow-Origin'] = req.headers['Origin'] headers['Access-Control-Expose-Headers'] = ', '.join( ['x-trans-id']) #json.dumps(dict)可以将字典形式的dict对象转换为json格式的对象 info = json.dumps(get_swift_info( admin=admin_request, disallowed_sections=self.disallowed_sections)) return HTTPOk(request=req, headers=headers, body=info, content_type='application/json; charset=UTF-8')
def GETorHEAD(self, req): """Handler for HTTP GET/HEAD requests.""" """ Handles requests to /info Should return a WSGI-style callable (such as swob.Response). :param req: swob.Request object """ if not self.expose_info: return HTTPForbidden(request=req) admin_request = False sig = req.params.get('swiftinfo_sig', '') expires = req.params.get('swiftinfo_expires', '') if sig != '' or expires != '': admin_request = True if not self.admin_key: return HTTPForbidden(request=req) try: expires = int(expires) except ValueError: return HTTPUnauthorized(request=req) if expires < time(): return HTTPUnauthorized(request=req) valid_sigs = [] for method in self.allowed_hmac_methods[req.method]: valid_sigs.append( get_hmac(method, '/info', expires, self.admin_key)) # While it's true that any() will short-circuit, this doesn't # affect the timing-attack resistance since the only way this will # short-circuit is when a valid signature is passed in. is_valid_hmac = any( streq_const_time(valid_sig, sig) for valid_sig in valid_sigs) if not is_valid_hmac: return HTTPUnauthorized(request=req) headers = {} if 'Origin' in req.headers: headers['Access-Control-Allow-Origin'] = req.headers['Origin'] headers['Access-Control-Expose-Headers'] = ', '.join( ['x-trans-id']) info = json.dumps( get_swift_info(admin=admin_request, disallowed_sections=self.disallowed_sections)) return HTTPOk(request=req, headers=headers, body=info, content_type='application/json; charset=UTF-8')
def __call__(self, req): if not self.allow_full_urls: sync_to = req.headers.get('x-container-sync-to') if sync_to and not sync_to.startswith('//'): raise HTTPBadRequest( body='Full URLs are not allowed for X-Container-Sync-To ' 'values. Only realm values of the format ' '//realm/cluster/account/container are allowed.\n', request=req) auth = req.headers.get('x-container-sync-auth') if auth: valid = False auth = auth.split() if len(auth) != 3: req.environ.setdefault('swift.log_info', []).append( 'cs:not-3-args') else: realm, nonce, sig = auth realm_key = self.realms_conf.key(realm) realm_key2 = self.realms_conf.key2(realm) if not realm_key: req.environ.setdefault('swift.log_info', []).append( 'cs:no-local-realm-key') else: info = get_container_info( req.environ, self.app, swift_source='CS') user_key = info.get('sync_key') if not user_key: req.environ.setdefault('swift.log_info', []).append( 'cs:no-local-user-key') else: expected = self.realms_conf.get_sig( req.method, req.path, req.headers.get('x-timestamp', '0'), nonce, realm_key, user_key) expected2 = self.realms_conf.get_sig( req.method, req.path, req.headers.get('x-timestamp', '0'), nonce, realm_key2, user_key) if realm_key2 else expected if not streq_const_time(sig, expected) and \ not streq_const_time(sig, expected2): req.environ.setdefault( 'swift.log_info', []).append('cs:invalid-sig') else: req.environ.setdefault( 'swift.log_info', []).append('cs:valid') valid = True if not valid: exc = HTTPUnauthorized( body='X-Container-Sync-Auth header not valid; ' 'contact cluster operator for support.', headers={'content-type': 'text/plain'}, request=req) exc.headers['www-authenticate'] = ' '.join([ 'SwiftContainerSync', exc.www_authenticate().split(None, 1)[1]]) raise exc else: req.environ['swift.authorize_override'] = True if req.path == '/info': # Ensure /info requests get the freshest results dct = {} for realm in self.realms_conf.realms(): clusters = self.realms_conf.clusters(realm) if clusters: dct[realm] = {'clusters': dict((c, {}) for c in clusters)} register_swift_info('container_sync', realms=dct) return self.app
def _perform_subrequest(self, orig_env, attributes, fp, keys): """ Performs the subrequest and returns the response. :param orig_env: The WSGI environment dict; will only be used to form a new env for the subrequest. :param attributes: dict of the attributes of the form so far. :param fp: The file-like object containing the request body. :param keys: The account keys to validate the signature with. :returns: (status_line, headers_list, message) """ if not keys: raise FormUnauthorized("invalid signature") try: max_file_size = int(attributes.get("max_file_size") or 0) except ValueError: raise FormInvalid("max_file_size not an integer") subenv = make_pre_authed_env(orig_env, "PUT", agent=None, swift_source="FP") if "QUERY_STRING" in subenv: del subenv["QUERY_STRING"] subenv["HTTP_TRANSFER_ENCODING"] = "chunked" subenv["wsgi.input"] = _CappedFileLikeObject(fp, max_file_size) if subenv["PATH_INFO"][-1] != "/" and subenv["PATH_INFO"].count("/") < 4: subenv["PATH_INFO"] += "/" subenv["PATH_INFO"] += attributes["filename"] or "filename" if "x_delete_at" in attributes: try: subenv["HTTP_X_DELETE_AT"] = int(attributes["x_delete_at"]) except ValueError: raise FormInvalid("x_delete_at not an integer: " "Unix timestamp required.") if "x_delete_after" in attributes: try: subenv["HTTP_X_DELETE_AFTER"] = int(attributes["x_delete_after"]) except ValueError: raise FormInvalid("x_delete_after not an integer: " "Number of seconds required.") if "content-type" in attributes: subenv["CONTENT_TYPE"] = attributes["content-type"] or "application/octet-stream" elif "CONTENT_TYPE" in subenv: del subenv["CONTENT_TYPE"] try: if int(attributes.get("expires") or 0) < time(): raise FormUnauthorized("form expired") except ValueError: raise FormInvalid("expired not an integer") hmac_body = "%s\n%s\n%s\n%s\n%s" % ( orig_env["PATH_INFO"], attributes.get("redirect") or "", attributes.get("max_file_size") or "0", attributes.get("max_file_count") or "0", attributes.get("expires") or "0", ) has_valid_sig = False for key in keys: sig = hmac.new(key, hmac_body, sha1).hexdigest() if streq_const_time(sig, (attributes.get("signature") or "invalid")): has_valid_sig = True if not has_valid_sig: raise FormUnauthorized("invalid signature") substatus = [None] subheaders = [None] wsgi_input = subenv["wsgi.input"] def _start_response(status, headers, exc_info=None): if wsgi_input.file_size_exceeded: raise EOFError("max_file_size exceeded") substatus[0] = status subheaders[0] = headers i = iter(self.app(subenv, _start_response)) try: i.next() except StopIteration: pass return substatus[0], subheaders[0], ""
def _perform_subrequest(self, orig_env, attributes, fp, keys): """ Performs the subrequest and returns the response. :param orig_env: The WSGI environment dict; will only be used to form a new env for the subrequest. :param attributes: dict of the attributes of the form so far. :param fp: The file-like object containing the request body. :param keys: The account keys to validate the signature with. :returns: (status_line, headers_list) """ if not keys: raise FormUnauthorized('invalid signature') try: max_file_size = int(attributes.get('max_file_size') or 0) except ValueError: raise FormInvalid('max_file_size not an integer') subenv = make_pre_authed_env(orig_env, 'PUT', agent=None, swift_source='FP') if 'QUERY_STRING' in subenv: del subenv['QUERY_STRING'] subenv['HTTP_TRANSFER_ENCODING'] = 'chunked' subenv['wsgi.input'] = _CappedFileLikeObject(fp, max_file_size) if not subenv['PATH_INFO'].endswith('/') and \ subenv['PATH_INFO'].count('/') < 4: subenv['PATH_INFO'] += '/' subenv['PATH_INFO'] += attributes['filename'] or 'filename' if 'x_delete_at' in attributes: try: subenv['HTTP_X_DELETE_AT'] = int(attributes['x_delete_at']) except ValueError: raise FormInvalid('x_delete_at not an integer: ' 'Unix timestamp required.') if 'x_delete_after' in attributes: try: subenv['HTTP_X_DELETE_AFTER'] = int( attributes['x_delete_after']) except ValueError: raise FormInvalid('x_delete_after not an integer: ' 'Number of seconds required.') if 'content-type' in attributes: subenv['CONTENT_TYPE'] = \ attributes['content-type'] or 'application/octet-stream' if 'content-encoding' in attributes: subenv['HTTP_CONTENT_ENCODING'] = attributes['content-encoding'] try: if int(attributes.get('expires') or 0) < time(): raise FormUnauthorized('form expired') except ValueError: raise FormInvalid('expired not an integer') hmac_body = '%s\n%s\n%s\n%s\n%s' % ( orig_env['PATH_INFO'], attributes.get('redirect') or '', attributes.get('max_file_size') or '0', attributes.get('max_file_count') or '0', attributes.get('expires') or '0') if six.PY3: hmac_body = hmac_body.encode('utf-8') has_valid_sig = False for key in keys: sig = hmac.new(key, hmac_body, sha1).hexdigest() if streq_const_time(sig, (attributes.get('signature') or 'invalid')): has_valid_sig = True if not has_valid_sig: raise FormUnauthorized('invalid signature') substatus = [None] subheaders = [None] wsgi_input = subenv['wsgi.input'] def _start_response(status, headers, exc_info=None): if wsgi_input.file_size_exceeded: raise EOFError("max_file_size exceeded") substatus[0] = status subheaders[0] = headers # reiterate to ensure the response started, # but drop any data on the floor close_if_possible(reiterate(self.app(subenv, _start_response))) return substatus[0], subheaders[0]
def __call__(self, req): if not self.allow_full_urls: sync_to = req.headers.get('x-container-sync-to') if sync_to and not sync_to.startswith('//'): raise HTTPBadRequest( body='Full URLs are not allowed for X-Container-Sync-To ' 'values. Only realm values of the format ' '//realm/cluster/account/container are allowed.\n', request=req) auth = req.headers.get('x-container-sync-auth') if auth: valid = False auth = auth.split() if len(auth) != 3: req.environ.setdefault('swift.log_info', []).append('cs:not-3-args') else: realm, nonce, sig = auth realm_key = self.realms_conf.key(realm) realm_key2 = self.realms_conf.key2(realm) if not realm_key: req.environ.setdefault('swift.log_info', []).append('cs:no-local-realm-key') else: info = get_container_info(req.environ, self.app, swift_source='CS') user_key = info.get('sync_key') if not user_key: req.environ.setdefault( 'swift.log_info', []).append('cs:no-local-user-key') else: # x-timestamp headers get shunted by gatekeeper if 'x-backend-inbound-x-timestamp' in req.headers: req.headers['x-timestamp'] = req.headers.pop( 'x-backend-inbound-x-timestamp') expected = self.realms_conf.get_sig( req.method, req.path, req.headers.get('x-timestamp', '0'), nonce, realm_key, user_key) expected2 = self.realms_conf.get_sig( req.method, req.path, req.headers.get('x-timestamp', '0'), nonce, realm_key2, user_key) if realm_key2 else expected if not streq_const_time(sig, expected) and \ not streq_const_time(sig, expected2): req.environ.setdefault('swift.log_info', []).append('cs:invalid-sig') else: req.environ.setdefault('swift.log_info', []).append('cs:valid') valid = True if not valid: exc = HTTPUnauthorized( body='X-Container-Sync-Auth header not valid; ' 'contact cluster operator for support.', headers={'content-type': 'text/plain'}, request=req) exc.headers['www-authenticate'] = ' '.join([ 'SwiftContainerSync', exc.www_authenticate().split(None, 1)[1] ]) raise exc else: req.environ['swift.authorize_override'] = True # An SLO manifest will already be in the internal manifest # syntax and might be synced before its segments, so stop SLO # middleware from performing the usual manifest validation. req.environ['swift.slo_override'] = True if req.path == '/info': # Ensure /info requests get the freshest results self.register_info() return self.app
def __call__(self, env, start_response): """ Main hook into the WSGI paste.deploy filter/app pipeline. :param env: The WSGI environment dict. :param start_response: The WSGI start_response hook. :returns: Response as per WSGI. """ if env['REQUEST_METHOD'] == 'OPTIONS': return self.app(env, start_response) info = self._get_temp_url_info(env) temp_url_sig, temp_url_expires, temp_url_prefix, filename,\ inline_disposition = info if temp_url_sig is None and temp_url_expires is None: return self.app(env, start_response) if not temp_url_sig or not temp_url_expires: return self._invalid(env, start_response) account, container, obj = self._get_path_parts(env) if not account: return self._invalid(env, start_response) keys = self._get_keys(env) if not keys: return self._invalid(env, start_response) if temp_url_prefix is None: path = '/v1/%s/%s/%s' % (account, container, obj) else: if not obj.startswith(temp_url_prefix): return self._invalid(env, start_response) path = 'prefix:/v1/%s/%s/%s' % (account, container, temp_url_prefix) if env['REQUEST_METHOD'] == 'HEAD': hmac_vals = ( self._get_hmacs(env, temp_url_expires, path, keys) + self._get_hmacs(env, temp_url_expires, path, keys, request_method='GET') + self._get_hmacs(env, temp_url_expires, path, keys, request_method='POST') + self._get_hmacs(env, temp_url_expires, path, keys, request_method='PUT')) else: hmac_vals = self._get_hmacs(env, temp_url_expires, path, keys) is_valid_hmac = False hmac_scope = None for hmac, scope in hmac_vals: # While it's true that we short-circuit, this doesn't affect the # timing-attack resistance since the only way this will # short-circuit is when a valid signature is passed in. if streq_const_time(temp_url_sig, hmac): is_valid_hmac = True hmac_scope = scope break if not is_valid_hmac: return self._invalid(env, start_response) # disallowed headers prevent accidentally allowing upload of a pointer # to data that the PUT tempurl would not otherwise allow access for. # It should be safe to provide a GET tempurl for data that an # untrusted client just uploaded with a PUT tempurl. resp = self._clean_disallowed_headers(env, start_response) if resp: return resp self._clean_incoming_headers(env) if hmac_scope == ACCOUNT_SCOPE: env['swift.authorize'] = authorize_same_account(account) else: env['swift.authorize'] = authorize_same_container(account, container) env['swift.authorize_override'] = True env['REMOTE_USER'] = '******' qs = {'temp_url_sig': temp_url_sig, 'temp_url_expires': temp_url_expires} if temp_url_prefix is not None: qs['temp_url_prefix'] = temp_url_prefix if filename: qs['filename'] = filename env['QUERY_STRING'] = urlencode(qs) def _start_response(status, headers, exc_info=None): headers = self._clean_outgoing_headers(headers) if env['REQUEST_METHOD'] in ('GET', 'HEAD') and status[0] == '2': # figure out the right value for content-disposition # 1) use the value from the query string # 2) use the value from the object metadata # 3) use the object name (default) out_headers = [] existing_disposition = None for h, v in headers: if h.lower() != 'content-disposition': out_headers.append((h, v)) else: existing_disposition = v if inline_disposition: if filename: disposition_value = disposition_format('inline', filename) else: disposition_value = 'inline' elif filename: disposition_value = disposition_format('attachment', filename) elif existing_disposition: disposition_value = existing_disposition else: name = basename(env['PATH_INFO'].rstrip('/')) disposition_value = disposition_format('attachment', name) # this is probably just paranoia, I couldn't actually get a # newline into existing_disposition value = disposition_value.replace('\n', '%0A') out_headers.append(('Content-Disposition', value)) # include Expires header for better cache-control out_headers.append(('Expires', strftime( "%a, %d %b %Y %H:%M:%S GMT", gmtime(temp_url_expires)))) headers = out_headers return start_response(status, headers, exc_info) return self.app(env, _start_response)
def _perform_subrequest(self, orig_env, attributes, fp, keys): """ Performs the subrequest and returns the response. :param orig_env: The WSGI environment dict; will only be used to form a new env for the subrequest. :param attributes: dict of the attributes of the form so far. :param fp: The file-like object containing the request body. :param keys: The account keys to validate the signature with. :returns: (status_line, headers_list) """ if not keys: raise FormUnauthorized('invalid signature') try: max_file_size = int(attributes.get('max_file_size') or 0) except ValueError: raise FormInvalid('max_file_size not an integer') subenv = make_pre_authed_env(orig_env, 'PUT', agent=None, swift_source='FP') if 'QUERY_STRING' in subenv: del subenv['QUERY_STRING'] subenv['HTTP_TRANSFER_ENCODING'] = 'chunked' subenv['wsgi.input'] = _CappedFileLikeObject(fp, max_file_size) if not subenv['PATH_INFO'].endswith('/') and \ subenv['PATH_INFO'].count('/') < 4: subenv['PATH_INFO'] += '/' subenv['PATH_INFO'] += str_to_wsgi( attributes['filename'] or 'filename') if 'x_delete_at' in attributes: try: subenv['HTTP_X_DELETE_AT'] = int(attributes['x_delete_at']) except ValueError: raise FormInvalid('x_delete_at not an integer: ' 'Unix timestamp required.') if 'x_delete_after' in attributes: try: subenv['HTTP_X_DELETE_AFTER'] = int( attributes['x_delete_after']) except ValueError: raise FormInvalid('x_delete_after not an integer: ' 'Number of seconds required.') if 'content-type' in attributes: subenv['CONTENT_TYPE'] = \ attributes['content-type'] or 'application/octet-stream' if 'content-encoding' in attributes: subenv['HTTP_CONTENT_ENCODING'] = attributes['content-encoding'] try: if int(attributes.get('expires') or 0) < time(): raise FormUnauthorized('form expired') except ValueError: raise FormInvalid('expired not an integer') hmac_body = '%s\n%s\n%s\n%s\n%s' % ( wsgi_to_str(orig_env['PATH_INFO']), attributes.get('redirect') or '', attributes.get('max_file_size') or '0', attributes.get('max_file_count') or '0', attributes.get('expires') or '0') if six.PY3: hmac_body = hmac_body.encode('utf-8') has_valid_sig = False for key in keys: # Encode key like in swift.common.utls.get_hmac. if not isinstance(key, six.binary_type): key = key.encode('utf8') sig = hmac.new(key, hmac_body, sha1).hexdigest() if streq_const_time(sig, (attributes.get('signature') or 'invalid')): has_valid_sig = True if not has_valid_sig: raise FormUnauthorized('invalid signature') substatus = [None] subheaders = [None] wsgi_input = subenv['wsgi.input'] def _start_response(status, headers, exc_info=None): if wsgi_input.file_size_exceeded: raise EOFError("max_file_size exceeded") substatus[0] = status subheaders[0] = headers # reiterate to ensure the response started, # but drop any data on the floor close_if_possible(reiterate(self.app(subenv, _start_response))) return substatus[0], subheaders[0]
def __call__(self, req): if not self.allow_full_urls: sync_to = req.headers.get('x-container-sync-to') if sync_to and not sync_to.startswith('//'): raise HTTPBadRequest( body='Full URLs are not allowed for X-Container-Sync-To ' 'values. Only realm values of the format ' '//realm/cluster/account/container are allowed.\n', request=req) auth = req.headers.get('x-container-sync-auth') if auth: valid = False auth = auth.split() if len(auth) != 3: req.environ.setdefault('swift.log_info', []).append( 'cs:not-3-args') else: realm, nonce, sig = auth realm_key = self.realms_conf.key(realm) realm_key2 = self.realms_conf.key2(realm) if not realm_key: req.environ.setdefault('swift.log_info', []).append( 'cs:no-local-realm-key') else: info = get_container_info( req.environ, self.app, swift_source='CS') user_key = info.get('sync_key') if not user_key: req.environ.setdefault('swift.log_info', []).append( 'cs:no-local-user-key') else: # x-timestamp headers get shunted by gatekeeper if 'x-backend-inbound-x-timestamp' in req.headers: req.headers['x-timestamp'] = req.headers.pop( 'x-backend-inbound-x-timestamp') expected = self.realms_conf.get_sig( req.method, req.path, req.headers.get('x-timestamp', '0'), nonce, realm_key, user_key) expected2 = self.realms_conf.get_sig( req.method, req.path, req.headers.get('x-timestamp', '0'), nonce, realm_key2, user_key) if realm_key2 else expected if not streq_const_time(sig, expected) and \ not streq_const_time(sig, expected2): req.environ.setdefault( 'swift.log_info', []).append('cs:invalid-sig') else: req.environ.setdefault( 'swift.log_info', []).append('cs:valid') valid = True if not valid: exc = HTTPUnauthorized( body='X-Container-Sync-Auth header not valid; ' 'contact cluster operator for support.', headers={'content-type': 'text/plain'}, request=req) exc.headers['www-authenticate'] = ' '.join([ 'SwiftContainerSync', exc.www_authenticate().split(None, 1)[1]]) raise exc else: req.environ['swift.authorize_override'] = True # An SLO manifest will already be in the internal manifest # syntax and might be synced before its segments, so stop SLO # middleware from performing the usual manifest validation. req.environ['swift.slo_override'] = True if req.path == '/info': # Ensure /info requests get the freshest results self.register_info() return self.app
def test_streq_const_time(self): self.assertTrue(utils.streq_const_time('abc123', 'abc123')) self.assertFalse(utils.streq_const_time('a', 'aaaaa')) self.assertFalse(utils.streq_const_time('ABC123', 'abc123'))
def __call__(self, env, start_response): """ Main hook into the WSGI paste.deploy filter/app pipeline. :param env: The WSGI environment dict. :param start_response: The WSGI start_response hook. :returns: Response as per WSGI. """ if env["REQUEST_METHOD"] == "OPTIONS": return self.app(env, start_response) info = self._get_temp_url_info(env) temp_url_sig, temp_url_expires, filename, inline_disposition = info if temp_url_sig is None and temp_url_expires is None: return self.app(env, start_response) if not temp_url_sig or not temp_url_expires: return self._invalid(env, start_response) account, container = self._get_account_and_container(env) if not account: return self._invalid(env, start_response) keys = self._get_keys(env) if not keys: return self._invalid(env, start_response) if env["REQUEST_METHOD"] == "HEAD": hmac_vals = ( self._get_hmacs(env, temp_url_expires, keys) + self._get_hmacs(env, temp_url_expires, keys, request_method="GET") + self._get_hmacs(env, temp_url_expires, keys, request_method="POST") + self._get_hmacs(env, temp_url_expires, keys, request_method="PUT") ) else: hmac_vals = self._get_hmacs(env, temp_url_expires, keys) is_valid_hmac = False hmac_scope = None for hmac, scope in hmac_vals: # While it's true that we short-circuit, this doesn't affect the # timing-attack resistance since the only way this will # short-circuit is when a valid signature is passed in. if streq_const_time(temp_url_sig, hmac): is_valid_hmac = True hmac_scope = scope break if not is_valid_hmac: return self._invalid(env, start_response) # disallowed headers prevent accidently allowing upload of a pointer # to data that the PUT tempurl would not otherwise allow access for. # It should be safe to provide a GET tempurl for data that an # untrusted client just uploaded with a PUT tempurl. resp = self._clean_disallowed_headers(env, start_response) if resp: return resp self._clean_incoming_headers(env) if hmac_scope == ACCOUNT_SCOPE: env["swift.authorize"] = authorize_same_account(account) else: env["swift.authorize"] = authorize_same_container(account, container) env["swift.authorize_override"] = True env["REMOTE_USER"] = "******" qs = {"temp_url_sig": temp_url_sig, "temp_url_expires": temp_url_expires} if filename: qs["filename"] = filename env["QUERY_STRING"] = urlencode(qs) def _start_response(status, headers, exc_info=None): headers = self._clean_outgoing_headers(headers) if env["REQUEST_METHOD"] == "GET" and status[0] == "2": # figure out the right value for content-disposition # 1) use the value from the query string # 2) use the value from the object metadata # 3) use the object name (default) out_headers = [] existing_disposition = None for h, v in headers: if h.lower() != "content-disposition": out_headers.append((h, v)) else: existing_disposition = v if inline_disposition: disposition_value = "inline" elif filename: disposition_value = disposition_format(filename) elif existing_disposition: disposition_value = existing_disposition else: name = basename(env["PATH_INFO"].rstrip("/")) disposition_value = disposition_format(name) # this is probably just paranoia, I couldn't actually get a # newline into existing_disposition value = disposition_value.replace("\n", "%0A") out_headers.append(("Content-Disposition", value)) headers = out_headers return start_response(status, headers, exc_info) return self.app(env, _start_response)
def __call__(self, req): if not self.allow_full_urls: sync_to = req.headers.get('x-container-sync-to') if sync_to and not sync_to.startswith('//'): raise HTTPBadRequest( body='Full URLs are not allowed for X-Container-Sync-To ' 'values. Only realm values of the format ' '//realm/cluster/account/container are allowed.\n', request=req) auth = req.headers.get('x-container-sync-auth') if auth: valid = False auth = auth.split() if len(auth) != 3: req.environ.setdefault('swift.log_info', []).append('cs:not-3-args') else: realm, nonce, sig = auth realm_key = self.realms_conf.key(realm) realm_key2 = self.realms_conf.key2(realm) if not realm_key: req.environ.setdefault('swift.log_info', []).append('cs:no-local-realm-key') else: info = get_container_info(req.environ, self.app, swift_source='CS') user_key = info.get('sync_key') if not user_key: req.environ.setdefault( 'swift.log_info', []).append('cs:no-local-user-key') else: expected = self.realms_conf.get_sig( req.method, req.path, req.headers.get('x-timestamp', '0'), nonce, realm_key, user_key) expected2 = self.realms_conf.get_sig( req.method, req.path, req.headers.get('x-timestamp', '0'), nonce, realm_key2, user_key) if realm_key2 else expected if not streq_const_time(sig, expected) and \ not streq_const_time(sig, expected2): req.environ.setdefault('swift.log_info', []).append('cs:invalid-sig') else: req.environ.setdefault('swift.log_info', []).append('cs:valid') valid = True if not valid: exc = HTTPUnauthorized( body='X-Container-Sync-Auth header not valid; ' 'contact cluster operator for support.', headers={'content-type': 'text/plain'}, request=req) exc.headers['www-authenticate'] = ' '.join([ 'SwiftContainerSync', exc.www_authenticate().split(None, 1)[1] ]) raise exc else: req.environ['swift.authorize_override'] = True if req.path == '/info': # Ensure /info requests get the freshest results dct = {} for realm in self.realms_conf.realms(): clusters = self.realms_conf.clusters(realm) if clusters: dct[realm] = {'clusters': dict((c, {}) for c in clusters)} register_swift_info('container_sync', realms=dct) return self.app
def __call__(self, req): if req.path == '/info': # Ensure /info requests get the freshest results self.register_info() return self.app try: (version, acc, cont, obj) = req.split_path(3, 4, True) bad_path = False except ValueError: bad_path = True # use of bad_path bool is to avoid recursive tracebacks if bad_path or not valid_api_version(version): return self.app # validate container-sync metdata update info = get_container_info(req.environ, self.app, swift_source='CS') sync_to = req.headers.get('x-container-sync-to') if req.method in ('PUT', 'POST') and cont and not obj: versions_cont = info.get('sysmeta', {}).get('versions-container') if sync_to and versions_cont: raise HTTPBadRequest( 'Cannot configure container sync on a container ' 'with object versioning configured.', request=req) if not self.allow_full_urls: if sync_to and not sync_to.startswith('//'): raise HTTPBadRequest( body='Full URLs are not allowed for X-Container-Sync-To ' 'values. Only realm values of the format ' '//realm/cluster/account/container are allowed.\n', request=req) auth = req.headers.get('x-container-sync-auth') if auth: valid = False auth = auth.split() if len(auth) != 3: req.environ.setdefault('swift.log_info', []).append('cs:not-3-args') else: realm, nonce, sig = auth realm_key = self.realms_conf.key(realm) realm_key2 = self.realms_conf.key2(realm) if not realm_key: req.environ.setdefault('swift.log_info', []).append('cs:no-local-realm-key') else: user_key = info.get('sync_key') if not user_key: req.environ.setdefault( 'swift.log_info', []).append('cs:no-local-user-key') else: # x-timestamp headers get shunted by gatekeeper if 'x-backend-inbound-x-timestamp' in req.headers: req.headers['x-timestamp'] = req.headers.pop( 'x-backend-inbound-x-timestamp') expected = self.realms_conf.get_sig( req.method, req.path, req.headers.get('x-timestamp', '0'), nonce, realm_key, user_key) expected2 = self.realms_conf.get_sig( req.method, req.path, req.headers.get('x-timestamp', '0'), nonce, realm_key2, user_key) if realm_key2 else expected if not streq_const_time(sig, expected) and \ not streq_const_time(sig, expected2): req.environ.setdefault('swift.log_info', []).append('cs:invalid-sig') else: req.environ.setdefault('swift.log_info', []).append('cs:valid') valid = True if not valid: exc = HTTPUnauthorized( body='X-Container-Sync-Auth header not valid; ' 'contact cluster operator for support.', headers={'content-type': 'text/plain'}, request=req) exc.headers['www-authenticate'] = ' '.join([ 'SwiftContainerSync', exc.www_authenticate().split(None, 1)[1] ]) raise exc else: req.environ['swift.authorize_override'] = True # An SLO manifest will already be in the internal manifest # syntax and might be synced before its segments, so stop SLO # middleware from performing the usual manifest validation. req.environ['swift.slo_override'] = True # Similar arguments for static symlinks req.environ['swift.symlink_override'] = True return self.app
def __call__(self, env, start_response): """ Main hook into the WSGI paste.deploy filter/app pipeline. :param env: The WSGI environment dict. :param start_response: The WSGI start_response hook. :returns: Response as per WSGI. """ if env['REQUEST_METHOD'] == 'OPTIONS': return self.app(env, start_response) info = self._get_temp_url_info(env) temp_url_sig, temp_url_expires, filename, inline_disposition = info if temp_url_sig is None and temp_url_expires is None: return self.app(env, start_response) if not temp_url_sig or not temp_url_expires: return self._invalid(env, start_response) account = self._get_account(env) if not account: return self._invalid(env, start_response) keys = self._get_keys(env, account) if not keys: return self._invalid(env, start_response) if env['REQUEST_METHOD'] == 'HEAD': hmac_vals = ( self._get_hmacs(env, temp_url_expires, keys) + self._get_hmacs(env, temp_url_expires, keys, request_method='GET') + self._get_hmacs(env, temp_url_expires, keys, request_method='POST') + self._get_hmacs(env, temp_url_expires, keys, request_method='PUT')) else: hmac_vals = self._get_hmacs(env, temp_url_expires, keys) # While it's true that any() will short-circuit, this doesn't affect # the timing-attack resistance since the only way this will # short-circuit is when a valid signature is passed in. is_valid_hmac = any(streq_const_time(temp_url_sig, hmac) for hmac in hmac_vals) if not is_valid_hmac: return self._invalid(env, start_response) self._clean_incoming_headers(env) env['swift.authorize'] = lambda req: None env['swift.authorize_override'] = True env['REMOTE_USER'] = '******' qs = {'temp_url_sig': temp_url_sig, 'temp_url_expires': temp_url_expires} if filename: qs['filename'] = filename env['QUERY_STRING'] = urlencode(qs) def _start_response(status, headers, exc_info=None): headers = self._clean_outgoing_headers(headers) if env['REQUEST_METHOD'] == 'GET' and status[0] == '2': # figure out the right value for content-disposition # 1) use the value from the query string # 2) use the value from the object metadata # 3) use the object name (default) out_headers = [] existing_disposition = None for h, v in headers: if h.lower() != 'content-disposition': out_headers.append((h, v)) else: existing_disposition = v if inline_disposition: disposition_value = 'inline' elif filename: disposition_value = disposition_format(filename) elif existing_disposition: disposition_value = existing_disposition else: name = basename(env['PATH_INFO'].rstrip('/')) disposition_value = disposition_format(name) # this is probably just paranoia, I couldn't actually get a # newline into existing_disposition value = disposition_value.replace('\n', '%0A') out_headers.append(('Content-Disposition', value)) headers = out_headers return start_response(status, headers, exc_info) return self.app(env, _start_response)
def __call__(self, env, start_response): """ Main hook into the WSGI paste.deploy filter/app pipeline. :param env: The WSGI environment dict. :param start_response: The WSGI start_response hook. :returns: Response as per WSGI. """ if env['REQUEST_METHOD'] == 'OPTIONS': return self.app(env, start_response) info = self._get_temp_url_info(env) temp_url_sig, temp_url_expires, filename, inline_disposition = info if temp_url_sig is None and temp_url_expires is None: return self.app(env, start_response) if not temp_url_sig or not temp_url_expires: return self._invalid(env, start_response) account = self._get_account(env) if not account: return self._invalid(env, start_response) keys = self._get_keys(env, account) if not keys: return self._invalid(env, start_response) if env['REQUEST_METHOD'] == 'HEAD': hmac_vals = ( self._get_hmacs(env, temp_url_expires, keys) + self._get_hmacs( env, temp_url_expires, keys, request_method='GET') + self._get_hmacs( env, temp_url_expires, keys, request_method='PUT')) else: hmac_vals = self._get_hmacs(env, temp_url_expires, keys) # While it's true that any() will short-circuit, this doesn't affect # the timing-attack resistance since the only way this will # short-circuit is when a valid signature is passed in. is_valid_hmac = any( streq_const_time(temp_url_sig, hmac) for hmac in hmac_vals) if not is_valid_hmac: return self._invalid(env, start_response) self._clean_incoming_headers(env) env['swift.authorize'] = lambda req: None env['swift.authorize_override'] = True env['REMOTE_USER'] = '******' qs = { 'temp_url_sig': temp_url_sig, 'temp_url_expires': temp_url_expires } if filename: qs['filename'] = filename env['QUERY_STRING'] = urlencode(qs) def _start_response(status, headers, exc_info=None): headers = self._clean_outgoing_headers(headers) if env['REQUEST_METHOD'] == 'GET' and status[0] == '2': # figure out the right value for content-disposition # 1) use the value from the query string # 2) use the value from the object metadata # 3) use the object name (default) out_headers = [] existing_disposition = None for h, v in headers: if h.lower() != 'content-disposition': out_headers.append((h, v)) else: existing_disposition = v if inline_disposition: disposition_value = 'inline' elif filename: disposition_value = disposition_format(filename) elif existing_disposition: disposition_value = existing_disposition else: name = basename(env['PATH_INFO'].rstrip('/')) disposition_value = disposition_format(name) out_headers.append(('Content-Disposition', disposition_value)) headers = out_headers return start_response(status, headers, exc_info) return self.app(env, _start_response)
def __call__(self, env, start_response): """ Main hook into the WSGI paste.deploy filter/app pipeline. :param env: The WSGI environment dict. :param start_response: The WSGI start_response hook. :returns: Response as per WSGI. """ if env['REQUEST_METHOD'] == 'OPTIONS': return self.app(env, start_response) temp_url_sig, temp_url_expires, filename = self._get_temp_url_info(env) if temp_url_sig is None and temp_url_expires is None: return self.app(env, start_response) if not temp_url_sig or not temp_url_expires: return self._invalid(env, start_response) account = self._get_account(env) if not account: return self._invalid(env, start_response) keys = self._get_keys(env, account) if not keys: return self._invalid(env, start_response) if env['REQUEST_METHOD'] == 'HEAD': hmac_vals = (self._get_hmacs(env, temp_url_expires, keys, request_method='GET') + self._get_hmacs(env, temp_url_expires, keys, request_method='PUT')) else: hmac_vals = self._get_hmacs(env, temp_url_expires, keys) # While it's true that any() will short-circuit, this doesn't affect # the timing-attack resistance since the only way this will # short-circuit is when a valid signature is passed in. is_valid_hmac = any(streq_const_time(temp_url_sig, h) for h in hmac_vals) if not is_valid_hmac: return self._invalid(env, start_response) self._clean_incoming_headers(env) env['swift.authorize'] = lambda req: None env['swift.authorize_override'] = True env['REMOTE_USER'] = '******' qs = {'temp_url_sig': temp_url_sig, 'temp_url_expires': temp_url_expires} if filename: qs['filename'] = filename env['QUERY_STRING'] = urlencode(qs) def _start_response(status, headers, exc_info=None): headers = self._clean_outgoing_headers(headers) if env['REQUEST_METHOD'] == 'GET' and status[0] == '2': already = False for h, v in headers: if h.lower() == 'content-disposition': already = True break if already and filename: headers = list((h, v) for h, v in headers if h.lower() != 'content-disposition') already = False if not already: name = filename or basename(env['PATH_INFO'].rstrip('/')) headers.append(( 'Content-Disposition', 'attachment; filename="%s"' % ( name.replace('"', '\\"')))) return start_response(status, headers, exc_info) return self.app(env, _start_response)
def _perform_subrequest(self, orig_env, attributes, fp, keys): """ Performs the subrequest and returns the response. :param orig_env: The WSGI environment dict; will only be used to form a new env for the subrequest. :param attributes: dict of the attributes of the form so far. :param fp: The file-like object containing the request body. :param keys: The account keys to validate the signature with. :returns: (status_line, headers_list, message) """ if not keys: raise FormUnauthorized('invalid signature') try: max_file_size = int(attributes.get('max_file_size') or 0) except ValueError: raise FormInvalid('max_file_size not an integer') subenv = make_pre_authed_env(orig_env, 'PUT', agent=None, swift_source='FP') if 'QUERY_STRING' in subenv: del subenv['QUERY_STRING'] subenv['HTTP_TRANSFER_ENCODING'] = 'chunked' subenv['wsgi.input'] = _CappedFileLikeObject(fp, max_file_size) if subenv['PATH_INFO'][-1] != '/' and \ subenv['PATH_INFO'].count('/') < 4: subenv['PATH_INFO'] += '/' subenv['PATH_INFO'] += attributes['filename'] or 'filename' if 'x_delete_at' in attributes: try: subenv['HTTP_X_DELETE_AT'] = int(attributes['x_delete_at']) except ValueError: raise FormInvalid('x_delete_at not an integer: ' 'Unix timestamp required.') if 'x_delete_after' in attributes: try: subenv['HTTP_X_DELETE_AFTER'] = int( attributes['x_delete_after']) except ValueError: raise FormInvalid('x_delete_after not an integer: ' 'Number of seconds required.') if 'content-type' in attributes: subenv['CONTENT_TYPE'] = \ attributes['content-type'] or 'application/octet-stream' elif 'CONTENT_TYPE' in subenv: del subenv['CONTENT_TYPE'] try: if int(attributes.get('expires') or 0) < time(): raise FormUnauthorized('form expired') except ValueError: raise FormInvalid('expired not an integer') hmac_body = '%s\n%s\n%s\n%s\n%s' % ( orig_env['PATH_INFO'], attributes.get('redirect') or '', attributes.get('max_file_size') or '0', attributes.get('max_file_count') or '0', attributes.get('expires') or '0') has_valid_sig = False for key in keys: sig = hmac.new(key, hmac_body, sha1).hexdigest() if streq_const_time(sig, (attributes.get('signature') or 'invalid')): has_valid_sig = True if not has_valid_sig: raise FormUnauthorized('invalid signature') substatus = [None] subheaders = [None] wsgi_input = subenv['wsgi.input'] def _start_response(status, headers, exc_info=None): if wsgi_input.file_size_exceeded: raise EOFError("max_file_size exceeded") substatus[0] = status subheaders[0] = headers i = iter(self.app(subenv, _start_response)) try: next(i) except StopIteration: pass return substatus[0], subheaders[0], ''