Example #1
0
    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
                      ]))
Example #2
0
    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)
Example #3
0
    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)
Example #4
0
    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)
Example #5
0
    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
                      ]))
Example #6
0
 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)
Example #7
0
    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))
Example #8
0
    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)
Example #9
0
    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))
Example #10
0
    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)
Example #11
0
 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)
Example #12
0
    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)
Example #13
0
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))
Example #14
0
    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))
Example #15
0
    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)
Example #16
0
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))
Example #17
0
 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)
Example #18
0
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))
Example #19
0
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)
Example #20
0
 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)
Example #21
0
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))
Example #22
0
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)