def test_formatDataForResponse_no_data(self): request = DummyRequest([self.pagename]) request.method = b'GET' rendered = self.resource.formatDataForResponse(None, request) self.assertEqual(rendered, b'')
def test_IndexResource_render_GET_lang_ta(self): """renderGet() with ?lang=ta should return the index page in Tamil.""" request = DummyRequest([self.pagename]) request.method = b'GET' request.addArg('lang', 'ta') page = self.indexResource.render_GET(request) self.assertSubstring("bridge-களை Tor Browser-உள்", page)
def test_render_GET_RTLlang_obfs3(self): """Test rendering a request for obfs3 bridges in Farsi.""" self.useBenignBridges() request = DummyRequest([b"bridges?transport=obfs3"]) request.method = b'GET' request.getClientIP = lambda: '3.3.3.3' request.headers.update({'accept-language': 'fa,en,en_US,'}) # We actually have to set the request args manually when using a # DummyRequest: request.args.update({'transport': ['obfs3']}) page = self.bridgesResource.render(request) self.assertSubstring("rtl.css", page) self.assertSubstring( # "How to use the above bridge lines" (since there should be # bridges in this response, we don't tell them about alternative # mechanisms for getting bridges) "چگونگی از پلهای خود استفاده کنید", page) for bridgeLine in self.parseBridgesFromHTMLPage(page): # Check that each bridge line had the expected number of fields: bridgeLine = bridgeLine.split(' ') self.assertEqual(len(bridgeLine), 3) self.assertEqual(bridgeLine[0], 'obfs3') # Check that the IP and port seem okay: ip, port = bridgeLine[1].rsplit(':') self.assertIsInstance(ipaddr.IPv4Address(ip), ipaddr.IPv4Address) self.assertIsInstance(int(port), int) self.assertGreater(int(port), 0) self.assertLessEqual(int(port), 65535)
def test_render_GET_vanilla(self): """Test rendering a request for normal, vanilla bridges.""" self.useBenignBridges() request = DummyRequest([self.pagename]) request.method = b'GET' request.getClientIP = lambda: '1.1.1.1' page = self.bridgesResource.render(request) # The response should explain how to use the bridge lines: self.assertTrue("How to start using your bridges" in str(page)) for b in self.parseBridgesFromHTMLPage(page): # Check that each bridge line had the expected number of fields: fields = b.split(' ') self.assertEqual(len(fields), 2) # Check that the IP and port seem okay: ip, port = fields[0].rsplit(':') self.assertIsInstance(ipaddress.ip_address(ip), ipaddress.IPv4Address) self.assertIsInstance(int(port), int) self.assertGreater(int(port), 0) self.assertLessEqual(int(port), 65535)
def setUp(self): """Create a :class:`server.BridgesResource` and protect it with a :class:`GimpCaptchaProtectedResource`. """ # Create our cached CAPTCHA directory: self.captchaDir = 'captchas' if not os.path.isdir(self.captchaDir): os.makedirs(self.captchaDir) # Set up our resources to fake a minimal HTTP(S) server: self.pagename = b'captcha.html' self.root = Resource() # (None, None) is the (distributor, scheduleInterval): self.protectedResource = server.BridgesResource(None, None) self.captchaResource = server.GimpCaptchaProtectedResource( secretKey='42', publicKey='23', hmacKey='abcdefghijklmnopqrstuvwxyz012345', captchaDir='captchas', useForwardedHeader=True, protectedResource=self.protectedResource) self.root.putChild(self.pagename, self.captchaResource) # Set up the basic parts of our faked request: self.request = DummyRequest([self.pagename])
def test_renderAnswer_textplain(self): """If the request format specifies 'plain', we should return content with mimetype 'text/plain'. """ self.useBenignBridges() request = DummyRequest([self.pagename]) request.args.update({'format': ['plain']}) request.getClientIP = lambda: '4.4.4.4' request.method = b'GET' page = self.bridgesResource.render(request) self.assertTrue("html" not in str(page)) # We just need to strip and split it because it looks like: # # 94.235.85.233:9492 0d9d0547c3471cddc473f7288a6abfb54562dc06 # 255.225.204.145:9511 1fb89d618b3a12afe3529fd072127ea08fb50466 # # (Yes, there are two leading spaces at the beginning of each line) # bridgeLines = [line.strip() for line in page.strip().split('\n')] for bridgeLine in bridgeLines: bridgeLine = bridgeLine.split(' ') self.assertEqual(len(bridgeLine), 2) # Check that the IP and port seem okay: ip, port = bridgeLine[0].rsplit(':') self.assertIsInstance(ipaddr.IPv4Address(ip), ipaddr.IPv4Address) self.assertIsInstance(int(port), int) self.assertGreater(int(port), 0) self.assertLessEqual(int(port), 65535)
def test_HowtoResource_render_GET_lang_ru(self): """renderGet() with ?lang=ru should return the howto page in Russian.""" request = DummyRequest([self.pagename]) request.method = b'GET' request.addArg('lang', 'ru') page = self.howtoResource.render_GET(request) self.assertSubstring("следуйте инструкциям установщика", page)
def test_render_GET_RTLlang(self): """Test rendering a request for plain bridges in Arabic.""" if 'ar' not in _langs.get_langs(): self.skipTest("'ar' language unsupported") self.useBenignBridges() request = DummyRequest([b"bridges?transport=obfs3"]) request.method = b'GET' request.getClientIP = lambda: '3.3.3.3' # For some strange reason, the 'Accept-Language' value *should not* be # a list, unlike all the other headers and args… request.headers.update({'accept-language': 'ar,en,en_US,'}) page = self.bridgesResource.render(request) self.assertSubstring(b"rtl.css", page) self.assertSubstring( # "I need an alternative way to get bridges!" "أحتاج إلى وسيلة بديلة للحصول على bridges".encode("utf-8"), page) for bridgeLine in self.parseBridgesFromHTMLPage(page): # Check that each bridge line had the expected number of fields: bridgeLine = bridgeLine.split(' ') self.assertEqual(len(bridgeLine), 2)
def test_renderAnswer_textplain(self): """If the request format specifies 'plain', we should return content with mimetype 'text/plain'. """ self.useBenignBridges() request = DummyRequest([self.pagename]) request.args.update({'format': ['plain']}) request.getClientIP = lambda: '4.4.4.4' request.method = b'GET' page = self.bridgesResource.render(request) self.assertTrue("html" not in str(page)) # We just need to strip and split it because it looks like: # # 94.235.85.233:9492 0d9d0547c3471cddc473f7288a6abfb54562dc06 # 255.225.204.145:9511 1fb89d618b3a12afe3529fd072127ea08fb50466 # # (Yes, there are two leading spaces at the beginning of each line) # bridgeLines = [line.strip() for line in page.strip().split(b'\n')] for bridgeLine in bridgeLines: bridgeLine = bridgeLine.split(b' ') self.assertEqual(len(bridgeLine), 2) # Check that the IP and port seem okay: ip, port = bridgeLine[0].rsplit(b':') self.assertIsInstance(ipaddress.ip_address(ip.decode("utf-8")), ipaddress.IPv4Address) self.assertIsInstance(int(port), int) self.assertGreater(int(port), 0) self.assertLessEqual(int(port), 65535)
def test_getCaptchaImage(self): request = DummyRequest([self.pagename]) request.method = b'GET' image, challenge = self.resource.getCaptchaImage(request) self.assertIsNotNone(image) self.assertIsNotNone(challenge)
def create_POST_with_data(self, data): request = DummyRequest([self.pagename]) request.requestHeaders.addRawHeader('Content-Type', 'application/vnd.api+json') request.requestHeaders.addRawHeader('Accept', 'application/vnd.api+json') request.method = b'POST' request.writeContent(data) return request
def test_render_POST(self): """render_POST() with a wrong 'captcha_response_field' should return a redirect to the CaptchaProtectedResource page. """ request = DummyRequest([self.pagename]) request.method = b'POST' page = self.captchaResource.render_POST(request) self.assertEqual(BeautifulSoup(page).find('meta')['http-equiv'], 'refresh')
def test_render_GET_noCaptcha(self): """render_GET() should return a page without a CAPTCHA, which has the image alt text. """ request = DummyRequest([self.pagename]) request.method = b'GET' page = self.captchaResource.render_GET(request) self.assertSubstring( "Your browser is not displaying images properly", page)
def test_render_POST(self): """render_POST() with a wrong 'captcha_response_field' should return a redirect to the CaptchaProtectedResource page. """ request = DummyRequest([self.pagename]) request.method = b'POST' page = self.captchaResource.render_POST(request) self.assertEqual( BeautifulSoup(page).find('meta')['http-equiv'], 'refresh')
def test_render_GET_noCaptcha(self): """render_GET() should return a page without a CAPTCHA, which has the image alt text. """ request = DummyRequest([self.pagename]) request.method = b'GET' page = self.captchaResource.render_GET(request) self.assertSubstring(b"Your browser is not displaying images properly", page)
def test_HowtoResource_render_GET_lang_ru(self): """renderGet() with ?lang=ru should return the howto page in Russian.""" if 'ru' not in _langs.get_langs(): self.skipTest("'ru' language unsupported") request = DummyRequest([self.pagename]) request.method = b'GET' request.addArg(b'lang', 'ru') page = self.howtoResource.render_GET(request) self.assertSubstring("Как использовать мосты".encode("utf-8"), page)
def createRequestWithIPs(self): """Set the IP address returned from ``request.getClientIP()`` to '3.3.3.3', and the IP address reported in the 'X-Forwarded-For' header to '2.2.2.2'. """ request = DummyRequest(['']) request.headers.update({'x-forwarded-for': '2.2.2.2'}) # See :api:`twisted.test.requesthelper.DummyRequest.getClientIP` request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) request.method = b'GET' return request
def test_IndexResource_render_GET_lang_ta(self): """renderGet() with ?lang=ta should return the index page in Tamil.""" if 'ta' not in _langs.get_langs(): self.skipTest("'ta' language unsupported") request = DummyRequest([self.pagename]) request.method = b'GET' request.addArg(b'lang', 'ta') page = self.indexResource.render_GET(request) self.assertSubstring("bridge-களை Tor Browser-உள்".encode("utf-8"), page)
def test_getChild(self): request = DummyRequest(['foo']) request.method = b'GET' response_resource = self.resource.getChild('/foo', request) self.assertTrue(response_resource) self.assertIsInstance(response_resource, server.JsonAPIErrorResource) response = response_resource.render(request) detail = json.loads(response)['errors'][0]['detail'] self.assertIn('does not implement GET http://dummy/', detail)
def test_getCaptchaImage_empty_captcha_dir(self): request = DummyRequest([self.pagename]) request.method = b'GET' captchaDirOrig = self.resource.captchaDir captchaDirNew = tempfile.mkdtemp() self.resource.captchaDir = captchaDirNew image, challenge = self.resource.getCaptchaImage(request) self.resource.captchaDir = captchaDirOrig shutil.rmtree(captchaDirNew) self.assertIsNone(image) self.assertIsNone(challenge)
def test_render_GET_RTLlang(self): """Test rendering a request for obfs3 bridges in Hebrew.""" request = DummyRequest(["bridges?transport=obfs3"]) request.method = b'GET' request.getClientIP = lambda: '3.3.3.3' request.headers.update({'accept-language': 'he'}) # We actually have to set the request args manually when using a # DummyRequest: request.args.update({'transport': ['obfs2']}) page = self.optionsResource.render(request) self.assertSubstring("rtl.css", page) self.assertSubstring("מהם גשרים?", page)
def test_render_GET_missingTemplate(self): """render_GET() with a missing template should raise an error and return the result of replaceErrorPage(). """ oldLookup = server.lookup try: server.lookup = None request = DummyRequest([self.pagename]) request.method = b'GET' page = self.captchaResource.render_GET(request) errorPage = server.replaceErrorPage(request, Exception('kablam')) self.assertEqual(page, errorPage) finally: server.lookup = oldLookup
def test_render_GET_malicious_newlines(self): """Test rendering a request when the some of the bridges returned have malicious (HTML, Javascript, etc., in their) PT arguments. """ self.useMaliciousBridges() request = DummyRequest([self.pagename]) request.method = b'GET' request.getClientIP = lambda: '1.1.1.1' page = self.bridgesResource.render(request) self.assertTrue( 'bad=Bridge 6.6.6.6:6666 0123456789abcdef0123456789abcdef01234567' in str(page), "Newlines in bridge lines should be removed.")
def test_render_GET_malicious_returnchar(self): """Test rendering a request when the some of the bridges returned have malicious (HTML, Javascript, etc., in their) PT arguments. """ self.useMaliciousBridges() request = DummyRequest([self.pagename]) request.method = b'GET' request.getClientIP = lambda: '1.1.1.1' page = self.bridgesResource.render(request) self.assertTrue( 'eww=Bridge 1.2.3.4:1234' in str(page), "Return characters in bridge lines should be removed.")
def test_checkRequestHeaders_accept_with_media_parameters(self): data = { 'data': [{ 'type': 'client-transports', 'version': server.MOAT_API_VERSION, 'supported': ['obfs4'], }] } encoded_data = json.dumps(data) request = DummyRequest([self.pagename]) request.requestHeaders.addRawHeader('Accept', 'application/vnd.api+json;mp3') request.method = b'POST' request.writeContent(encoded_data)
def createRequestWithIPs(self): """Set the IP address returned from ``request.getClientIP()`` to '3.3.3.3', and the IP address reported in the 'X-Forwarded-For' header to '2.2.2.2'. """ request = DummyRequest([self.pagename]) # Since we do not set ``request.getClientIP`` here like we do in some # of the other unittests, an exception would be raised here if # ``getBridgesForRequest()`` is unable to get the IP address from this # 'X-Forwarded-For' header (because ``ip`` would get set to ``None``). request.headers.update({'x-forwarded-for': '2.2.2.2'}) # See :api:`twisted.test.requesthelper.DummyRequest.getClientIP` request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) request.method = b'GET' return request
def test_render_GET_malicious_javascript(self): """Test rendering a request when the some of the bridges returned have malicious (HTML, Javascript, etc., in their) PT arguments. """ self.useMaliciousBridges() request = DummyRequest([self.pagename]) request.method = b'GET' request.getClientIP = lambda: '1.1.1.1' page = self.bridgesResource.render(request) self.assertTrue( "evil=<script>alert('fuuuu');</script>" in str(page), ("The characters &, <, >, ', and \" in bridge lines should be " "replaced with their corresponding HTML special characters."))
def test_replaceErrorPage(self): """``replaceErrorPage`` should return the error-500.html page.""" request = DummyRequest(['']) exc = Exception("Under Maintenance") errorPage = server.replaceErrorPage(request, exc) self.assertSubstring(b"Internal Error", errorPage) self.assertNotSubstring("Under Maintenance".encode("utf-8"), errorPage)
def test_formatDataForResponse(self): request = DummyRequest([self.pagename]) request.method = b'GET' data = {'data': { 'version': 'wow', 'dinosaurs': 'cool', 'triceratops': 'awesome', 'velociraptors': 'terrifying', }} rendered = self.resource.formatDataForResponse(data, request) self.assertTrue(rendered) self.assertTrue(request.responseHeaders.hasHeader('content-type')) self.assertTrue(request.responseHeaders.hasHeader('server')) self.assertEqual(request.responseHeaders.getRawHeaders('content-type'), ['application/vnd.api+json'])
def test_replaceErrorPage_matches_resource500(self): """``replaceErrorPage`` should return the error-500.html page.""" request = DummyRequest(['']) exc = Exception("Under Maintenance") errorPage = server.replaceErrorPage(request, exc) error500Page = server.resource500.render(request) self.assertEqual(errorPage, error500Page)
def randomClientRequestForNotBlockedIn(self, cc): httpRequest = DummyRequest(['']) httpRequest.args.update({'unblocked': [cc]}) bridgeRequest = self.randomClientRequest() bridgeRequest.withoutBlockInCountry(httpRequest) bridgeRequest.generateFilters() return bridgeRequest
def test_replaceErrorPage(self): """``replaceErrorPage`` should return the error-500.html page.""" request = DummyRequest(['']) exc = Exception("vegan gümmibären") errorPage = server.replaceErrorPage(request, exc) self.assertSubstring(b"Bad News Bears", errorPage) self.assertNotSubstring("vegan gümmibären".encode("utf-8"), errorPage)
def test_render_POST_unexpired_with_qrcode(self): request = DummyRequest([self.pagename]) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) request.requestHeaders.addRawHeader('Content-Type', 'application/vnd.api+json') request.requestHeaders.addRawHeader('Accept', 'application/vnd.api+json') request.requestHeaders.addRawHeader('X-Forwarded-For', '3.3.3.3') resource = server.CaptchaFetchResource(self.hmacKey, self.publicKey, self.secretKey, self.captchaDir, useForwardedHeader=False) image, challenge = resource.getCaptchaImage(request) data = { 'data': [{ 'id': '2', 'type': 'moat-solution', 'version': server.MOAT_API_VERSION, 'transport': 'obfs4', 'challenge': challenge, 'solution': self.solution, 'qrcode': 'true', }] } encoded_data = json.dumps(data) request = self.create_POST_with_data(encoded_data) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) request.requestHeaders.addRawHeader('X-Forwarded-For', '3.3.3.3') response = self.resource.render(request) decoded = json.loads(response) self.assertTrue(decoded) self.assertIsNotNone(decoded.get('data')) datas = decoded['data'] self.assertEqual(len(datas), 1) data = datas[0] self.assertIsNotNone(data['qrcode']) self.assertIsNotNone(data['bridges']) self.assertEqual(data['version'], server.MOAT_API_VERSION) self.assertEqual(data['type'], 'moat-bridges') self.assertEqual(data['id'], '3')
def test_getChild(self): """``CustomErrorHandlingResource.getChild`` should return a rendered copy of ``server.resource404``. """ request = DummyRequest(['']) resource = server.CustomErrorHandlingResource() self.assertEqual(server.resource404, resource.getChild('foobar', request))
def create_valid_POST_make_new_challenge(self): request = DummyRequest([self.pagename]) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) request.requestHeaders.addRawHeader('Content-Type', 'application/vnd.api+json') request.requestHeaders.addRawHeader('Accept', 'application/vnd.api+json') request.requestHeaders.addRawHeader('X-Forwarded-For', '3.3.3.3') resource = server.CaptchaFetchResource(self.hmacKey, self.publicKey, self.secretKey, self.captchaDir, useForwardedHeader=False) image, challenge = resource.getCaptchaImage(request) request = self.create_valid_POST_with_challenge(challenge) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) request.requestHeaders.addRawHeader('X-Forwarded-For', '3.3.3.3') return request
def test_renderAnswer_GET_textplain_malicious(self): """If the request format specifies 'plain', we should return content with mimetype 'text/plain' and ASCII control characters replaced. """ self.useMaliciousBridges() request = DummyRequest([self.pagename]) request.args.update({'format': ['plain']}) request.getClientIP = lambda: '4.4.4.4' request.method = b'GET' page = self.bridgesResource.render(request) self.assertTrue("html" not in str(page)) self.assertTrue( 'eww=Bridge 1.2.3.4:1234' in str(page), "Return characters in bridge lines should be removed.") self.assertTrue( 'bad=Bridge 6.6.6.6:6666' in str(page), "Newlines in bridge lines should be removed.")
def test_renderAnswer_textplain_error(self): """If we hit some error while returning bridge lines in text/plain format, then our custom plaintext error message (the hardcoded HTML in ``server.replaceErrorPage``) should be returned. """ self.useBenignBridges() request = DummyRequest([self.pagename]) request.args.update({'format': ['plain']}) request.getClientIP = lambda: '4.4.4.4' request.method = b'GET' # We'll cause a TypeError here due to calling '\n'.join(None) page = self.bridgesResource.renderAnswer(request, bridgeLines=None) # We don't want the fancy version: self.assertNotSubstring("Bad News Bears", page) self.assertSubstring("Sorry! Something went wrong with your request.", page)
def test_render_GET_XForwardedFor(self): """The client's IP address should be obtainable from the 'X-Forwarded-For' header in the request. """ self.useBenignBridges() self.bridgesResource.useForwardedHeader = True request = DummyRequest([self.pagename]) request.method = b'GET' # Since we do not set ``request.getClientIP`` here like we do in some # of the other unittests, an exception would be raised here if # ``getBridgesForRequest()`` is unable to get the IP address from this # 'X-Forwarded-For' header (because ``ip`` would get set to ``None``). request.headers.update({'x-forwarded-for': '2.2.2.2'}) page = self.bridgesResource.render(request) self.bridgesResource.useForwardedHeader = False # Reset it # The response should explain how to use the bridge lines: self.assertTrue("To enter bridges into Tor Browser" in str(page))
def test_render_GET_RTLlang(self): """Test rendering a request for plain bridges in Arabic.""" self.useBenignBridges() request = DummyRequest([b"bridges?transport=obfs3"]) request.method = b'GET' request.getClientIP = lambda: '3.3.3.3' # For some strange reason, the 'Accept-Language' value *should not* be # a list, unlike all the other headers and args… request.headers.update({'accept-language': 'ar,en,en_US,'}) page = self.bridgesResource.render(request) self.assertSubstring("rtl.css", page) self.assertSubstring( # "I need an alternative way to get bridges!" "أحتاج إلى وسيلة بديلة للحصول على bridges", page) for bridgeLine in self.parseBridgesFromHTMLPage(page): # Check that each bridge line had the expected number of fields: bridgeLine = bridgeLine.split(' ') self.assertEqual(len(bridgeLine), 2)
def do_render_for_method(self, method): request = DummyRequest([self.pagename]) request.method = method rendered = self.resource.render(request) self.assertTrue(rendered) self.assertTrue(request.responseHeaders.hasHeader('content-type')) self.assertTrue(request.responseHeaders.hasHeader('server')) self.assertEqual(request.responseHeaders.getRawHeaders('content-type'), ['application/vnd.api+json']) decoded = json.loads(rendered) self.assertTrue(decoded) self.assertIsNotNone(decoded.get('errors')) errors = decoded['errors'] self.assertEqual(len(errors), 1) error = errors[0] return error
def test_render_GET_vanilla(self): """Test rendering a request for normal, vanilla bridges.""" self.useBenignBridges() request = DummyRequest([self.pagename]) request.method = b'GET' request.getClientIP = lambda: '1.1.1.1' page = self.bridgesResource.render(request) # The response should explain how to use the bridge lines: self.assertTrue("To enter bridges into Tor Browser" in str(page)) for b in self.parseBridgesFromHTMLPage(page): # Check that each bridge line had the expected number of fields: fields = b.split(' ') self.assertEqual(len(fields), 2) # Check that the IP and port seem okay: ip, port = fields[0].rsplit(':') self.assertIsInstance(ipaddr.IPv4Address(ip), ipaddr.IPv4Address) self.assertIsInstance(int(port), int) self.assertGreater(int(port), 0) self.assertLessEqual(int(port), 65535)
def setUp(self): """Create a :class:`server.BridgesResource` and protect it with a :class:`ReCaptchaProtectedResource`. """ self.timeout = 10.0 # Can't take longer than that, right? # Set up our resources to fake a minimal HTTP(S) server: self.pagename = b'captcha.html' self.root = Resource() # (None, None) is the (distributor, scheduleInterval): self.protectedResource = server.BridgesResource(None, None) self.captchaResource = server.ReCaptchaProtectedResource( publicKey='23', secretKey='42', remoteIP='111.111.111.111', useForwardedHeader=True, protectedResource=self.protectedResource) self.root.putChild(self.pagename, self.captchaResource) # Set up the basic parts of our faked request: self.request = DummyRequest([self.pagename])