Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,27 @@ has_arg(int argc, char **argv, const char *arg)
return false;
}

/**
* Builds a proxy URL from a scheme, optional credentials, host, and a port
* read from an environment variable (with a fallback default).
*/
static void
make_proxy_url(char *buf, size_t buf_size, const char *scheme,
const char *credentials, const char *host, const char *port_env_var,
const char *default_port)
{
const char *port = getenv(port_env_var);
if (!port)
port = default_port;

if (credentials) {
snprintf(
buf, buf_size, "%s://%s@%s:%s", scheme, credentials, host, port);
} else {
snprintf(buf, buf_size, "%s://%s:%s", scheme, host, port);
}
}

#if defined(SENTRY_PLATFORM_WINDOWS) && !defined(__MINGW32__) \
&& !defined(__MINGW64__)

Expand Down Expand Up @@ -515,21 +536,31 @@ main(int argc, char **argv)
}

if (has_arg(argc, argv, "http-proxy")) {
sentry_options_set_proxy(options, "http://127.0.0.1:8080");
char proxy_url[128];
make_proxy_url(proxy_url, sizeof(proxy_url), "http", NULL, "127.0.0.1",
"SENTRY_TEST_PROXY_PORT", "8080");
sentry_options_set_proxy(options, proxy_url);
}
if (has_arg(argc, argv, "http-proxy-auth")) {
sentry_options_set_proxy(
options, "http://user:password@127.0.0.1:8080");
char proxy_url[128];
make_proxy_url(proxy_url, sizeof(proxy_url), "http", "user:password",
"127.0.0.1", "SENTRY_TEST_PROXY_PORT", "8080");
sentry_options_set_proxy(options, proxy_url);
}
if (has_arg(argc, argv, "http-proxy-ipv6")) {
sentry_options_set_proxy(options, "http://[::1]:8080");
char proxy_url[128];
make_proxy_url(proxy_url, sizeof(proxy_url), "http", NULL, "[::1]",
"SENTRY_TEST_PROXY_PORT", "8080");
sentry_options_set_proxy(options, proxy_url);
}
if (has_arg(argc, argv, "proxy-empty")) {
sentry_options_set_proxy(options, "");
}

if (has_arg(argc, argv, "socks5-proxy")) {
sentry_options_set_proxy(options, "socks5://127.0.0.1:1080");
char proxy_url[128];
make_proxy_url(proxy_url, sizeof(proxy_url), "socks5", NULL,
"127.0.0.1", "SENTRY_TEST_PROXY_PORT", "1080");
sentry_options_set_proxy(options, proxy_url);
}

