def test_2012_preflight_wrong_content_length_and_chunked(self): """Mix both Content-Length and chunked (inv), may be rejected. This is not plainly wrong, so it can still be accepted, but if it is not rejected chunked has priority on Content-Length. This one is like the previous one, but headers order is inverted. """ self.real_test = "{0}".format(inspect.stack()[0][3]) self.setGravity(self.GRAVITY_MINOR) method = self._get_valid_chunk_method() self.req.set_method(method) # like in other tests, req2 is not really used by our client # but just used to build req1's body self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) # this will cover the 0\r\n\r\n end-of-chunks regular markup # from query1 that will be present after query2 when embedded self.req2.add_header('Content-Length', '5') req2 = str(self.req2) # Adding a wrong content length which only covers the chunk size # and first crlf of first chunk. size_of_first_chunk = len(req2) first_chunk_attr = hex(size_of_first_chunk)[2:] size_of_first_chunk_size = len(str(first_chunk_attr)) wrong_length = size_of_first_chunk_size + 2 self.req.add_header('Content-Length', str(wrong_length)) self.req.add_chunk(req2) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self._add_default_status_map(valid=False) # local additions self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { 'Content-Length: 0': self.STATUS_TRANSMITTED_CRAP, } # we send only 1 query (with a body). # but if C-L has the priority this will be 2 queries # Note that this query MAY be completly valid, but it is safer # if it is rejected (so it's really a minor issue self._end_expected_error(expected_number=1) Register.flag('cl_and_chunk_{0}'.format(self.reverse_proxy_mode), (self.status not in [ self.STATUS_ACCEPTED, self.STATUS_TRANSMITTED, self.STATUS_SPLITTED, self.STATUS_WOOKIEE ]))
def test_2011_chunked_and_wrong_content_length(self): """Use chunk+Content length with a wrong Content Length. This is not plainly wrong, so it can still be accepted, but if it is not rejected chunked has priority on Content-Length. If Content-Length has the priority this can be a piece of a splitting attack. """ self.real_test = "{0}".format(inspect.stack()[0][3]) self.setGravity(self.GRAVITY_MINOR) if not Register.hasFlag('chunk_and_cl_{0}'.format( self.reverse_proxy_mode)): self.skipTest("Preflight invalidated all chunk+CL queries.") method = self._get_valid_chunk_method() self.req.method = method self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') # like in other tests, req2 is not really used by our client # but just used to build req1's body self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) # this will cover the 0\r\n\r\n end-of-chunks regular markup # from query1 that will be present after query2 when embedded self.req2.add_header('Content-Length', '5') req2 = str(self.req2) self.req.add_chunk(req2) # Adding a wrong content length which only covers the chunk size # and first crlf of first chunk. size_of_first_chunk = len(req2) first_chunk_attr = hex(size_of_first_chunk)[2:] size_of_first_chunk_size = len(str(first_chunk_attr)) wrong_length = size_of_first_chunk_size + 2 self.req.add_header('Content-Length', str(wrong_length)) self._add_default_status_map(valid=False) # local additions self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { 'Content-Length: 5': self.STATUS_TRANSMITTED_CRAP, } # we send only 1 query (with a body). # but if C-L has the priority this will be 2 queries self._end_expected_error(expected_number=1)
def test_2041_chunked_size_overflow_with_trailers(self): """Try an int overflow on the chunk size, add trailer headers. """ self.real_test = "{0}_{1}".format(inspect.stack()[0][3], self.nb) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req.add_header('Trailer', 'Zorglub') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) # this will cover the 0\r\n\r\n end-of-chunks regular markup # from query 1 that will be present after query2 when embedded self.req2.add_header('Content-Length', '5') req2 = str(self.req2) # We add 4 chars to cover the chunk size of second header +CRLF chunk_size = hex(self.nb)[2:] trailers = u'Zorglub: Bulgroz\r\nContent-Length:42\r\n' bad_chunk = '{0}{1}{2}'.format(trailers, Tools.CRLF, req2) self.req.add_chunk(bad_chunk, chunk_size=chunk_size) self._add_default_status_map(valid=False) # local additions # here accepting a chunked query with crap in chunk size attribute # is always quite strange self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_WARNING self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { '\r\n{0}\r\n'.format(chunk_size): self.STATUS_TRANSMITTED, 'Content-Length: {0}\r\n'.format(self.nb): self.STATUS_TRANSMITTED_CRAP, 'Content-Length:{0}\r\n'.format(self.nb): self.STATUS_TRANSMITTED_CRAP, # Content-Length header in trailers is not forbidden # but at least it should have no consequences, so we should # not find it in the headers. 'Content-Length:42\r\n': self.STATUS_TRANSMITTED_CRAP, 'Content-Length: 42\r\n': self.STATUS_TRANSMITTED_CRAP, } self._end_expected_error(expected_number=1)
def test_2024_chunked_header_hidden_by_bad_eol_3(self): """Use chunk+Content length, but chunk header should be invalid. The query should be rejected, or only apply Content-Lenght. If Content-Length is applied. """ self.real_test = "{0}".format(inspect.stack()[0][3]) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Dummy', 'Header{0}{1}Transfer-Encoding:chunked'.format( Tools.CR, 'Z'), sep=':') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) req2 = Tools.CRLF + str(self.req2) self.req.add_header('Content-Length', str(len(req2))) # by forcing chunk_size to 0 we will detect splitters # as they will apply the chunked mode and req2 wont be a body anymore self.req.add_chunk(req2, chunk_size=0) self.req.end_of_chunks = False self._add_default_status_map(valid=False) # local additions self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { 'Content-Length: 0': self.STATUS_TRANSMITTED_CRAP, 'Dummy: Header\rZ' 'Transfer-Encoding:chunked': self.STATUS_TRANSMITTED, 'Dummy: Header\rZ' 'Transfer-Encoding: chunked': self.STATUS_TRANSMITTED, '\nZTransfer-Encoding:chunked': self.STATUS_TRANSMITTED_CRAP, '\nZ Transfer-Encoding:chunked': self.STATUS_TRANSMITTED_CRAP, } self._end_expected_error(expected_number=1)
def test_2010_preflight_chunked_and_content_length(self): """Mix both Content-Length and chunked, could be rejected (or not). This is not plainly wrong, so it can still be accepted, but if it is not rejected chunked has priority on Content-Length. """ self.real_test = "{0}".format(inspect.stack()[0][3]) self.setGravity(self.GRAVITY_MINOR) method = self._get_valid_chunk_method() self.req.set_method(method) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') # like in other tests, req2 is not really used by our client # but just used to build req1's body self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) req2 = str(self.req2) # Adding a valid Content-Length header (right size) # it's size of req2 + CRLF to end chunk + size of end-of-chunks (5) # + size of first chunk size and crlf (4) self.req.add_header('Content-Length', str(len(req2) + 9)) self.req.add_chunk(req2) self._add_default_status_map(valid=False) # local additions self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { 'Content-Length: 0': self.STATUS_TRANSMITTED_CRAP, } # we send only 1 query (with a body). # but if C-L has the priority this will be 2 queries # Note that this query MAY be completly valid, but it is safer # if it is rejected (so it's really a minor issue self._end_expected_error(expected_number=1) Register.flag('chunk_and_cl_{0}'.format(self.reverse_proxy_mode), (self.status not in [ self.STATUS_ACCEPTED, self.STATUS_TRANSMITTED, self.STATUS_SPLITTED, self.STATUS_WOOKIEE ]))
def _prepare_pipe_test(self, method1='GET', method2='GET'): outmsg("={0}=".format(self.real_test)) if Register.flags['keepalive'] is False: self.skipTest("No keepalive support.") if Register.flags['pipelining'] is False: self.skipTest("No pipelining support.") self.send_mode = self.SEND_MODE_PIPE location = self.get_default_location( with_prefix=self.use_backend_location) self.req1 = Request(id(self)) self.req1.add_header('Connection', 'keep-alive') self.req1.set_location(location, random=True) self.req1.set_method(method1) self.req2 = Request(id(self)) self.req2.set_location(location, random=True) self.req2.set_method(method2)
def test_0005_bad_pipeline(self): "Chain 3 queries, second is bad, should stop the response stream." self.real_test = "{0}".format(inspect.stack()[0][3]) self._prepare_pipe_test() self.req2.set_location(Tools.NULL, random=True) self.req3 = Request(id(self)) self.req3.set_location(self.config.get('SERVER_DEFAULT_LOCATION'), random=True) with Client() as csock: csock.send(self.req1) csock.send(self.req2) csock.send(self.req3) responses = csock.read_all() outmsg(str(responses)) self.analysis(responses, expected_number=2, regular_expected=False) self.assertTrue((responses.count <= 2)) if (responses.count > 1): self.assertIn(self.status, [self.STATUS_REJECTED, self.STATUS_ERR400], 'Bad response status {0}'.format(self.status))
def test_2030_chunked_size_truncation(self): """Try an int truncation on the chunk size. """ self.real_test = "{0}".format(inspect.stack()[0][3]) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) # this will cover the 0\r\n\r\n end-of-chunks regular markup # from query1 that will be present after query2 when embedded self.req2.add_header('Content-Length', '5') req2 = str(self.req2) something = '{0}{1}'.format(Tools.CRLF, req2) # Apache issue, truncation in hexadecimal chunk size, final size is 0 # for Apache, so the rest of the chunk is interpreted as a trailer (if # any). This means an end of query. # we add 4 chars to cover the chunk size of second header +CRLF # chunk_size = 0000000...00000004e # which will be read as chunk_size = 00000000000000000000000000000 chunk_size = "0" * 33 + hex(len(something))[2:] self.req.add_chunk(something, chunk_size=chunk_size) self._add_default_status_map(valid=True, always_allow_rejected=True) # local additions # here transmission is valid, but some actr may split on this syntax # so it would be better to fix the 00000... prefix syntax self.status_map[self.STATUS_TRANSMITTED_CRAP] = self.GRAVITY_WARNING # for RP mode: self.transmission_zone = Tools.ZONE_CHUNK_SIZE self.transmission_map = { '000000000000000000000000': self.STATUS_TRANSMITTED_CRAP, } # this is a valid query in fact. # so we should not expect an error (but 2 responses would be very bad) self._end_regular_query(expected_number=1, can_be_rejected=False)
def test_2002_preflight_regular_chunked_double_query(self): """We make two chunked queries with a third query inside, no tricks The goal is to ensure we really have 2 responses. Makes other failing stuff more important (else it means the chunk algo is completly broken) """ self.real_test = "{0}".format(inspect.stack()[0][3]) self.setGravity(self.GRAVITY_MINOR) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') method = self._get_valid_chunk_method() self.req.set_method(method) # req2 is not a real query, just the chunked body of req1 self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) req2 = str(self.req2) self.req.add_chunk(req2) self.req3 = Request(id(self)) self.req3.set_location(self.req.location, random=True) self.req3.set_method(method) req3 = str(self.req3) # Yes, not the nicest way to make a pipeline, but it should work. # we'll have a pipeline and our client will not temporise the # output. self.req.add_suffix(req3) self._end_almost_regular_query(expected_number=2) if not self.status == self.STATUS_ACCEPTED: Register.flag('chunk_brain', False) if self.STATUS_SPLITTED == self.status: Register.flag('chunk_brain', False) self.setGravity(self.GRAVITY_CRITICAL) self.assertIn(self.status, [self.STATUS_ACCEPTED], 'Bad response status {0}'.format(self.status))
def test_2042_chunked_size_overflow_with_delayed_chunks(self): """Try an int overflow on the chunk size, add trailer headers. """ self.real_test = "{0}_{1}".format(inspect.stack()[0][3], self.nb) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req.add_header('Trailer', 'Zorglub') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) self.req2.add_header('Content-Length', '5') req2 = str(self.req2) chunk_size = hex(self.nb)[2:] bad_chunk = '' self.req.add_chunk(bad_chunk, chunk_size=chunk_size, chunk_eol=u'') self.req.add_delayed_chunk(req2, delay=0.5) self.req.add_delayed_chunk(req2, delay=2) self._add_default_status_map(valid=False) # local additions # here accepting a chunked query with crap in chunk size attribute # is always quite strange self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_WARNING self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { '\r\n{0}\r\n'.format(chunk_size): self.STATUS_TRANSMITTED, 'Content-Length: {0}\r\n'.format(self.nb): self.STATUS_TRANSMITTED_CRAP, 'Content-Length:{0}\r\n'.format(self.nb): self.STATUS_TRANSMITTED_CRAP, } self._end_expected_error(expected_number=1)
def test_2040_chunked_size_overflow(self): """Try an int overflow on the chunk size """ self.real_test = "{0}_{1}".format(inspect.stack()[0][3], self.nb) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) # this will cover the 0\r\n\r\n end-of-chunks regular markup # from query 1 that will be present after query2 when embedded self.req2.add_header('Content-Length', '5') req2 = str(self.req2) # we add 4 chars to cover the chunk size of second header +CRLF chunk_size = hex(self.nb)[2:] self.req.add_chunk(Tools.CRLF + req2, chunk_size=chunk_size) self._add_default_status_map(valid=False) # local additions # here accepting a chunked query with crap in chunk size attribute # is always quite strange self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_WARNING self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { '\r\n{0}\r\n'.format(chunk_size): self.STATUS_TRANSMITTED, 'Content-Length: {0}\r\n'.format(self.nb): self.STATUS_TRANSMITTED_CRAP, 'Content-Length:{0}\r\n'.format(self.nb): self.STATUS_TRANSMITTED_CRAP, } self._end_expected_error(expected_number=1)
class AbstractTestRegularPipe(BaseTest): def __init__(self, methodName="runTest"): super(AbstractTestRegularPipe, self).__init__(methodName) self.send_mode = self.SEND_MODE_PIPE def test_0004_regular_pipeline(self): "Chain two regular queries in a pipeline (no waits between requests)." self.real_test = "{0}".format(inspect.stack()[0][3]) self._prepare_pipe_test() responses = self.send_queries() if responses.count == 1: Register.flag('pipelining', False) self._end_regular_query(responses, expected_number=2) def test_0005_bad_pipeline(self): "Chain 3 queries, second is bad, should stop the response stream." self.real_test = "{0}".format(inspect.stack()[0][3]) self._prepare_pipe_test() self.req2.set_location(Tools.NULL, random=True) self.req3 = Request(id(self)) self.req3.set_location(self.config.get('SERVER_DEFAULT_LOCATION'), random=True) with Client() as csock: csock.send(self.req1) csock.send(self.req2) csock.send(self.req3) responses = csock.read_all() outmsg(str(responses)) self.analysis(responses, expected_number=2, regular_expected=False) self.assertTrue((responses.count <= 2)) if (responses.count > 1): self.assertIn(self.status, [self.STATUS_REJECTED, self.STATUS_ERR400], 'Bad response status {0}'.format(self.status))
def test_2021_chunked_header_hidden_by_NULL(self): """Transfer-Encoding header splitted by a NULL char. """ self.real_test = "{0}".format(inspect.stack()[0][3]) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Transfer-{0}: 42{1}Encoding'.format( Tools.NULL, Tools.CRLF), 'chunked', sep=':') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) req2 = Tools.CRLF + str(self.req2) self.req.add_header('Content-Length', str(len(req2))) # by forcing chunk_size to 0 we will detect splitters # as they will apply the chunked mode and req2 wont be a body anymore self.req.add_chunk(req2, chunk_size=0) self.req.end_of_chunks = False self._add_default_status_map(valid=False) # local additions self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { 'Content-Length: 0': self.STATUS_TRANSMITTED_CRAP, 'Transfer-{0}: 42'.format(Tools.NULL): self.STATUS_TRANSMITTED, } self._end_expected_error(expected_number=1)
def _prepare_simple_test(self): outmsg("={0}=".format(self.real_test)) self.req = Request(id(self)) location = self.get_default_location() self.req.set_location(location, random=True)
class BaseTest(unittest.TestCase): GRAVITY_UNKNOWN = 0 GRAVITY_MINOR = 1 GRAVITY_WARNING = 2 GRAVITY_CRITICAL = 3 GRAVITY_OK = 4 gravity_format = { GRAVITY_UNKNOWN: u'Unknown', GRAVITY_MINOR: u'Minor', GRAVITY_WARNING: u'Warning', GRAVITY_CRITICAL: u'Critical', GRAVITY_OK: u'Ok', } STATUS_UNKNOWN = 'Unknown' STATUS_ACCEPTED = 'Accepted' STATUS_REJECTED = 'Rejected' STATUS_ERR400 = 'Err400' STATUS_ERR401 = 'Err401' STATUS_ERR403 = 'Err403' STATUS_ERR404 = 'Err404' STATUS_ERR405 = 'Err405' STATUS_ERR411 = 'Err411' STATUS_ERR413 = 'Err413' STATUS_ERR414 = 'Err414' STATUS_ERR500 = 'Err500' STATUS_501_NOT_IMPLEMENTED = '501NotImpl.' STATUS_5032 = '502or5023' STATUS_505_NOT_SUPPORTED = '505' STATUS_SPLITTED = 'Splitted' STATUS_09DOWNGRADE = 'Downgrade09' STATUS_09OK = 'Regular09' STATUS_RED_301 = 'Redir301' STATUS_RED_302 = 'Redir302' STATUS_TRANSMITTED = 'Transmit' STATUS_TRANSMITTED_EXACT = 'Transmit+' STATUS_TRANSMITTED_CRAP = 'Transmit^!$#^' STATUS_REMOVED = 'Removed' STATUS_WOOKIEE = 'Wookiee' status_format = { STATUS_REJECTED: {'long': u'rejected', 'short': u'r'}, STATUS_ERR400: {'long': u'-err400-', 'short': u'4'}, STATUS_ERR401: {'long': u'-err401-', 'short': u'4'}, STATUS_ERR403: {'long': u'-err403-', 'short': u'4'}, STATUS_ERR404: {'long': u'-err404-', 'short': u'4'}, STATUS_ERR405: {'long': u'-err405-', 'short': u'4'}, STATUS_ERR411: {'long': u'-err411-', 'short': u'4'}, STATUS_ERR413: {'long': u'-err413-', 'short': u'4'}, STATUS_ERR414: {'long': u'-err414-', 'short': u'4'}, STATUS_ERR500: {'long': u'-err500-', 'short': u'5'}, STATUS_RED_301: {'long': u'-red301-', 'short': u'3'}, STATUS_RED_302: {'long': u'-red302-', 'short': u'3'}, STATUS_501_NOT_IMPLEMENTED: {'long': u'-501-ni', 'short': u'n'}, STATUS_5032: {'long': u'-502-03', 'short': u'x'}, STATUS_505_NOT_SUPPORTED: {'long': u'-505-ns', 'short': u'5'}, STATUS_ACCEPTED: {'long': u'accepted', 'short': u'a'}, STATUS_UNKNOWN: {'long': u'-unknown', 'short': u'u'}, STATUS_SPLITTED: {'long': u'splitted', 'short': u's'}, STATUS_09DOWNGRADE: {'long': u'-down-09', 'short': u'D'}, STATUS_09OK: {'long': u'regular9', 'short': u'9'}, STATUS_TRANSMITTED: {'long': u'transmit', 'short': u't'}, STATUS_TRANSMITTED_EXACT: {'long': u'transmit', 'short': u'T'}, STATUS_TRANSMITTED_CRAP: {'long': u'transmit', 'short': u'Z'}, STATUS_REMOVED: {'long': u'removed-', 'short': u'R'}, STATUS_WOOKIEE: {'long': u'wookiee-', 'short': u'W'}, } SEND_MODE_UNIQUE = 0 SEND_MODE_PIPE = 1 use_backend_location = False req = None req1 = None req2 = None send_mode = None reverse_proxy_mode = False def __init__(self, methodName="runTest"): self.config = ConfigFactory.getConfig() self.status = self.STATUS_UNKNOWN self.real_test = None self.gravity = self.GRAVITY_UNKNOWN self.status_map = None self.transmission_zone = None self.transmission_map = None if self.send_mode is None: self.send_mode = self.SEND_MODE_UNIQUE self.use_backend_location = False self.reverse_proxy_mode = False super(BaseTest, self).__init__(methodName) self.addCleanup(self.CustomCleanup) def __str__(self): return "[{0}] ({1})".format(self._testMethodName, strclass(self.__class__)) def setUp(self): super(BaseTest, self).setUp() if (not self.reverse_proxy_mode and self.config.getboolean('REVERSEPROXY_TESTS_ONLY')): self.skipTest('Only Reverse Proxy (server) tests are ' 'allowed by config') self._prepare_queries() def setStatus(self, status): if status not in self.status_format.keys(): raise ValueError('Unexpected Test status {0}'.format(status)) self.status = status def getStatus(self, format=None): if format is None: return self.status else: try: return self.status_format[self.status][format] except KeyError: return '' def getGravity(self, human=False): if not human: return self.gravity else: return self.gravity_format[self.gravity] def setGravity(self, gravity): if gravity not in self.gravity_format.keys(): raise ValueError('Unexpected Test gravity {0}'.format(gravity)) self.gravity = gravity def CustomCleanup(self): pass # print('CUSTOmCleanup') def _prepare_queries(self): if self.send_mode == self.SEND_MODE_UNIQUE: self._prepare_simple_test() elif self.send_mode == self.SEND_MODE_PIPE: self._prepare_pipe_test() else: raise ValueError('Unknown send mode for test HTTP queries.') def get_default_location(self, with_prefix=None): if with_prefix is None: with_prefix = self.use_backend_location if with_prefix: return "{0}{1}".format( self.config.get('BACKEND_LOCATION_PREFIX'), self.config.get('SERVER_DEFAULT_LOCATION')) else: return self.config.get('SERVER_DEFAULT_LOCATION') def get_non_default_location(self, with_prefix=None): if with_prefix is None: with_prefix = self.use_backend_location if with_prefix: return "{0}{1}".format( self.config.get('BACKEND_LOCATION_PREFIX'), self.config.get('SERVER_NON_DEFAULT_LOCATION')) else: return self.config.get('SERVER_NON_DEFAULT_LOCATION') def get_wookiee_location(self, with_prefix=None): if with_prefix is None: with_prefix = self.use_backend_location if with_prefix: return "{0}{1}".format( self.config.get('BACKEND_LOCATION_PREFIX'), self.config.get('BACKEND_WOOKIEE_LOCATION')) else: return self.config.get('BACKEND_WOOKIEE_LOCATION') def _get_valid_chunk_method(self): if not Register.hasFlag('post_chunk_{0}'.format( self.reverse_proxy_mode)): if not Register.hasFlag('get_chunk_{0}'.format( self.reverse_proxy_mode)): self.skipTest("Preflight invalidated all chunk queries.") return 'GET' else: return 'POST' def _prepare_simple_test(self): outmsg("={0}=".format(self.real_test)) self.req = Request(id(self)) location = self.get_default_location() self.req.set_location(location, random=True) def _prepare_pipe_test(self, method1='GET', method2='GET'): outmsg("={0}=".format(self.real_test)) if Register.flags['keepalive'] is False: self.skipTest("No keepalive support.") if Register.flags['pipelining'] is False: self.skipTest("No pipelining support.") self.send_mode = self.SEND_MODE_PIPE location = self.get_default_location( with_prefix=self.use_backend_location) self.req1 = Request(id(self)) self.req1.add_header('Connection', 'keep-alive') self.req1.set_location(location, random=True) self.req1.set_method(method1) self.req2 = Request(id(self)) self.req2.set_location(location, random=True) self.req2.set_method(method2) def _hook_while_sending(self): pass def send_queries(self): responses = None if self.send_mode == self.SEND_MODE_UNIQUE: with Client() as csock: csock.send(self.req) responses = csock.read_all() self._hook_while_sending() elif self.send_mode == self.SEND_MODE_PIPE: with Client() as csock: # csock.send(u'{0}{1}'.format(self.req1, self.req2)) csock.send(self.req1) csock.send(self.req2) responses = csock.read_all() self._hook_while_sending() else: raise ValueError('Unknown send mode for test HTTP queries.') outmsg(str(responses)) return responses def _end_regular_query(self, responses=None, http09_allowed=False, can_be_rejected=False, expected_number=1, status_map=None): self._end_almost_regular_query(responses, http09_allowed=http09_allowed, expected_number=expected_number, regular_expected=True, status_map=status_map) allowed = [self.STATUS_ACCEPTED] if can_be_rejected: # sometimes it's 'regular', but not really allowed.append(self.STATUS_REJECTED) allowed.append(self.STATUS_ERR400) allowed.append(self.STATUS_ERR413) allowed.append(self.STATUS_ERR411) allowed.append(self.STATUS_501_NOT_IMPLEMENTED) allowed.append(self.STATUS_505_NOT_SUPPORTED) # rfc 7230 allows 301 on bad request line allowed.append(self.STATUS_RED_301) if http09_allowed: allowed.append(self.STATUS_09OK) self.assertIn(self.status, allowed, 'Bad response status {0} for regular query'.format( self.status)) def _end_almost_regular_query(self, responses=None, http09_allowed=False, regular_expected=False, expected_number=1, status_map=None): "same as _end_regular_query but without the status assertions." if responses is None: responses = self.send_queries() self.analysis(responses, expected_number=expected_number, http09_allowed=http09_allowed, regular_expected=regular_expected) self.adjust_status_by_map(status_map) def _end_expected_error(self, responses=None, expected_number=1, regular_expected=False, http09_allowed=False, status_map=None): "Bad queries test end management." if responses is None: responses = self.send_queries() self.analysis(responses, expected_number=expected_number, http09_allowed=http09_allowed, regular_expected=regular_expected) self.adjust_status_by_map(status_map) self.assertNotEqual(self.status, self.STATUS_WOOKIEE, 'Wookiee response detected,' + ' this should never happen.') self.assertIn(self.status, [self.STATUS_REJECTED, self.STATUS_ERR400, self.STATUS_ERR411, self.STATUS_ERR413, self.STATUS_ERR414, # yes, too bad this is used by Tomcat for most 400 self.STATUS_ERR500, self.STATUS_501_NOT_IMPLEMENTED, self.STATUS_505_NOT_SUPPORTED], 'Bad response status "{0}"'.format(self.status)) def _end_1st_line_query(self, responses=None, http09_allowed=False, expected_number=1, status_map=None): if responses is None: responses = self.send_queries() self.analysis(responses, http09_allowed=http09_allowed, regular_expected=False) self.adjust_status_by_map(status_map) allowed = [] for status_elt, status_gravity in Tools.iteritems(self.status_map): if status_gravity is self.GRAVITY_OK: allowed.append(status_elt) self.assertIn(self.status, allowed) def _add_default_status_map(self, valid=False, http09_allowed=False, always_allow_rejected=False): self.status_map = { # this is only dangerous if any RP is a transmitter of # such bad queries. Should be very rare. self.STATUS_09DOWNGRADE: self.GRAVITY_WARNING, self.STATUS_09OK: self.GRAVITY_WARNING, # but, hey, if you are such proxy that's a very bad cleanup # FIXME: ensure a RP having final 0.9 response, without # tansmission in 0.9, is marked as critical self.STATUS_TRANSMITTED_EXACT: self.GRAVITY_CRITICAL, self.STATUS_TRANSMITTED_CRAP: self.GRAVITY_CRITICAL, self.STATUS_TRANSMITTED: self.GRAVITY_WARNING, self.STATUS_WOOKIEE: self.GRAVITY_CRITICAL, self.STATUS_SPLITTED: self.GRAVITY_CRITICAL, } if valid: self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_OK else: self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR if always_allow_rejected or not valid: self.status_map[self.STATUS_REJECTED] = self.GRAVITY_OK self.status_map[self.STATUS_ERR400] = self.GRAVITY_OK self.status_map[self.STATUS_ERR413] = self.GRAVITY_OK self.status_map[self.STATUS_ERR414] = self.GRAVITY_OK self.status_map[self.STATUS_ERR411] = self.GRAVITY_OK self.status_map[self.STATUS_501_NOT_IMPLEMENTED] = self.GRAVITY_OK self.status_map[self.STATUS_505_NOT_SUPPORTED] = self.GRAVITY_OK # rfc 7230 allows 301 on bad request line self.status_map[self.STATUS_RED_301] = self.GRAVITY_OK if http09_allowed: self.status_map[self.STATUS_09OK] = self.GRAVITY_OK self.status_map[self.STATUS_09DOWNGRADE] = self.GRAVITY_OK def adjust_status_by_map(self, status_map=None): "Adjust gravity of the test based on status." if status_map is not None: if self.status in status_map: self.setGravity(status_map[self.status]) if self.status_map is not None: if self.status in self.status_map: if self.status_map[self.status] is not self.GRAVITY_OK: self.setGravity(self.status_map[self.status]) def analysis(self, responses, expected_number=1, http09_allowed=False, regular_expected=False): "Launch deep analysis of the responses status." self.count_responses = responses.count if not self.count_responses: self.setStatus(self.STATUS_REJECTED) elif self.count_responses > expected_number: # Splitting responses is ALWAYS Critical self.gravity = self.GRAVITY_CRITICAL self.setStatus(self.STATUS_SPLITTED) else: for response in responses: # Continue checking while test status is undecided or while # all responses are accepted if (self.STATUS_UNKNOWN == self.status or self.STATUS_ACCEPTED == self.status): self.check_for_errors(response) if (self.STATUS_UNKNOWN == self.status or self.STATUS_ACCEPTED == self.status): self.check_for_redirect(response) if (self.STATUS_UNKNOWN == self.status or self.STATUS_ACCEPTED == self.status): self.check_for_regular_content( response, http09_allowed=http09_allowed, required=regular_expected) # check_for_regular_content may have set self.STATUS_09OK if (999 == response.code and not http09_allowed and self.status != self.STATUS_09OK): self.setStatus(self.STATUS_09DOWNGRADE) # raise AssertionError('Unauthorized HTTP/0.9 response') # @deprecated def assertBodyContainsDefaultContent(self, body): """Assert that the given bytes contains the default location content""" # we obtain an unicode string, body contains only bytes, so we need # to obtain a b'' expected = self.config.get( 'SERVER_DEFAULT_LOCATION_CONTENT').encode('utf8') if expected not in body: raise AssertionError('{0} is not present in body'.format(expected)) def check_for_errors(self, response): if ((b" -mMMNNdhdmhyhNs/mmy+mmyyy+shdo/-...." in response.body) and (b"``.-:/mdMNmh++dNddddh+hmod+/ohdy" in response.body) and (b"Wookiee !" in response.body)): self.setStatus(self.STATUS_WOOKIEE) elif Response.ERROR_HTTP09_RESPONSE in response.errors: if (b"400" in response.body and b"Bad Request" in response.body): self.setStatus(self.STATUS_ERR400) if (b"401" in response.body and b"Unauthorized" in response.body): self.setStatus(self.STATUS_ERR401) if (b"403" in response.body and b"Forbidden" in response.body): self.setStatus(self.STATUS_ERR403) if (b"404" in response.body and b"Not Found" in response.body): self.setStatus(self.STATUS_ERR404) if (b"405" in response.body and b"Method Not Allowed" in response.body): self.setStatus(self.STATUS_ERR405) if (b"411" in response.body and b"Length Required" in response.body): self.setStatus(self.STATUS_ERR411) if (b"413" in response.body and b"Request Entity Too Large" in response.body): self.setStatus(self.STATUS_ERR413) if (b"414" in response.body and b"Request URI too long" in response.body): self.setStatus(self.STATUS_ERR414) if (b"501" in response.body and b"Not Implemented" in response.body): self.setStatus(self.STATUS_501_NOT_IMPLEMENTED) if (b"505" in response.body and b"Not Supported" in response.body): self.setStatus(self.STATUS_505_NOT_SUPPORTED) if ((b"502" in response.body or b"503" in response.body) and (b"Bad gateway" in response.body or b"Service Unavailable" in response.body)): self.setStatus(self.STATUS_5032) else: if 400 == response.code: self.setStatus(self.STATUS_ERR400) if 401 == response.code: self.setStatus(self.STATUS_ERR401) if 403 == response.code: self.setStatus(self.STATUS_ERR403) if 404 == response.code: self.setStatus(self.STATUS_ERR404) if 411 == response.code: self.setStatus(self.STATUS_ERR411) if 413 == response.code: self.setStatus(self.STATUS_ERR413) if 414 == response.code: self.setStatus(self.STATUS_ERR414) if 405 == response.code: self.setStatus(self.STATUS_ERR405) if 501 == response.code: self.setStatus(self.STATUS_501_NOT_IMPLEMENTED) if 505 == response.code: self.setStatus(self.STATUS_505_NOT_SUPPORTED) if 502 == response.code: self.setStatus(self.STATUS_5032) if 503 == response.code: self.setStatus(self.STATUS_5032) def check_for_redirect(self, response): if Response.ERROR_HTTP09_RESPONSE in response.errors: if (b"301" in response.body and b"Moved Permanently" in response.body): self.setStatus(self.STATUS_RED_301) elif (b"302" in response.body and b"Found" in response.body): self.setStatus(self.STATUS_RED_302) else: if 301 == response.code: self.setStatus(self.STATUS_RED_301) elif 302 == response.code: self.setStatus(self.STATUS_RED_302) def _get_expected_content(self): expected = self.config.get( 'SERVER_DEFAULT_LOCATION_CONTENT').encode('utf8') return expected def check_for_regular_content(self, response, http09_allowed=False, required=True): if 200 == response.code: self.setStatus(self.STATUS_ACCEPTED) expected = self._get_expected_content() if Response.ERROR_HTTP09_RESPONSE in response.errors: if expected in response.body: if http09_allowed: self.setStatus(self.STATUS_09OK) else: # regular content present in an http/0.9 response # this is dangerous unless you really requested in 0.9 mode self.setStatus(self.STATUS_09DOWNGRADE) self.gravity = self.GRAVITY_CRITICAL else: if required and expected not in response.body: raise AssertionError('{0} is not present in body'.format( expected))
class AbstractTestChunks(BaseTest): """Test various hack on Tranfer-Encoding: chunked. """ def __init__(self, *args, **kwargs): super(AbstractTestChunks, self).__init__(*args, **kwargs) # for RP mode message analysis self.transmission_zone = Tools.ZONE_HEADERS self.send_mode = self.SEND_MODE_UNIQUE def test_2000_preflight_regular_chunked_get(self): """Let's start by a regular GET with chunked body """ self.real_test = "{0}".format(inspect.stack()[0][3]) self.setGravity(self.GRAVITY_MINOR) self.req.set_method('GET') self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req.add_chunk('Hello') self.req.add_chunk('World') self._end_almost_regular_query() Register.flag('get_chunk_{0}'.format(self.reverse_proxy_mode), (self.status == self.STATUS_ACCEPTED)) self.assertIn(self.status, [self.STATUS_ACCEPTED, self.STATUS_ERR405], 'Bad response status {0}'.format(self.status)) def test_2001_preflight_regular_chunked_post(self): """If get is not good, try POST for chunked queries. """ self.real_test = "{0}".format(inspect.stack()[0][3]) self.setGravity(self.GRAVITY_MINOR) self.req.set_method('POST') self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req.add_chunk('Hello') self.req.add_chunk('World') self._end_almost_regular_query() Register.flag('post_chunk_{0}'.format(self.reverse_proxy_mode), (self.status == self.STATUS_ACCEPTED)) self.assertIn(self.status, [self.STATUS_ACCEPTED, self.STATUS_ERR405], 'Bad response status {0}'.format(self.status)) def test_2002_preflight_regular_chunked_double_query(self): """We make two chunked queries with a third query inside, no tricks The goal is to ensure we really have 2 responses. Makes other failing stuff more important (else it means the chunk algo is completly broken) """ self.real_test = "{0}".format(inspect.stack()[0][3]) self.setGravity(self.GRAVITY_MINOR) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') method = self._get_valid_chunk_method() self.req.set_method(method) # req2 is not a real query, just the chunked body of req1 self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) req2 = str(self.req2) self.req.add_chunk(req2) self.req3 = Request(id(self)) self.req3.set_location(self.req.location, random=True) self.req3.set_method(method) req3 = str(self.req3) # Yes, not the nicest way to make a pipeline, but it should work. # we'll have a pipeline and our client will not temporise the # output. self.req.add_suffix(req3) self._end_almost_regular_query(expected_number=2) if not self.status == self.STATUS_ACCEPTED: Register.flag('chunk_brain', False) if self.STATUS_SPLITTED == self.status: Register.flag('chunk_brain', False) self.setGravity(self.GRAVITY_CRITICAL) self.assertIn(self.status, [self.STATUS_ACCEPTED], 'Bad response status {0}'.format(self.status)) def test_2010_preflight_chunked_and_content_length(self): """Mix both Content-Length and chunked, could be rejected (or not). This is not plainly wrong, so it can still be accepted, but if it is not rejected chunked has priority on Content-Length. """ self.real_test = "{0}".format(inspect.stack()[0][3]) self.setGravity(self.GRAVITY_MINOR) method = self._get_valid_chunk_method() self.req.set_method(method) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') # like in other tests, req2 is not really used by our client # but just used to build req1's body self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) req2 = str(self.req2) # Adding a valid Content-Length header (right size) # it's size of req2 + CRLF to end chunk + size of end-of-chunks (5) # + size of first chunk size and crlf (4) self.req.add_header('Content-Length', str(len(req2) + 9)) self.req.add_chunk(req2) self._add_default_status_map(valid=False) # local additions self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { 'Content-Length: 0': self.STATUS_TRANSMITTED_CRAP, } # we send only 1 query (with a body). # but if C-L has the priority this will be 2 queries # Note that this query MAY be completly valid, but it is safer # if it is rejected (so it's really a minor issue self._end_expected_error(expected_number=1) Register.flag('chunk_and_cl_{0}'.format(self.reverse_proxy_mode), (self.status not in [ self.STATUS_ACCEPTED, self.STATUS_TRANSMITTED, self.STATUS_SPLITTED, self.STATUS_WOOKIEE ])) def test_2011_chunked_and_wrong_content_length(self): """Use chunk+Content length with a wrong Content Length. This is not plainly wrong, so it can still be accepted, but if it is not rejected chunked has priority on Content-Length. If Content-Length has the priority this can be a piece of a splitting attack. """ self.real_test = "{0}".format(inspect.stack()[0][3]) self.setGravity(self.GRAVITY_MINOR) if not Register.hasFlag('chunk_and_cl_{0}'.format( self.reverse_proxy_mode)): self.skipTest("Preflight invalidated all chunk+CL queries.") method = self._get_valid_chunk_method() self.req.method = method self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') # like in other tests, req2 is not really used by our client # but just used to build req1's body self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) # this will cover the 0\r\n\r\n end-of-chunks regular markup # from query1 that will be present after query2 when embedded self.req2.add_header('Content-Length', '5') req2 = str(self.req2) self.req.add_chunk(req2) # Adding a wrong content length which only covers the chunk size # and first crlf of first chunk. size_of_first_chunk = len(req2) first_chunk_attr = hex(size_of_first_chunk)[2:] size_of_first_chunk_size = len(str(first_chunk_attr)) wrong_length = size_of_first_chunk_size + 2 self.req.add_header('Content-Length', str(wrong_length)) self._add_default_status_map(valid=False) # local additions self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { 'Content-Length: 5': self.STATUS_TRANSMITTED_CRAP, } # we send only 1 query (with a body). # but if C-L has the priority this will be 2 queries self._end_expected_error(expected_number=1) def test_2012_preflight_wrong_content_length_and_chunked(self): """Mix both Content-Length and chunked (inv), may be rejected. This is not plainly wrong, so it can still be accepted, but if it is not rejected chunked has priority on Content-Length. This one is like the previous one, but headers order is inverted. """ self.real_test = "{0}".format(inspect.stack()[0][3]) self.setGravity(self.GRAVITY_MINOR) method = self._get_valid_chunk_method() self.req.set_method(method) # like in other tests, req2 is not really used by our client # but just used to build req1's body self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) # this will cover the 0\r\n\r\n end-of-chunks regular markup # from query1 that will be present after query2 when embedded self.req2.add_header('Content-Length', '5') req2 = str(self.req2) # Adding a wrong content length which only covers the chunk size # and first crlf of first chunk. size_of_first_chunk = len(req2) first_chunk_attr = hex(size_of_first_chunk)[2:] size_of_first_chunk_size = len(str(first_chunk_attr)) wrong_length = size_of_first_chunk_size + 2 self.req.add_header('Content-Length', str(wrong_length)) self.req.add_chunk(req2) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self._add_default_status_map(valid=False) # local additions self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { 'Content-Length: 0': self.STATUS_TRANSMITTED_CRAP, } # we send only 1 query (with a body). # but if C-L has the priority this will be 2 queries # Note that this query MAY be completly valid, but it is safer # if it is rejected (so it's really a minor issue self._end_expected_error(expected_number=1) Register.flag('cl_and_chunk_{0}'.format(self.reverse_proxy_mode), (self.status not in [ self.STATUS_ACCEPTED, self.STATUS_TRANSMITTED, self.STATUS_SPLITTED, self.STATUS_WOOKIEE ])) def test_2020_bad_chunked_transfer_encoding(self): """chunk not used as last marker in Transfer-Encoding header. """ self.real_test = "{0}".format(inspect.stack()[0][3]) self.setGravity(self.GRAVITY_MINOR) method = self._get_valid_chunk_method() self.req.method = method # chunk MUST be the last marker self.req.add_header('Transfer-Encoding', 'chunked, zorg') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req.add_chunk('Hello') self.req.add_chunk('World') self._end_expected_error() def test_2021_chunked_header_hidden_by_NULL(self): """Transfer-Encoding header splitted by a NULL char. """ self.real_test = "{0}".format(inspect.stack()[0][3]) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Transfer-{0}: 42{1}Encoding'.format( Tools.NULL, Tools.CRLF), 'chunked', sep=':') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) req2 = Tools.CRLF + str(self.req2) self.req.add_header('Content-Length', str(len(req2))) # by forcing chunk_size to 0 we will detect splitters # as they will apply the chunked mode and req2 wont be a body anymore self.req.add_chunk(req2, chunk_size=0) self.req.end_of_chunks = False self._add_default_status_map(valid=False) # local additions self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { 'Content-Length: 0': self.STATUS_TRANSMITTED_CRAP, 'Transfer-{0}: 42'.format(Tools.NULL): self.STATUS_TRANSMITTED, } self._end_expected_error(expected_number=1) def test_2022_chunked_header_hidden_by_bad_eol_1(self): """Use chunk+Content length, but chunk header should be invalid. """ self.real_test = "{0}".format(inspect.stack()[0][3]) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Dummy', 'Header{0}{1}Transfer-Encoding:chunked'.format( Tools.CR, ''), sep=':') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) req2 = Tools.CRLF + str(self.req2) self.req.add_header('Content-Length', str(len(req2))) # by forcing chunk_size to 0 we will detect splitters # as they will apply the chunked mode and req2 wont be a body anymore self.req.add_chunk(req2, chunk_size=0) self.req.end_of_chunks = False self._add_default_status_map(valid=False) # local additions self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { 'Content-Length: 0': self.STATUS_TRANSMITTED_CRAP, 'Dummy: Header\r' 'Transfer-Encoding:chunked': self.STATUS_TRANSMITTED, 'Dummy: Header\r' 'Transfer-Encoding: chunked': self.STATUS_TRANSMITTED, } self._end_expected_error(expected_number=1) def test_2023_chunked_header_hidden_by_bad_eol_2(self): """Use chunk+Content length, but chunk header should be invalid. """ self.real_test = "{0}".format(inspect.stack()[0][3]) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Dummy', 'Header{0}{1}Transfer-Encoding:chunked'.format( Tools.CR, Tools.SP), sep=':') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) req2 = Tools.CRLF + str(self.req2) self.req.add_header('Content-Length', str(len(req2))) # by forcing chunk_size to 0 we will detect splitters # as they will apply the chunked mode and req2 wont be a body anymore self.req.add_chunk(req2, chunk_size=0) self.req.end_of_chunks = False self._add_default_status_map(valid=False) # local additions self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { 'Content-Length: 0': self.STATUS_TRANSMITTED_CRAP, 'Dummy: Header\r ' 'Transfer-Encoding:chunked': self.STATUS_TRANSMITTED, 'Dummy: Header\r ' 'Transfer-Encoding: chunked': self.STATUS_TRANSMITTED, } self._end_expected_error(expected_number=1) def test_2024_chunked_header_hidden_by_bad_eol_3(self): """Use chunk+Content length, but chunk header should be invalid. The query should be rejected, or only apply Content-Lenght. If Content-Length is applied. """ self.real_test = "{0}".format(inspect.stack()[0][3]) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Dummy', 'Header{0}{1}Transfer-Encoding:chunked'.format( Tools.CR, 'Z'), sep=':') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) req2 = Tools.CRLF + str(self.req2) self.req.add_header('Content-Length', str(len(req2))) # by forcing chunk_size to 0 we will detect splitters # as they will apply the chunked mode and req2 wont be a body anymore self.req.add_chunk(req2, chunk_size=0) self.req.end_of_chunks = False self._add_default_status_map(valid=False) # local additions self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { 'Content-Length: 0': self.STATUS_TRANSMITTED_CRAP, 'Dummy: Header\rZ' 'Transfer-Encoding:chunked': self.STATUS_TRANSMITTED, 'Dummy: Header\rZ' 'Transfer-Encoding: chunked': self.STATUS_TRANSMITTED, '\nZTransfer-Encoding:chunked': self.STATUS_TRANSMITTED_CRAP, '\nZ Transfer-Encoding:chunked': self.STATUS_TRANSMITTED_CRAP, } self._end_expected_error(expected_number=1) def test_2030_chunked_size_truncation(self): """Try an int truncation on the chunk size. """ self.real_test = "{0}".format(inspect.stack()[0][3]) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) # this will cover the 0\r\n\r\n end-of-chunks regular markup # from query1 that will be present after query2 when embedded self.req2.add_header('Content-Length', '5') req2 = str(self.req2) something = '{0}{1}'.format(Tools.CRLF, req2) # Apache issue, truncation in hexadecimal chunk size, final size is 0 # for Apache, so the rest of the chunk is interpreted as a trailer (if # any). This means an end of query. # we add 4 chars to cover the chunk size of second header +CRLF # chunk_size = 0000000...00000004e # which will be read as chunk_size = 00000000000000000000000000000 chunk_size = "0" * 33 + hex(len(something))[2:] self.req.add_chunk(something, chunk_size=chunk_size) self._add_default_status_map(valid=True, always_allow_rejected=True) # local additions # here transmission is valid, but some actr may split on this syntax # so it would be better to fix the 00000... prefix syntax self.status_map[self.STATUS_TRANSMITTED_CRAP] = self.GRAVITY_WARNING # for RP mode: self.transmission_zone = Tools.ZONE_CHUNK_SIZE self.transmission_map = { '000000000000000000000000': self.STATUS_TRANSMITTED_CRAP, } # this is a valid query in fact. # so we should not expect an error (but 2 responses would be very bad) self._end_regular_query(expected_number=1, can_be_rejected=False)
class BaseTest(unittest.TestCase): GRAVITY_UNKNOWN = 0 GRAVITY_MINOR = 1 GRAVITY_WARNING = 2 GRAVITY_CRITICAL = 3 GRAVITY_OK = 4 gravity_format = { GRAVITY_UNKNOWN: u'Unknown', GRAVITY_MINOR: u'Minor', GRAVITY_WARNING: u'Warning', GRAVITY_CRITICAL: u'Critical', GRAVITY_OK: u'Ok', } STATUS_UNKNOWN = 'Unknown' STATUS_ACCEPTED = 'Accepted' STATUS_REJECTED = 'Rejected' STATUS_ERR400 = 'Err400' STATUS_ERR401 = 'Err401' STATUS_ERR403 = 'Err403' STATUS_ERR404 = 'Err404' STATUS_ERR405 = 'Err405' STATUS_ERR411 = 'Err411' STATUS_ERR413 = 'Err413' STATUS_ERR414 = 'Err414' STATUS_ERR500 = 'Err500' STATUS_501_NOT_IMPLEMENTED = '501NotImpl.' STATUS_5032 = '502or5023' STATUS_505_NOT_SUPPORTED = '505' STATUS_SPLITTED = 'Splitted' STATUS_09DOWNGRADE = 'Downgrade09' STATUS_09OK = 'Regular09' STATUS_RED_301 = 'Redir301' STATUS_RED_302 = 'Redir302' STATUS_TRANSMITTED = 'Transmit' STATUS_TRANSMITTED_EXACT = 'Transmit+' STATUS_TRANSMITTED_CRAP = 'Transmit^!$#^' STATUS_REMOVED = 'Removed' STATUS_WOOKIEE = 'Wookiee' status_format = { STATUS_REJECTED: { 'long': u'rejected', 'short': u'r' }, STATUS_ERR400: { 'long': u'-err400-', 'short': u'4' }, STATUS_ERR401: { 'long': u'-err401-', 'short': u'4' }, STATUS_ERR403: { 'long': u'-err403-', 'short': u'4' }, STATUS_ERR404: { 'long': u'-err404-', 'short': u'4' }, STATUS_ERR405: { 'long': u'-err405-', 'short': u'4' }, STATUS_ERR411: { 'long': u'-err411-', 'short': u'4' }, STATUS_ERR413: { 'long': u'-err413-', 'short': u'4' }, STATUS_ERR414: { 'long': u'-err414-', 'short': u'4' }, STATUS_ERR500: { 'long': u'-err500-', 'short': u'5' }, STATUS_RED_301: { 'long': u'-red301-', 'short': u'3' }, STATUS_RED_302: { 'long': u'-red302-', 'short': u'3' }, STATUS_501_NOT_IMPLEMENTED: { 'long': u'-501-ni', 'short': u'n' }, STATUS_5032: { 'long': u'-502-03', 'short': u'x' }, STATUS_505_NOT_SUPPORTED: { 'long': u'-505-ns', 'short': u'5' }, STATUS_ACCEPTED: { 'long': u'accepted', 'short': u'a' }, STATUS_UNKNOWN: { 'long': u'-unknown', 'short': u'u' }, STATUS_SPLITTED: { 'long': u'splitted', 'short': u's' }, STATUS_09DOWNGRADE: { 'long': u'-down-09', 'short': u'D' }, STATUS_09OK: { 'long': u'regular9', 'short': u'9' }, STATUS_TRANSMITTED: { 'long': u'transmit', 'short': u't' }, STATUS_TRANSMITTED_EXACT: { 'long': u'transmit', 'short': u'T' }, STATUS_TRANSMITTED_CRAP: { 'long': u'transmit', 'short': u'Z' }, STATUS_REMOVED: { 'long': u'removed-', 'short': u'R' }, STATUS_WOOKIEE: { 'long': u'wookiee-', 'short': u'W' }, } SEND_MODE_UNIQUE = 0 SEND_MODE_PIPE = 1 use_backend_location = False req = None req1 = None req2 = None send_mode = None reverse_proxy_mode = False def __init__(self, methodName="runTest"): self.config = ConfigFactory.getConfig() self.status = self.STATUS_UNKNOWN self.real_test = None self.gravity = self.GRAVITY_UNKNOWN self.status_map = None self.transmission_zone = None self.transmission_map = None if self.send_mode is None: self.send_mode = self.SEND_MODE_UNIQUE self.use_backend_location = False self.reverse_proxy_mode = False super(BaseTest, self).__init__(methodName) self.addCleanup(self.CustomCleanup) def __str__(self): return "[{0}] ({1})".format(self._testMethodName, strclass(self.__class__)) def setUp(self): super(BaseTest, self).setUp() if (not self.reverse_proxy_mode and self.config.getboolean('REVERSEPROXY_TESTS_ONLY')): self.skipTest('Only Reverse Proxy (server) tests are ' 'allowed by config') self._prepare_queries() def setStatus(self, status): if status not in self.status_format.keys(): raise ValueError('Unexpected Test status {0}'.format(status)) self.status = status def getStatus(self, format=None): if format is None: return self.status else: try: return self.status_format[self.status][format] except KeyError: return '' def getGravity(self, human=False): if not human: return self.gravity else: return self.gravity_format[self.gravity] def setGravity(self, gravity): if gravity not in self.gravity_format.keys(): raise ValueError('Unexpected Test gravity {0}'.format(gravity)) self.gravity = gravity def CustomCleanup(self): pass # print('CUSTOmCleanup') def _prepare_queries(self): if self.send_mode == self.SEND_MODE_UNIQUE: self._prepare_simple_test() elif self.send_mode == self.SEND_MODE_PIPE: self._prepare_pipe_test() else: raise ValueError('Unknown send mode for test HTTP queries.') def get_default_location(self, with_prefix=None): if with_prefix is None: with_prefix = self.use_backend_location if with_prefix: return "{0}{1}".format(self.config.get('BACKEND_LOCATION_PREFIX'), self.config.get('SERVER_DEFAULT_LOCATION')) else: return self.config.get('SERVER_DEFAULT_LOCATION') def get_non_default_location(self, with_prefix=None): if with_prefix is None: with_prefix = self.use_backend_location if with_prefix: return "{0}{1}".format( self.config.get('BACKEND_LOCATION_PREFIX'), self.config.get('SERVER_NON_DEFAULT_LOCATION')) else: return self.config.get('SERVER_NON_DEFAULT_LOCATION') def get_wookiee_location(self, with_prefix=None): if with_prefix is None: with_prefix = self.use_backend_location if with_prefix: return "{0}{1}".format(self.config.get('BACKEND_LOCATION_PREFIX'), self.config.get('BACKEND_WOOKIEE_LOCATION')) else: return self.config.get('BACKEND_WOOKIEE_LOCATION') def _get_valid_chunk_method(self): if not Register.hasFlag('post_chunk_{0}'.format( self.reverse_proxy_mode)): if not Register.hasFlag('get_chunk_{0}'.format( self.reverse_proxy_mode)): self.skipTest("Preflight invalidated all chunk queries.") return 'GET' else: return 'POST' def _prepare_simple_test(self): outmsg("={0}=".format(self.real_test)) self.req = Request(id(self)) location = self.get_default_location() self.req.set_location(location, random=True) def _prepare_pipe_test(self, method1='GET', method2='GET'): outmsg("={0}=".format(self.real_test)) if Register.flags['keepalive'] is False: self.skipTest("No keepalive support.") if Register.flags['pipelining'] is False: self.skipTest("No pipelining support.") self.send_mode = self.SEND_MODE_PIPE location = self.get_default_location( with_prefix=self.use_backend_location) self.req1 = Request(id(self)) self.req1.add_header('Connection', 'keep-alive') self.req1.set_location(location, random=True) self.req1.set_method(method1) self.req2 = Request(id(self)) self.req2.set_location(location, random=True) self.req2.set_method(method2) def _hook_while_sending(self): pass def send_queries(self): responses = None if self.send_mode == self.SEND_MODE_UNIQUE: with Client() as csock: csock.send(self.req) responses = csock.read_all() self._hook_while_sending() elif self.send_mode == self.SEND_MODE_PIPE: with Client() as csock: # csock.send(u'{0}{1}'.format(self.req1, self.req2)) csock.send(self.req1) csock.send(self.req2) responses = csock.read_all() self._hook_while_sending() else: raise ValueError('Unknown send mode for test HTTP queries.') outmsg(str(responses)) return responses def _end_regular_query(self, responses=None, http09_allowed=False, can_be_rejected=False, expected_number=1, status_map=None): self._end_almost_regular_query(responses, http09_allowed=http09_allowed, expected_number=expected_number, regular_expected=True, status_map=status_map) allowed = [self.STATUS_ACCEPTED] if can_be_rejected: # sometimes it's 'regular', but not really allowed.append(self.STATUS_REJECTED) allowed.append(self.STATUS_ERR400) allowed.append(self.STATUS_ERR413) allowed.append(self.STATUS_ERR411) allowed.append(self.STATUS_501_NOT_IMPLEMENTED) allowed.append(self.STATUS_505_NOT_SUPPORTED) # rfc 7230 allows 301 on bad request line allowed.append(self.STATUS_RED_301) if http09_allowed: allowed.append(self.STATUS_09OK) self.assertIn( self.status, allowed, 'Bad response status {0} for regular query'.format(self.status)) def _end_almost_regular_query(self, responses=None, http09_allowed=False, regular_expected=False, expected_number=1, status_map=None): "same as _end_regular_query but without the status assertions." if responses is None: responses = self.send_queries() self.analysis(responses, expected_number=expected_number, http09_allowed=http09_allowed, regular_expected=regular_expected) self.adjust_status_by_map(status_map) def _end_expected_error(self, responses=None, expected_number=1, regular_expected=False, http09_allowed=False, status_map=None): "Bad queries test end management." if responses is None: responses = self.send_queries() self.analysis(responses, expected_number=expected_number, http09_allowed=http09_allowed, regular_expected=regular_expected) self.adjust_status_by_map(status_map) self.assertNotEqual( self.status, self.STATUS_WOOKIEE, 'Wookiee response detected,' + ' this should never happen.') self.assertIn( self.status, [ self.STATUS_REJECTED, self.STATUS_ERR400, self.STATUS_ERR411, self.STATUS_ERR413, self.STATUS_ERR414, # yes, too bad this is used by Tomcat for most 400 self.STATUS_ERR500, self.STATUS_501_NOT_IMPLEMENTED, self.STATUS_505_NOT_SUPPORTED ], 'Bad response status "{0}"'.format(self.status)) def _end_1st_line_query(self, responses=None, http09_allowed=False, expected_number=1, status_map=None): if responses is None: responses = self.send_queries() self.analysis(responses, http09_allowed=http09_allowed, regular_expected=False) self.adjust_status_by_map(status_map) allowed = [] for status_elt, status_gravity in Tools.iteritems(self.status_map): if status_gravity is self.GRAVITY_OK: allowed.append(status_elt) self.assertIn(self.status, allowed) def _add_default_status_map(self, valid=False, http09_allowed=False, always_allow_rejected=False): self.status_map = { # this is only dangerous if any RP is a transmitter of # such bad queries. Should be very rare. self.STATUS_09DOWNGRADE: self.GRAVITY_WARNING, self.STATUS_09OK: self.GRAVITY_WARNING, # but, hey, if you are such proxy that's a very bad cleanup # FIXME: ensure a RP having final 0.9 response, without # tansmission in 0.9, is marked as critical self.STATUS_TRANSMITTED_EXACT: self.GRAVITY_CRITICAL, self.STATUS_TRANSMITTED_CRAP: self.GRAVITY_CRITICAL, self.STATUS_TRANSMITTED: self.GRAVITY_WARNING, self.STATUS_WOOKIEE: self.GRAVITY_CRITICAL, self.STATUS_SPLITTED: self.GRAVITY_CRITICAL, } if valid: self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_OK else: self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_MINOR if always_allow_rejected or not valid: self.status_map[self.STATUS_REJECTED] = self.GRAVITY_OK self.status_map[self.STATUS_ERR400] = self.GRAVITY_OK self.status_map[self.STATUS_ERR413] = self.GRAVITY_OK self.status_map[self.STATUS_ERR414] = self.GRAVITY_OK self.status_map[self.STATUS_ERR411] = self.GRAVITY_OK self.status_map[self.STATUS_501_NOT_IMPLEMENTED] = self.GRAVITY_OK self.status_map[self.STATUS_505_NOT_SUPPORTED] = self.GRAVITY_OK # rfc 7230 allows 301 on bad request line self.status_map[self.STATUS_RED_301] = self.GRAVITY_OK if http09_allowed: self.status_map[self.STATUS_09OK] = self.GRAVITY_OK self.status_map[self.STATUS_09DOWNGRADE] = self.GRAVITY_OK def adjust_status_by_map(self, status_map=None): "Adjust gravity of the test based on status." if status_map is not None: if self.status in status_map: self.setGravity(status_map[self.status]) if self.status_map is not None: if self.status in self.status_map: if self.status_map[self.status] is not self.GRAVITY_OK: self.setGravity(self.status_map[self.status]) def analysis(self, responses, expected_number=1, http09_allowed=False, regular_expected=False): "Launch deep analysis of the responses status." self.count_responses = responses.count if not self.count_responses: self.setStatus(self.STATUS_REJECTED) elif self.count_responses > expected_number: # Splitting responses is ALWAYS Critical self.gravity = self.GRAVITY_CRITICAL self.setStatus(self.STATUS_SPLITTED) else: for response in responses: # Continue checking while test status is undecided or while # all responses are accepted if (self.STATUS_UNKNOWN == self.status or self.STATUS_ACCEPTED == self.status): self.check_for_errors(response) if (self.STATUS_UNKNOWN == self.status or self.STATUS_ACCEPTED == self.status): self.check_for_redirect(response) if (self.STATUS_UNKNOWN == self.status or self.STATUS_ACCEPTED == self.status): self.check_for_regular_content( response, http09_allowed=http09_allowed, required=regular_expected) # check_for_regular_content may have set self.STATUS_09OK if (999 == response.code and not http09_allowed and self.status != self.STATUS_09OK): self.setStatus(self.STATUS_09DOWNGRADE) # raise AssertionError('Unauthorized HTTP/0.9 response') # @deprecated def assertBodyContainsDefaultContent(self, body): """Assert that the given bytes contains the default location content""" # we obtain an unicode string, body contains only bytes, so we need # to obtain a b'' expected = self.config.get('SERVER_DEFAULT_LOCATION_CONTENT').encode( 'utf8') if expected not in body: raise AssertionError('{0} is not present in body'.format(expected)) def check_for_errors(self, response): if ((b" -mMMNNdhdmhyhNs/mmy+mmyyy+shdo/-...." in response.body) and (b"``.-:/mdMNmh++dNddddh+hmod+/ohdy" in response.body) and (b"Wookiee !" in response.body)): self.setStatus(self.STATUS_WOOKIEE) elif Response.ERROR_HTTP09_RESPONSE in response.errors: if (b"400" in response.body and b"Bad Request" in response.body): self.setStatus(self.STATUS_ERR400) if (b"401" in response.body and b"Unauthorized" in response.body): self.setStatus(self.STATUS_ERR401) if (b"403" in response.body and b"Forbidden" in response.body): self.setStatus(self.STATUS_ERR403) if (b"404" in response.body and b"Not Found" in response.body): self.setStatus(self.STATUS_ERR404) if (b"405" in response.body and b"Method Not Allowed" in response.body): self.setStatus(self.STATUS_ERR405) if (b"411" in response.body and b"Length Required" in response.body): self.setStatus(self.STATUS_ERR411) if (b"413" in response.body and b"Request Entity Too Large" in response.body): self.setStatus(self.STATUS_ERR413) if (b"414" in response.body and b"Request URI too long" in response.body): self.setStatus(self.STATUS_ERR414) if (b"501" in response.body and b"Not Implemented" in response.body): self.setStatus(self.STATUS_501_NOT_IMPLEMENTED) if (b"505" in response.body and b"Not Supported" in response.body): self.setStatus(self.STATUS_505_NOT_SUPPORTED) if ((b"502" in response.body or b"503" in response.body) and (b"Bad gateway" in response.body or b"Service Unavailable" in response.body)): self.setStatus(self.STATUS_5032) else: if 400 == response.code: self.setStatus(self.STATUS_ERR400) if 401 == response.code: self.setStatus(self.STATUS_ERR401) if 403 == response.code: self.setStatus(self.STATUS_ERR403) if 404 == response.code: self.setStatus(self.STATUS_ERR404) if 411 == response.code: self.setStatus(self.STATUS_ERR411) if 413 == response.code: self.setStatus(self.STATUS_ERR413) if 414 == response.code: self.setStatus(self.STATUS_ERR414) if 405 == response.code: self.setStatus(self.STATUS_ERR405) if 501 == response.code: self.setStatus(self.STATUS_501_NOT_IMPLEMENTED) if 505 == response.code: self.setStatus(self.STATUS_505_NOT_SUPPORTED) if 502 == response.code: self.setStatus(self.STATUS_5032) if 503 == response.code: self.setStatus(self.STATUS_5032) def check_for_redirect(self, response): if Response.ERROR_HTTP09_RESPONSE in response.errors: if (b"301" in response.body and b"Moved Permanently" in response.body): self.setStatus(self.STATUS_RED_301) elif (b"302" in response.body and b"Found" in response.body): self.setStatus(self.STATUS_RED_302) else: if 301 == response.code: self.setStatus(self.STATUS_RED_301) elif 302 == response.code: self.setStatus(self.STATUS_RED_302) def _get_expected_content(self): expected = self.config.get('SERVER_DEFAULT_LOCATION_CONTENT').encode( 'utf8') return expected def check_for_regular_content(self, response, http09_allowed=False, required=True): if 200 == response.code: self.setStatus(self.STATUS_ACCEPTED) expected = self._get_expected_content() if Response.ERROR_HTTP09_RESPONSE in response.errors: if expected in response.body: if http09_allowed: self.setStatus(self.STATUS_09OK) else: # regular content present in an http/0.9 response # this is dangerous unless you really requested in 0.9 mode self.setStatus(self.STATUS_09DOWNGRADE) self.gravity = self.GRAVITY_CRITICAL else: if required and expected not in response.body: raise AssertionError( '{0} is not present in body'.format(expected))
class AbstractChunksOverflow(BaseTest): def __init__(self, *args, **kwargs): super(AbstractChunksOverflow, self).__init__(*args, **kwargs) # for RP mode message analysis self.transmission_zone = Tools.ZONE_HEADERS self.send_mode = self.SEND_MODE_UNIQUE def setUp(self): # allows children to alter main behavior of all tests self.setLocalSettings() super(AbstractChunksOverflow, self).setUp() def setLocalSettings(self): "You should overwrite this one." self.nb = 65535 def test_2040_chunked_size_overflow(self): """Try an int overflow on the chunk size """ self.real_test = "{0}_{1}".format(inspect.stack()[0][3], self.nb) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) # this will cover the 0\r\n\r\n end-of-chunks regular markup # from query 1 that will be present after query2 when embedded self.req2.add_header('Content-Length', '5') req2 = str(self.req2) # we add 4 chars to cover the chunk size of second header +CRLF chunk_size = hex(self.nb)[2:] self.req.add_chunk(Tools.CRLF + req2, chunk_size=chunk_size) self._add_default_status_map(valid=False) # local additions # here accepting a chunked query with crap in chunk size attribute # is always quite strange self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_WARNING self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { '\r\n{0}\r\n'.format(chunk_size): self.STATUS_TRANSMITTED, 'Content-Length: {0}\r\n'.format(self.nb): self.STATUS_TRANSMITTED_CRAP, 'Content-Length:{0}\r\n'.format(self.nb): self.STATUS_TRANSMITTED_CRAP, } self._end_expected_error(expected_number=1) def test_2041_chunked_size_overflow_with_trailers(self): """Try an int overflow on the chunk size, add trailer headers. """ self.real_test = "{0}_{1}".format(inspect.stack()[0][3], self.nb) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req.add_header('Trailer', 'Zorglub') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) # this will cover the 0\r\n\r\n end-of-chunks regular markup # from query 1 that will be present after query2 when embedded self.req2.add_header('Content-Length', '5') req2 = str(self.req2) # We add 4 chars to cover the chunk size of second header +CRLF chunk_size = hex(self.nb)[2:] trailers = u'Zorglub: Bulgroz\r\nContent-Length:42\r\n' bad_chunk = '{0}{1}{2}'.format(trailers, Tools.CRLF, req2) self.req.add_chunk(bad_chunk, chunk_size=chunk_size) self._add_default_status_map(valid=False) # local additions # here accepting a chunked query with crap in chunk size attribute # is always quite strange self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_WARNING self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { '\r\n{0}\r\n'.format(chunk_size): self.STATUS_TRANSMITTED, 'Content-Length: {0}\r\n'.format(self.nb): self.STATUS_TRANSMITTED_CRAP, 'Content-Length:{0}\r\n'.format(self.nb): self.STATUS_TRANSMITTED_CRAP, # Content-Length header in trailers is not forbidden # but at least it should have no consequences, so we should # not find it in the headers. 'Content-Length:42\r\n': self.STATUS_TRANSMITTED_CRAP, 'Content-Length: 42\r\n': self.STATUS_TRANSMITTED_CRAP, } self._end_expected_error(expected_number=1) def test_2042_chunked_size_overflow_with_delayed_chunks(self): """Try an int overflow on the chunk size, add trailer headers. """ self.real_test = "{0}_{1}".format(inspect.stack()[0][3], self.nb) method = self._get_valid_chunk_method() self.req.set_method(method) self.setGravity(self.GRAVITY_CRITICAL) self.req.add_header('Transfer-Encoding', 'chunked') self.req.add_header('Content-Type', 'application/x-www-form-urlencoded') self.req.add_header('Trailer', 'Zorglub') self.req2 = Request(id(self)) self.req2.method = method wlocation = self.get_wookiee_location() self.req2.set_location(wlocation, random=True) self.req2.add_header('Content-Length', '5') req2 = str(self.req2) chunk_size = hex(self.nb)[2:] bad_chunk = '' self.req.add_chunk(bad_chunk, chunk_size=chunk_size, chunk_eol=u'') self.req.add_delayed_chunk(req2, delay=0.5) self.req.add_delayed_chunk(req2, delay=2) self._add_default_status_map(valid=False) # local additions # here accepting a chunked query with crap in chunk size attribute # is always quite strange self.status_map[self.STATUS_ACCEPTED] = self.GRAVITY_WARNING self.status_map[self.STATUS_TRANSMITTED] = self.GRAVITY_CRITICAL # for RP mode: self.transmission_map = { '\r\n{0}\r\n'.format(chunk_size): self.STATUS_TRANSMITTED, 'Content-Length: {0}\r\n'.format(self.nb): self.STATUS_TRANSMITTED_CRAP, 'Content-Length:{0}\r\n'.format(self.nb): self.STATUS_TRANSMITTED_CRAP, } self._end_expected_error(expected_number=1)