예제 #1
0
파일: remote.py 프로젝트: elias-pap/stem
class TestDescriptorDownloader(unittest.TestCase):
  def tearDown(self):
    # prevent our mocks from impacting other tests
    stem.descriptor.remote.SINGLETON_DOWNLOADER = None

  @patch('stem.client.Relay.connect', _orport_mock(TEST_DESCRIPTOR))
  def test_using_orport(self):
    """
    Download a descriptor through the ORPort.
    """

    reply = stem.descriptor.remote.their_server_descriptor(
      endpoints = [stem.ORPort('12.34.56.78', 1100)],
      validate = True,
    )

    self.assertEqual(1, len(list(reply)))
    self.assertEqual('moria1', list(reply)[0].nickname)
    self.assertEqual(5, len(reply.reply_headers))

  def test_orport_response_code_headers(self):
    """
    When successful Tor provides a '200 OK' status, but we should accept other 2xx
    response codes, reason text, and recognize HTTP errors.
    """

    response_code_headers = (
      b'HTTP/1.0 200 OK\r\n',
      b'HTTP/1.0 205 OK\r\n',
      b'HTTP/1.0 200 This is also alright\r\n',
    )

    for header in response_code_headers:
      with patch('stem.client.Relay.connect', _orport_mock(TEST_DESCRIPTOR, response_code_header = header)):
        stem.descriptor.remote.their_server_descriptor(
          endpoints = [stem.ORPort('12.34.56.78', 1100)],
          validate = True,
        ).run()

    with patch('stem.client.Relay.connect', _orport_mock(TEST_DESCRIPTOR, response_code_header = b'HTTP/1.0 500 Kaboom\r\n')):
      request = stem.descriptor.remote.their_server_descriptor(
        endpoints = [stem.ORPort('12.34.56.78', 1100)],
        validate = True,
      )

      self.assertRaisesWith(stem.ProtocolError, "Response should begin with HTTP success, but was 'HTTP/1.0 500 Kaboom'", request.run)

  @patch(URL_OPEN, _dirport_mock(TEST_DESCRIPTOR))
  def test_using_dirport(self):
    """
    Download a descriptor through the DirPort.
    """

    reply = stem.descriptor.remote.their_server_descriptor(
      endpoints = [stem.DirPort('12.34.56.78', 1100)],
      validate = True,
    )

    self.assertEqual(1, len(list(reply)))
    self.assertEqual('moria1', list(reply)[0].nickname)
    self.assertEqual(5, len(reply.reply_headers))

  def test_gzip_url_override(self):
    query = stem.descriptor.remote.Query(TEST_RESOURCE, start = False)
    self.assertEqual([Compression.PLAINTEXT], query.compression)
    self.assertEqual(TEST_RESOURCE, query.resource)

    query = stem.descriptor.remote.Query(TEST_RESOURCE + '.z', compression = Compression.PLAINTEXT, start = False)
    self.assertEqual([Compression.GZIP], query.compression)
    self.assertEqual(TEST_RESOURCE, query.resource)

  def test_zstd_support_check(self):
    with patch('stem.prereq.is_zstd_available', Mock(return_value = True)):
      query = stem.descriptor.remote.Query(TEST_RESOURCE, compression = Compression.ZSTD, start = False)
      self.assertEqual([Compression.ZSTD], query.compression)

    with patch('stem.prereq.is_zstd_available', Mock(return_value = False)):
      query = stem.descriptor.remote.Query(TEST_RESOURCE, compression = Compression.ZSTD, start = False)
      self.assertEqual([Compression.PLAINTEXT], query.compression)

  def test_lzma_support_check(self):
    with patch('stem.prereq.is_lzma_available', Mock(return_value = True)):
      query = stem.descriptor.remote.Query(TEST_RESOURCE, compression = Compression.LZMA, start = False)
      self.assertEqual([Compression.LZMA], query.compression)

    with patch('stem.prereq.is_lzma_available', Mock(return_value = False)):
      query = stem.descriptor.remote.Query(TEST_RESOURCE, compression = Compression.LZMA, start = False)
      self.assertEqual([Compression.PLAINTEXT], query.compression)

  @patch(URL_OPEN, _dirport_mock(read_resource('compressed_identity'), encoding = 'identity'))
  def test_compression_plaintext(self):
    """
    Download a plaintext descriptor.
    """

    descriptors = list(stem.descriptor.remote.get_server_descriptors(
      '9695DFC35FFEB861329B9F1AB04C46397020CE31',
      compression = Compression.PLAINTEXT,
      validate = True,
    ))

    self.assertEqual(1, len(descriptors))
    self.assertEqual('moria1', descriptors[0].nickname)

  @patch(URL_OPEN, _dirport_mock(read_resource('compressed_gzip'), encoding = 'gzip'))
  def test_compression_gzip(self):
    """
    Download a gip compressed descriptor.
    """

    descriptors = list(stem.descriptor.remote.get_server_descriptors(
      '9695DFC35FFEB861329B9F1AB04C46397020CE31',
      compression = Compression.GZIP,
      validate = True,
    ))

    self.assertEqual(1, len(descriptors))
    self.assertEqual('moria1', descriptors[0].nickname)

  @patch(URL_OPEN, _dirport_mock(read_resource('compressed_zstd'), encoding = 'x-zstd'))
  def test_compression_zstd(self):
    """
    Download a zstd compressed descriptor.
    """

    if not stem.prereq.is_zstd_available():
      self.skipTest('(requires zstd module)')
      return

    descriptors = list(stem.descriptor.remote.get_server_descriptors(
      '9695DFC35FFEB861329B9F1AB04C46397020CE31',
      compression = Compression.ZSTD,
      validate = True,
    ))

    self.assertEqual(1, len(descriptors))
    self.assertEqual('moria1', descriptors[0].nickname)

  @patch(URL_OPEN, _dirport_mock(read_resource('compressed_lzma'), encoding = 'x-tor-lzma'))
  def test_compression_lzma(self):
    """
    Download a lzma compressed descriptor.
    """

    if not stem.prereq.is_lzma_available():
      self.skipTest('(requires lzma module)')
      return

    descriptors = list(stem.descriptor.remote.get_server_descriptors(
      '9695DFC35FFEB861329B9F1AB04C46397020CE31',
      compression = Compression.LZMA,
      validate = True,
    ))

    self.assertEqual(1, len(descriptors))
    self.assertEqual('moria1', descriptors[0].nickname)

  @patch(URL_OPEN, _dirport_mock(TEST_DESCRIPTOR))
  def test_reply_headers(self):
    query = stem.descriptor.remote.get_server_descriptors('9695DFC35FFEB861329B9F1AB04C46397020CE31', start = False)
    self.assertEqual(None, query.reply_headers)  # initially we don't have a reply
    query.run()

    self.assertEqual('Fri, 13 Apr 2018 16:35:50 GMT', query.reply_headers.get('date'))
    self.assertEqual('application/octet-stream', query.reply_headers.get('content-type'))
    self.assertEqual('97.103.17.56', query.reply_headers.get('x-your-address-is'))
    self.assertEqual('no-cache', query.reply_headers.get('pragma'))
    self.assertEqual('identity', query.reply_headers.get('content-encoding'))

    # getting headers should be case insensitive
    self.assertEqual('identity', query.reply_headers.get('CoNtEnT-ENCODING'))

    # request a header that isn't present
    self.assertEqual(None, query.reply_headers.get('no-such-header'))
    self.assertEqual('default', query.reply_headers.get('no-such-header', 'default'))

    descriptors = list(query)
    self.assertEqual(1, len(descriptors))
    self.assertEqual('moria1', descriptors[0].nickname)

  @patch(URL_OPEN, _dirport_mock(TEST_DESCRIPTOR))
  def test_query_download(self):
    """
    Check Query functionality when we successfully download a descriptor.
    """

    query = stem.descriptor.remote.Query(
      TEST_RESOURCE,
      'server-descriptor 1.0',
      endpoints = [('128.31.0.39', 9131)],
      compression = Compression.PLAINTEXT,
      validate = True,
    )

    self.assertEqual(stem.DirPort('128.31.0.39', 9131), query._pick_endpoint())

    descriptors = list(query)
    self.assertEqual(1, len(descriptors))
    desc = descriptors[0]

    self.assertEqual('moria1', desc.nickname)
    self.assertEqual('128.31.0.34', desc.address)
    self.assertEqual('9695DFC35FFEB861329B9F1AB04C46397020CE31', desc.fingerprint)
    self.assertEqual(TEST_DESCRIPTOR.strip(), desc.get_bytes())

  @patch(URL_OPEN, _dirport_mock(b'some malformed stuff'))
  def test_query_with_malformed_content(self):
    """
    Query with malformed descriptor content.
    """

    query = stem.descriptor.remote.Query(
      TEST_RESOURCE,
      'server-descriptor 1.0',
      endpoints = [('128.31.0.39', 9131)],
      compression = Compression.PLAINTEXT,
      validate = True,
    )

    # checking via the iterator

    expected_error_msg = 'Content conform to being a server descriptor:\nsome malformed stuff'

    descriptors = list(query)
    self.assertEqual(0, len(descriptors))
    self.assertEqual(ValueError, type(query.error))
    self.assertEqual(expected_error_msg, str(query.error))

    # check via the run() method

    self.assertRaises(ValueError, query.run)

  @patch(URL_OPEN)
  def test_query_with_timeout(self, dirport_mock):
    def urlopen_call(*args, **kwargs):
      time.sleep(0.06)
      raise socket.timeout('connection timed out')

    dirport_mock.side_effect = urlopen_call

    query = stem.descriptor.remote.Query(
      TEST_RESOURCE,
      'server-descriptor 1.0',
      endpoints = [('128.31.0.39', 9131)],
      fall_back_to_authority = False,
      timeout = 0.1,
      validate = True,
    )

    # After two requests we'll have reached our total permissable timeout.
    # Check that we don't make a third.

    self.assertRaises(socket.timeout, query.run)
    self.assertEqual(2, dirport_mock.call_count)

  def test_query_with_invalid_endpoints(self):
    invalid_endpoints = {
      'hello': "'h' is a str.",
      ('hello',): "'hello' is a str.",
      (15,): "'15' is a int.",
      (('12.34.56.78', 15, 'third arg'),): "'('12.34.56.78', 15, 'third arg')' is a tuple.",
    }

    for endpoints, error_suffix in invalid_endpoints.items():
      expected_error = 'Endpoints must be an stem.ORPort, stem.DirPort, or two value tuple. ' + error_suffix
      self.assertRaisesWith(ValueError, expected_error, stem.descriptor.remote.Query, TEST_RESOURCE, 'server-descriptor 1.0', endpoints = endpoints)

  @patch(URL_OPEN, _dirport_mock(TEST_DESCRIPTOR))
  def test_can_iterate_multiple_times(self):
    query = stem.descriptor.remote.Query(
      TEST_RESOURCE,
      'server-descriptor 1.0',
      endpoints = [('128.31.0.39', 9131)],
      compression = Compression.PLAINTEXT,
      validate = True,
    )

    # check that iterating over the query provides the descriptors each time

    self.assertEqual(1, len(list(query)))
    self.assertEqual(1, len(list(query)))
    self.assertEqual(1, len(list(query)))