if (has_arg(argc, argv, "crashpad-wait-for-upload")) {
Expand Down
105 changes: 68 additions & 37 deletions tests/proxy.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import contextlib
import re
import os
import socket
import subprocess
Expand All @@ -8,48 +10,77 @@
from tests.assertions import assert_no_proxy_request


@contextlib.contextmanager
def closed_port():
"""Bind a port and hold it open without listening.
Connections are guaranteed to be refused, and no other process can claim it."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
yield s.getsockname()[1]


def setup_proxy_env_vars(port):
os.environ["http_proxy"] = f"http://localhost:{port}"
os.environ["https_proxy"] = f"http://localhost:{port}"
os.environ["http_proxy"] = f"http://127.0.0.1:{port}"
os.environ["https_proxy"] = f"http://127.0.0.1:{port}"


def cleanup_proxy_env_vars():
del os.environ["http_proxy"]
del os.environ["https_proxy"]
os.environ.pop("http_proxy", None)
os.environ.pop("https_proxy", None)


def _parse_listening_port(proxy_process, timeout=10):
"""Read the 'Proxy server listening at ...' line from mitmdump stdout
and extract the port. This consumes only the first line; subsequent
stdout (request logs) remains available via proxy_process.stdout."""
deadline = time.monotonic() + timeout
buf = b""
while time.monotonic() < deadline:
ch = proxy_process.stdout.read(1)
if not ch:
break
buf += ch
if ch == b"\n":
match = re.search(rb"listening at .+:(\d+)", buf)
if match:
return int(match.group(1))
buf = b""
if proxy_process.poll() is not None:
break
pytest.fail(
f"mitmdump did not report a listening port within {timeout}s. Output so far: {buf!r}"
)


def start_mitmdump(proxy_type, proxy_auth: str = None, listen_host: str = "127.0.0.1"):
"""Start mitmdump with OS-assigned port (--listen-port 0). Returns (process, port)."""
proxy_command = [
"mitmdump",
"--set",
f"listen_host={listen_host}",
"--listen-port",
"0",
]

if proxy_type == "socks5-proxy":
proxy_command += ["--mode", "socks5"]

if proxy_auth:
proxy_command += ["-v", "--proxyauth", proxy_auth]

# mitmdump (also written in python) often buffers output long enough so that we don't catch it
env = {**os.environ, "PYTHONUNBUFFERED": "1"}
proxy_process = subprocess.Popen(
proxy_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env
)

def is_proxy_running(host, port):
try:
with socket.create_connection((host, port), timeout=1):
return True
except ConnectionRefusedError:
return False


def start_mitmdump(proxy_type, proxy_auth: str = None):
# start mitmdump from terminal
proxy_process = None
if proxy_type == "http-proxy":
proxy_command = ["mitmdump"]
if proxy_auth:
proxy_command += ["-v", "--proxyauth", proxy_auth]
proxy_process = subprocess.Popen(
proxy_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
time.sleep(5) # Give mitmdump some time to start
if not is_proxy_running("localhost", 8080):
pytest.fail("mitmdump (HTTP) did not start correctly")
elif proxy_type == "socks5-proxy":
proxy_command = ["mitmdump", "--mode", "socks5"]
if proxy_auth:
proxy_command += ["-v", "--proxyauth", proxy_auth]
proxy_process = subprocess.Popen(
proxy_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
time.sleep(5) # Give mitmdump some time to start
if not is_proxy_running("localhost", 1080):
pytest.fail("mitmdump (SOCKS5) did not start correctly")
return proxy_process
port = _parse_listening_port(proxy_process)
except Exception:
proxy_process.terminate()
proxy_process.wait()
raise
return proxy_process, port


def proxy_test_finally(
Expand All @@ -66,8 +97,8 @@ def proxy_test_finally(
# Give mitmdump some time to get a response from the mock server
time.sleep(0.5)
proxy_process.terminate()
proxy_process.wait()
stdout, stderr = proxy_process.communicate()
stdout_bytes, _ = proxy_process.communicate()
stdout = stdout_bytes.decode("utf-8", errors="replace")
if expected_proxy_logsize == 0:
# don't expect any incoming requests to make it through the proxy
proxy_log_assert(stdout)
Expand Down
38 changes: 26 additions & 12 deletions tests/test_integration_crashpad.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,26 +60,33 @@ def test_crashpad_capture(cmake, httpserver):


def _setup_crashpad_proxy_test(cmake, httpserver, proxy):
proxy_process = start_mitmdump(proxy) if proxy else None
if proxy:
proxy_process, port = start_mitmdump(proxy)
else:
proxy_process, port = None, None

tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"})

env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver, proxy_host=True))
if port is not None:
env["SENTRY_TEST_PROXY_PORT"] = str(port)
httpserver.expect_oneshot_request("/api/123456/minidump/").respond_with_data("OK")

return env, proxy_process, tmp_path
return env, proxy_process, tmp_path, port


def test_crashpad_crash_proxy_env(cmake, httpserver):
if not shutil.which("mitmdump"):
pytest.skip("mitmdump is not installed")

proxy_process = None # store the proxy process to terminate it later
setup_proxy_env_vars(port=8080)
try:
env, proxy_process, tmp_path = _setup_crashpad_proxy_test(
env, proxy_process, tmp_path, port = _setup_crashpad_proxy_test(
cmake, httpserver, "http-proxy"
)
setup_proxy_env_vars(port=port)
env["http_proxy"] = f"http://127.0.0.1:{port}"
env["https_proxy"] = f"http://127.0.0.1:{port}"

with httpserver.wait(timeout=10) as waiting:
run(
Expand All @@ -100,11 +107,13 @@ def test_crashpad_crash_proxy_env_port_incorrect(cmake, httpserver):
pytest.skip("mitmdump is not installed")

proxy_process = None # store the proxy process to terminate it later
setup_proxy_env_vars(port=8081)
try:
env, proxy_process, tmp_path = _setup_crashpad_proxy_test(
env, proxy_process, tmp_path, port = _setup_crashpad_proxy_test(
cmake, httpserver, "http-proxy"
)
setup_proxy_env_vars(port=port + 1)
env["http_proxy"] = f"http://127.0.0.1:{port + 1}"
env["https_proxy"] = f"http://127.0.0.1:{port + 1}"

with pytest.raises(AssertionError):
with httpserver.wait(timeout=10):
Expand All @@ -125,11 +134,15 @@ def test_crashpad_proxy_set_empty(cmake, httpserver):
pytest.skip("mitmdump is not installed")

proxy_process = None # store the proxy process to terminate it later
setup_proxy_env_vars(port=8080) # we start the proxy but expect it to remain unused
try:
env, proxy_process, tmp_path = _setup_crashpad_proxy_test(
env, proxy_process, tmp_path, port = _setup_crashpad_proxy_test(
cmake, httpserver, "http-proxy"
)
setup_proxy_env_vars(
port=port
) # we start the proxy but expect it to remain unused
env["http_proxy"] = f"http://127.0.0.1:{port}"
env["https_proxy"] = f"http://127.0.0.1:{port}"

with httpserver.wait(timeout=10) as waiting:
run(
Expand All @@ -152,11 +165,12 @@ def test_crashpad_proxy_https_not_http(cmake, httpserver):

proxy_process = None # store the proxy process to terminate it later
# we start the proxy but expect it to remain unused (dsn is http, so shouldn't use https proxy)
os.environ["https_proxy"] = f"http://localhost:8080"
try:
env, proxy_process, tmp_path = _setup_crashpad_proxy_test(
env, proxy_process, tmp_path, port = _setup_crashpad_proxy_test(
cmake, httpserver, "http-proxy"
)
os.environ["https_proxy"] = f"http://127.0.0.1:{port}"
env["https_proxy"] = f"http://127.0.0.1:{port}"

with httpserver.wait(timeout=10) as waiting:
run(
Expand All @@ -169,7 +183,7 @@ def test_crashpad_proxy_https_not_http(cmake, httpserver):
assert waiting.result

finally:
del os.environ["https_proxy"]
os.environ.pop("https_proxy", None)
proxy_test_finally(1, httpserver, proxy_process, expected_proxy_logsize=0)


Expand All @@ -196,7 +210,7 @@ def test_crashpad_crash_proxy(cmake, httpserver, run_args, proxy_running):

try:
proxy_to_start = run_args[0] if proxy_running else None
env, proxy_process, tmp_path = _setup_crashpad_proxy_test(
env, proxy_process, tmp_path, port = _setup_crashpad_proxy_test(
cmake, httpserver, proxy_to_start
)

Expand Down
Loading
Loading