def start() -> None: """ Set up a new proxy object with an error handler, configuration that we read from argv[1], and the original user request from STDIN. """ try: configure_logging() logging.debug("Starting SecureDrop Proxy {}".format(version)) # path to config file must be at argv[1] if len(sys.argv) != 2: raise ValueError( "sd-proxy script not called with path to configuration file") # read config. `read_conf` will call `p.err_on_done` if there is a config # problem, and will return a Conf object on success. conf_path = sys.argv[1] # a fresh, new proxy object p = proxy.Proxy(conf_path=conf_path) # read user request from STDIN incoming_lines = [] for line in sys.stdin: incoming_lines.append(line) incoming = "\n".join(incoming_lines) main.__main__(incoming, p) except Exception as e: response = { "status": http.HTTPStatus.INTERNAL_SERVER_ERROR, "body": json.dumps({"error": str(e)}), } print(json.dumps(response)) sys.exit(1)
def test_custom_callbacks(self): """ Test the handlers in a real proxy request. """ conf = proxy.Conf() conf.host = "jsonplaceholder.typicode.com" conf.scheme = "https" conf.port = 443 req = proxy.Req() req.method = "GET" on_save_addition = "added by the on_save callback\n" on_done_addition = "added by the on_done callback\n" def on_save(self, fh, res): res.headers["Content-Type"] = "text/plain" res.body = on_save_addition def on_done(self): self.res.headers["Content-Type"] = "text/plain" self.res.body += on_done_addition p = proxy.Proxy(self.conf_path, req) # Patching for tests p.conf = conf p.on_done = types.MethodType(on_done, p) p.on_save = types.MethodType(on_save, p) p.proxy() self.assertEqual(p.res.body, "{}{}".format(on_save_addition, on_done_addition))
def test_json_response(self): test_input_json = """{ "method": "GET", "path_query": "/posts?userId=1" }""" req = proxy.Req() req.method = 'GET' req.path_query = '' req.headers = {'Accept': 'application/json'} # Use custom callbacks def on_save(res, fh, conf): pass def on_done(res): res = res.__dict__ self.assertEqual(res['status'], 200) self.p = proxy.Proxy(self.conf, req, on_save) self.p.on_done = on_done self.p.proxy() saved_stdout = sys.stdout try: out = StringIO() sys.stdout = out main.__main__(test_input_json, self.p) output = out.getvalue().strip() finally: sys.stdout = saved_stdout response = json.loads(output) for item in json.loads(response['body']): self.assertEqual(item['userId'], 1)
def test_config_has_valid_keys(self): p = proxy.Proxy("tests/files/valid-config.yaml") # Verify we have a valid Conf object self.assertEqual(p.conf.host, "jsonplaceholder.typicode.com") self.assertEqual(p.conf.port, 443) self.assertFalse(p.conf.dev) self.assertEqual(p.conf.scheme, "https") self.assertEqual(p.conf.target_vm, "compost")
def test_on_save_200_success(self): fh = tempfile.NamedTemporaryFile() p = proxy.Proxy(self.conf_path) p.on_save(fh, self.res) self.assertEqual(self.res.headers["Content-Type"], "application/json") self.assertEqual(self.res.headers["X-Origin-Content-Type"], "application/json") self.assertEqual(self.res.status, 200) self.assertIn("filename", self.res.body)
def test_version(self): req = proxy.Req() req.method = 'GET' req.path_query = '' req.headers = {'Accept': 'application/json'} p = proxy.Proxy() p.proxy() self.assertEqual(p.res.version, version.version)
def test_version(self): req = proxy.Req() req.method = "GET" req.path_query = "" req.headers = {"Accept": "application/json"} p = proxy.Proxy(self.conf_path) p.proxy() self.assertEqual(p.res.version, version.version)
def test_400_if_callback_not_set(self): req = proxy.Req() req.method = 'GET' req.path_query = '' req.headers = {'Accept': 'application/json'} p = proxy.Proxy() p.proxy() self.assertEqual(p.res.status, 400)
def test_config_file_when_yaml_is_invalid(self): def err_on_done(self): res = self.res.__dict__ assert res["status"] == 500 assert "YAML syntax error" in res["body"] assert res["headers"]["Content-Type"] == "application/json" sys.exit(1) p = proxy.Proxy(self.conf_path) p.err_on_done = types.MethodType(err_on_done, p) with self.assertRaises(SystemExit): p.read_conf("tests/files/invalid_yaml.yaml")
def test_internal_server_error(self): """ Test handling of "500 Internal Server Error" from the server. """ req = self.make_request(path_query="/crash") p = proxy.Proxy(self.conf_path, req) p.proxy() self.assertEqual(p.res.status, http.HTTPStatus.INTERNAL_SERVER_ERROR) self.assertIn("application/json", p.res.headers["Content-Type"]) body = json.loads(p.res.body) self.assertEqual(body["error"], http.HTTPStatus.INTERNAL_SERVER_ERROR.phrase.lower())
def test_bad_request(self): """ Test handling of "400 Bad Request" from the server. """ req = self.make_request(path_query="/bad") p = proxy.Proxy(self.conf_path, req) p.proxy() self.assertEqual(p.res.status, http.HTTPStatus.BAD_REQUEST) self.assertIn("application/json", p.res.headers["Content-Type"]) body = json.loads(p.res.body) self.assertEqual(body["error"], http.HTTPStatus.BAD_REQUEST.phrase.lower())
def test_proxy_basic_functionality(self): req = proxy.Req() req.method = 'GET' req.path_query = '' req.headers = {'Accept': 'application/json'} p = proxy.Proxy(self.conf, req, self.on_save) p.proxy() self.assertEqual(p.res.status, 200) self.assertEqual(p.res.body, json.dumps({'filename': self.fn})) self.assertEqual(p.res.headers['Content-Type'], 'application/json')
def test_config_file_does_not_exist(self): def err_on_done(self): res = self.res.__dict__ assert res["status"] == 500 assert "Configuration file does not exist" in res["body"] assert res["headers"]["Content-Type"] == "application/json" sys.exit(1) p = proxy.Proxy(self.conf_path) p.err_on_done = types.MethodType(err_on_done, p) with self.assertRaises(SystemExit): p.read_conf("not/a/real/path")
def test_proxy_400_no_handler(self): req = proxy.Req() req.method = 'GET' req.path_query = 'http://badpath.lol/path' req.headers = {'Accept': 'application/json'} p = proxy.Proxy(self.conf, req) p.proxy() self.assertEqual(p.res.status, 400) self.assertEqual(p.res.headers['Content-Type'], 'application/json') self.assertIn('Request callback is not set', p.res.body)
def test_proxy_produces_404(self): req = proxy.Req() req.method = "GET" req.path_query = "/notfound" req.headers = {"Accept": "application/json"} p = proxy.Proxy(self.conf_path, req) p.proxy() self.assertEqual(p.res.status, 404) self.assertEqual(p.res.headers["Content-Type"], "application/json")
def test_config_500_when_missing_target_vm(self): def err_on_done(self): res = self.res.__dict__ assert res["status"] == 500 assert "missing `target_vm` key" in res["body"] assert res["headers"]["Content-Type"] == "application/json" sys.exit(1) p = proxy.Proxy(self.conf_path) p.err_on_done = types.MethodType(err_on_done, p) with self.assertRaises(SystemExit): p.read_conf("tests/files/missing-target-vm.yaml")
def test_proxy_200_valid_path(self): req = proxy.Req() req.method = 'GET' req.path_query = '/posts/1' req.headers = {'Accept': 'application/json'} p = proxy.Proxy(self.conf, req, self.on_save) p.proxy() self.assertEqual(p.res.status, 200) self.assertIn('application/json', p.res.headers['Content-Type']) body = json.loads(p.res.body) self.assertEqual(body['userId'], 1)
def test_proxy_500_misconfiguration(self): req = proxy.Req() req.method = "GET" req.path_query = "/posts/1" req.headers = {"Accept": "application/json"} p = proxy.Proxy(self.conf_path, req) p.proxy() self.assertEqual(p.res.status, 500) self.assertEqual(p.res.headers["Content-Type"], "application/json") self.assertIn("Proxy error while generating URL to request", p.res.body)
def test_cannot_connect(self): """ Test for "502 Bad Gateway" when the server can't be reached. """ req = self.make_request() p = proxy.Proxy("tests/files/badgateway-config.yaml", req) p.proxy() self.assertEqual(p.res.status, http.HTTPStatus.BAD_GATEWAY) self.assertIn("application/json", p.res.headers["Content-Type"]) body = json.loads(p.res.body) self.assertEqual(body["error"], "could not connect to server")
def test_proxy_400_bad_path(self): req = proxy.Req() req.method = "GET" req.path_query = "http://badpath.lol/path" req.headers = {"Accept": "application/json"} p = proxy.Proxy(self.conf_path, req) p.proxy() self.assertEqual(p.res.status, 400) self.assertEqual(p.res.headers["Content-Type"], "application/json") self.assertIn("Path provided in request did not look valid", p.res.body)
def test_on_save_500_unhandled_error(self): fh = tempfile.NamedTemporaryFile() # Let's generate an error and ensure that an appropriate response # is sent back to the user with patch("subprocess.run", side_effect=IOError): p = proxy.Proxy(self.conf_path) p.on_save(fh, self.res) self.assertEqual(self.res.status, 500) self.assertEqual(self.res.headers["Content-Type"], "application/json") self.assertEqual(self.res.headers["X-Origin-Content-Type"], "application/json") self.assertIn("Unhandled error", self.res.body)
def test_default_callbacks(self): test_input = { "method": "GET", "path_query": "", } p = proxy.Proxy(self.conf_path, proxy.Req()) p.on_done = unittest.mock.MagicMock() p.on_save = unittest.mock.MagicMock() main.__main__(json.dumps(test_input), p) self.assertEqual(p.on_save.call_count, 1) self.assertEqual(p.on_done.call_count, 1)
def test_proxy_500_misconfiguration(self): req = proxy.Req() req.method = 'GET' req.path_query = '/posts/1' req.headers = {'Accept': 'application/json'} p = proxy.Proxy(self.conf, req, self.on_save) p.proxy() self.assertEqual(p.res.status, 500) self.assertEqual(p.res.headers['Content-Type'], 'application/json') self.assertIn('Proxy error while generating URL to request', p.res.body)
def test_input_headers(self): test_input = { "method": "GET", "path_query": "/posts?userId=1", "headers": {"X-Test-Header": "th"}, } def on_save(self, fh, res): pass p = proxy.Proxy(self.conf_path, proxy.Req()) main.__main__(json.dumps(test_input), p) self.assertEqual(p.req.headers, test_input["headers"])
def test_proxy_produces_404(self): req = proxy.Req() req.method = 'GET' req.path_query = '/notfound' req.headers = {'Accept': 'application/json'} p = proxy.Proxy(self.conf, req) p.on_save = self.on_save p.on_done = self.on_done p.proxy() self.assertEqual(p.res.status, 404) self.assertEqual(p.res.headers['Content-Type'], 'application/json')
def test_proxy_handles_query_params_gracefully(self): req = proxy.Req() req.method = 'GET' req.path_query = '/posts?userId=1' req.headers = {'Accept': 'application/json'} p = proxy.Proxy(self.conf, req, self.on_save) p.proxy() self.assertEqual(p.res.status, 200) self.assertIn('application/json', p.res.headers['Content-Type']) body = json.loads(p.res.body) for item in body: self.assertEqual(item['userId'], 1)
def test_proxy_200_valid_path(self): req = proxy.Req() req.method = "GET" req.path_query = "/posts/1" req.headers = {"Accept": "application/json"} p = proxy.Proxy(self.conf_path, req) p.proxy() self.assertEqual(p.res.status, 200) self.assertIn("application/json", p.res.headers["Content-Type"]) body = json.loads(p.res.body) self.assertEqual(body["userId"], 1)
def test_proxy_400_bad_path(self): req = proxy.Req() req.method = 'GET' req.path_query = 'http://badpath.lol/path' req.headers = {'Accept': 'application/json'} p = proxy.Proxy(self.conf, req) p.on_save = self.on_save p.on_done = self.on_done p.proxy() self.assertEqual(p.res.status, 400) self.assertEqual(p.res.headers['Content-Type'], 'application/json') self.assertIn('Path provided in request did not look valid', p.res.body)
def test_config_file_open_generic_exception(self): def err_on_done(self): res = self.res.__dict__ assert res["status"] == 500 assert res["headers"]["Content-Type"] == "application/json" sys.exit(1) p = proxy.Proxy(self.conf_path) p.err_on_done = types.MethodType(err_on_done, p) with self.assertRaises(SystemExit): # Patching open so that we can simulate a non-YAML error # (e.g. permissions) with patch("builtins.open", side_effect=IOError): p.read_conf("tests/files/valid-config.yaml")
def test_proxy_handles_query_params_gracefully(self): req = proxy.Req() req.method = "GET" req.path_query = "/posts?userId=1" req.headers = {"Accept": "application/json"} p = proxy.Proxy(self.conf_path, req) p.proxy() self.assertEqual(p.res.status, 200) self.assertIn("application/json", p.res.headers["Content-Type"]) body = json.loads(p.res.body) for item in body: self.assertEqual(item["userId"], 1)