예제 #2
0
class TestDescriptorDownloader(unittest.TestCase):
    def tearDown(self):
        # prevent our mocks from impacting other tests
        stem.descriptor.remote.SINGLETON_DOWNLOADER = None

    @mock_download(TEST_DESCRIPTOR)
    def test_initial_startup(self):
        """
    Check that the query can begin downloading in the background when first
    constructed.
    """

        query = stem.descriptor.remote.get_server_descriptors(
            '9695DFC35FFEB861329B9F1AB04C46397020CE31', start=False)
        self.assertTrue(query._downloader_task is None)

        query = stem.descriptor.remote.get_server_descriptors(
            '9695DFC35FFEB861329B9F1AB04C46397020CE31', start=True)
        self.assertTrue(query._downloader_task is not None)
        query.stop()

    def test_stop(self):
        """
    Stop a complete, in-process, and unstarted query.
    """

        # stop a completed query

        with mock_download(TEST_DESCRIPTOR):
            query = stem.descriptor.remote.get_server_descriptors(
                '9695DFC35FFEB861329B9F1AB04C46397020CE31')
            self.assertTrue(query._loop_thread.is_alive())

            query.run()  # complete the query
            self.assertFalse(query._loop_thread.is_alive())
            self.assertFalse(query._downloader_task.cancelled())

            query.stop()  # nothing to do
            self.assertFalse(query._loop_thread.is_alive())
            self.assertFalse(query._downloader_task.cancelled())

        # stop an in-process query

        def pause(*args):
            time.sleep(5)

        with patch('stem.descriptor.remote.Query._download_from',
                   Mock(side_effect=pause)):
            query = stem.descriptor.remote.get_server_descriptors(
                '9695DFC35FFEB861329B9F1AB04C46397020CE31')

            query.stop()  # terminates in-process query
            self.assertFalse(query._loop_thread.is_alive())
            self.assertTrue(query._downloader_task.cancelled())

        # stop an unstarted query

        query = stem.descriptor.remote.get_server_descriptors(
            '9695DFC35FFEB861329B9F1AB04C46397020CE31', start=False)

        query.stop()  # nothing to do
        self.assertTrue(query._loop_thread is None)
        self.assertTrue(query._downloader_task is None)

    @mock_download(TEST_DESCRIPTOR)
    def test_download(self):
        """
    Simply download and parse a descriptor.
    """

        reply = stem.descriptor.remote.their_server_descriptor(
            endpoints=[stem.ORPort('12.34.56.78', 1100)],
            validate=True,
            skip_crypto_validation=not test.require.CRYPTOGRAPHY_AVAILABLE,
        )

        self.assertEqual(1, len(list(reply)))
        self.assertEqual(5, len(reply.reply_headers))

        desc = list(reply)[0]

        self.assertEqual('moria1', desc.nickname)
        self.assertEqual('128.31.0.34', desc.address)
        self.assertEqual('9695DFC35FFEB861329B9F1AB04C46397020CE31',
                         desc.fingerprint)
        self.assertEqual(TEST_DESCRIPTOR.rstrip(), desc.get_bytes())

    def test_response_header_code(self):
        """
    When successful Tor provides a '200 OK' status, but we should accept other 2xx
    response codes, reason text, and recognize HTTP errors.
    """

        response_code_headers = (
            b'HTTP/1.0 200 OK\r\n',
            b'HTTP/1.0 205 OK\r\n',
            b'HTTP/1.0 200 This is also alright\r\n',
        )

        for header in response_code_headers:
            with mock_download(TEST_DESCRIPTOR, response_code_header=header):
                stem.descriptor.remote.their_server_descriptor(
                    endpoints=[stem.ORPort('12.34.56.78', 1100)],
                    validate=True,
                    skip_crypto_validation=not test.require.
                    CRYPTOGRAPHY_AVAILABLE,
                ).run()

        with mock_download(TEST_DESCRIPTOR,
                           response_code_header=b'HTTP/1.0 500 Kaboom\r\n'):
            request = stem.descriptor.remote.their_server_descriptor(
                endpoints=[stem.ORPort('12.34.56.78', 1100)],
                validate=True,
                skip_crypto_validation=not test.require.CRYPTOGRAPHY_AVAILABLE,
            )

            self.assertRaisesRegexp(
                stem.ProtocolError,
                "^Response should begin with HTTP success, but was 'HTTP/1.0 500 Kaboom'",
                request.run)

    @mock_download(TEST_DESCRIPTOR)
    def test_reply_header_data(self):
        query = stem.descriptor.remote.get_server_descriptors(
            '9695DFC35FFEB861329B9F1AB04C46397020CE31', start=False)
        self.assertEqual(
            None, query.reply_headers)  # initially we don't have a reply
        query.run()

        self.assertEqual('Fri, 13 Apr 2018 16:35:50 GMT',
                         query.reply_headers.get('Date'))
        self.assertEqual('application/octet-stream',
                         query.reply_headers.get('Content-Type'))
        self.assertEqual('97.103.17.56',
                         query.reply_headers.get('X-Your-Address-Is'))
        self.assertEqual('no-cache', query.reply_headers.get('Pragma'))
        self.assertEqual('identity',
                         query.reply_headers.get('Content-Encoding'))

        # request a header that isn't present
        self.assertEqual(None, query.reply_headers.get('no-such-header'))
        self.assertEqual('default',
                         query.reply_headers.get('no-such-header', 'default'))

        descriptors = list(query)
        self.assertEqual(1, len(descriptors))
        self.assertEqual('moria1', descriptors[0].nickname)

    def test_gzip_url_override(self):
        query = stem.descriptor.remote.Query(TEST_RESOURCE + '.z',
                                             compression=Compression.PLAINTEXT,
                                             start=False)
        self.assertEqual([stem.descriptor.Compression.GZIP], query.compression)
        self.assertEqual(TEST_RESOURCE, query.resource)

    @mock_download(read_resource('compressed_identity'), encoding='identity')
    def test_compression_plaintext(self):
        """
    Download a plaintext descriptor.
    """

        query = stem.descriptor.remote.get_server_descriptors(
            '9695DFC35FFEB861329B9F1AB04C46397020CE31',
            compression=Compression.PLAINTEXT,
            validate=True,
            skip_crypto_validation=not test.require.CRYPTOGRAPHY_AVAILABLE,
        )

        descriptors = list(query)

        self.assertEqual(1, len(descriptors))
        self.assertEqual('moria1', descriptors[0].nickname)

    @mock_download(read_resource('compressed_gzip'), encoding='gzip')
    def test_compression_gzip(self):
        """
    Download a gip compressed descriptor.
    """

        query = stem.descriptor.remote.get_server_descriptors(
            '9695DFC35FFEB861329B9F1AB04C46397020CE31',
            compression=Compression.GZIP,
            validate=True,
            skip_crypto_validation=not test.require.CRYPTOGRAPHY_AVAILABLE,
        )

        descriptors = list(query)

        self.assertEqual(1, len(descriptors))
        self.assertEqual('moria1', descriptors[0].nickname)

    @mock_download(read_resource('compressed_zstd'), encoding='x-zstd')
    def test_compression_zstd(self):
        """
    Download a zstd compressed descriptor.
    """

        if not Compression.ZSTD.available:
            self.skipTest('(requires zstd module)')

        query = stem.descriptor.remote.get_server_descriptors(
            '9695DFC35FFEB861329B9F1AB04C46397020CE31',
            compression=Compression.ZSTD,
            validate=True,
        )

        descriptors = list(query)

        self.assertEqual(1, len(descriptors))
        self.assertEqual('moria1', descriptors[0].nickname)

    @mock_download(read_resource('compressed_lzma'), encoding='x-tor-lzma')
    def test_compression_lzma(self):
        """
    Download a lzma compressed descriptor.
    """

        if not Compression.LZMA.available:
            self.skipTest('(requires lzma module)')

        query = stem.descriptor.remote.get_server_descriptors(
            '9695DFC35FFEB861329B9F1AB04C46397020CE31',
            compression=Compression.LZMA,
            validate=True,
        )

        descriptors = list(query)

        self.assertEqual(1, len(descriptors))
        self.assertEqual('moria1', descriptors[0].nickname)

    @mock_download(TEST_DESCRIPTOR)
    def test_each_getter(self):
        """
    Surface level exercising of each getter method for downloading descriptors.
    """

        queries = []

        downloader = stem.descriptor.remote.get_instance()

        queries.append(downloader.get_server_descriptors())
        queries.append(downloader.get_extrainfo_descriptors())
        queries.append(downloader.get_microdescriptors('test-hash'))
        queries.append(downloader.get_consensus())
        queries.append(
            downloader.get_vote(
                stem.directory.Authority.from_cache()['moria1']))
        queries.append(downloader.get_key_certificates())
        queries.append(downloader.get_bandwidth_file())
        queries.append(downloader.get_detached_signatures())

        for query in queries:
            query.stop()

    @mock_download(b'some malformed stuff')
    def test_malformed_content(self):
        """
    Query with malformed descriptor content.
    """

        query = stem.descriptor.remote.Query(
            TEST_RESOURCE,
            'server-descriptor 1.0',
            endpoints=[stem.DirPort('128.31.0.39', 9131)],
            compression=Compression.PLAINTEXT,
            validate=True,
        )

        # checking via the iterator

        descriptors = list(query)
        self.assertEqual(0, len(descriptors))
        self.assertEqual(ValueError, type(query.error))
        self.assertEqual("Descriptor must have a 'router' entry",
                         str(query.error))

        # check via the run() method

        self.assertRaises(ValueError, query.run)

    def test_query_with_invalid_endpoints(self):
        invalid_endpoints = {
            'hello': "'h' is a str.",
            ('hello', ): "'hello' is a str.",
            (('hello', ), ): "'('hello',)' is a tuple.",
            (15, ): "'15' is a int.",
        }

        for endpoints, error_suffix in invalid_endpoints.items():
            expected_error = 'Endpoints must be an stem.ORPort or stem.DirPort. ' + error_suffix
            self.assertRaisesWith(ValueError,
                                  expected_error,
                                  stem.descriptor.remote.Query,
                                  TEST_RESOURCE,
                                  'server-descriptor 1.0',
                                  endpoints=endpoints)

    @mock_download(TEST_DESCRIPTOR)
    def test_can_iterate_multiple_times(self):
        query = stem.descriptor.remote.Query(
            TEST_RESOURCE,
            'server-descriptor 1.0',
            endpoints=[stem.DirPort('128.31.0.39', 9131)],
            compression=Compression.PLAINTEXT,
            validate=True,
            skip_crypto_validation=not test.require.CRYPTOGRAPHY_AVAILABLE,
        )

        # check that iterating over the query provides the descriptors each time

        self.assertEqual(1, len(list(query)))
        self.assertEqual(1, len(list(query)))
        self.assertEqual(1, len(list(query)))