Exemple #1
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], ''
Exemple #2
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], ''
Exemple #3
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 _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 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')
Exemple #6
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'])

        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')
Exemple #7
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:
                     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], ""
Exemple #9
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]
Exemple #10
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
Exemple #11
0
    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)
Exemple #12
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'] += 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]
Exemple #13
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
Exemple #14
0
 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)
Exemple #16
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:
                     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
Exemple #17
0
    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
Exemple #18
0
    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)
Exemple #19
0
    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)
Exemple #20
0
    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)
Exemple #21
0
 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'))
Exemple #22
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, 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], ''