diff --git a/playbooks/verify.yml b/playbooks/verify.yml index a683e77..4609e86 100644 --- a/playbooks/verify.yml +++ b/playbooks/verify.yml @@ -256,7 +256,7 @@ - --proxy - "https://{{ megaproxy_services.https.endpoint }}:{{ megaproxy_services.https.port }}" - --proxy-user - - "{{ megaproxy_services.https.users[0].name }}:{{ megaproxy_services.https.users[0].password }}" + - "{{ megaproxy_services.https.users[0].name | urlencode }}:{{ megaproxy_services.https.users[0].password | urlencode }}" - "https://example.com/" register: authenticated_connect changed_when: false @@ -269,9 +269,7 @@ - name: Require successful authenticated HTTPS CONNECT ansible.builtin.assert: that: - - >- - authenticated_connect.rc == 0 or - 'CONNECT tunnel established, response 200' in authenticated_connect.stderr + - authenticated_connect.rc == 0 fail_msg: >- Authenticated CONNECT was not established. The curl output is hidden because it may contain proxy credentials; inspect the GOST service journal for authentication errors. @@ -286,11 +284,12 @@ - --fail - --max-time - "30" + - --proxytunnel - --proxy-insecure - --proxy - "https://{{ item.hostname }}:{{ megaproxy_services.https.port }}" - --proxy-user - - "{{ megaproxy_services.https.users[0].name }}:{{ megaproxy_services.https.users[0].password }}" + - "{{ megaproxy_services.https.users[0].name | urlencode }}:{{ megaproxy_services.https.users[0].password | urlencode }}" - http://api.ipify.org/ loop: "{{ megaproxy_services.get('https', {}).get('routes', []) | selectattr('chain') | list }}" register: chain_entry_ips @@ -308,11 +307,12 @@ - --fail - --max-time - "30" + - --proxytunnel - --proxy-insecure - --proxy - "https://{{ item.chain.host }}:{{ item.chain.port }}" - --proxy-user - - "{{ megaproxy_services.https.users[0].name }}:{{ megaproxy_services.https.users[0].password }}" + - "{{ megaproxy_services.https.users[0].name | urlencode }}:{{ megaproxy_services.https.users[0].password | urlencode }}" - http://api.ipify.org/ loop: "{{ megaproxy_services.get('https', {}).get('routes', []) | selectattr('chain') | list }}" register: chain_exit_ips diff --git a/roles/https_proxy/templates/megaproxy-certbot-renew.service.j2 b/roles/https_proxy/templates/megaproxy-certbot-renew.service.j2 index 804460c..48d64f1 100644 --- a/roles/https_proxy/templates/megaproxy-certbot-renew.service.j2 +++ b/roles/https_proxy/templates/megaproxy-certbot-renew.service.j2 @@ -5,5 +5,5 @@ Requires=docker.service [Service] Type=oneshot -ExecStart=/usr/bin/docker run --rm --network host -v /etc/letsencrypt:/etc/letsencrypt -v /var/lib/letsencrypt:/var/lib/letsencrypt certbot/certbot:{{ megaproxy_services.https.certbot_version }} renew --quiet +ExecStart=/usr/bin/docker run --rm --network host -v /etc/letsencrypt:/etc/letsencrypt -v /var/lib/letsencrypt:/var/lib/letsencrypt certbot/certbot:{{ megaproxy_services.https.certbot_version }} renew --cert-name {{ megaproxy_services.https.endpoint }} --quiet ExecStartPost=/usr/bin/systemctl try-restart megaproxy-gost.service diff --git a/tests/test_https_proxy_role.py b/tests/test_https_proxy_role.py index a90e349..fd320a0 100644 --- a/tests/test_https_proxy_role.py +++ b/tests/test_https_proxy_role.py @@ -119,3 +119,47 @@ def test_docker_cleanup_runs_after_docker_service_changes() -> None: assert "cleanup_old_images gogost/gost" in cleanup assert "cleanup_old_images certbot/certbot" in cleanup assert "megaproxy_services.https.certbot_version" in cleanup + + +def test_verify_curl_preserves_percent_encoded_credentials(): + import base64 + from http.server import BaseHTTPRequestHandler, HTTPServer + import subprocess as sp + from threading import Thread + + tasks = YAML(typ="safe").load((ROOT / "playbooks/verify.yml").read_text())[0]["tasks"] + probes = [task["ansible.builtin.command"]["argv"] for task in tasks + if isinstance(task.get("ansible.builtin.command"), dict) + and isinstance(task["ansible.builtin.command"].get("argv"), list) + and "--proxy-user" in task["ansible.builtin.command"]["argv"]] + assert len(probes) == 3 + assert all("--proxytunnel" in probe for probe in probes if "http://api.ipify.org/" in probe) + user = {"name": "test%2Fuser", "password": "test%C2:pass%40word"} + variables = {"megaproxy_services": {"https": {"users": [user]}}} + env = Environment(undefined=StrictUndefined) + credentials = {env.from_string(probe[probe.index("--proxy-user") + 1]).render(**variables) + for probe in probes} + assert len(credentials) == 1 + received = [] + + class Proxy(BaseHTTPRequestHandler): + def do_GET(self): + received.append(self.headers.get("Proxy-Authorization")) + self.send_response(200) + self.end_headers() + + def log_message(self, *args): + pass + + with HTTPServer(("127.0.0.1", 0), Proxy) as proxy: + proxy.timeout = 5 + worker = Thread(target=proxy.handle_request, daemon=True) + worker.start() + result = sp.run(["curl", "--silent", "--show-error", "--fail", "--max-time", "5", + "--noproxy", "", "--proxy", f"http://127.0.0.1:{proxy.server_port}", + "--proxy-user", credentials.pop(), "http://example.invalid/"], + capture_output=True, text=True, timeout=10) + worker.join(5) + assert result.returncode == 0, result.stderr + expected = base64.b64encode(f'{user["name"]}:{user["password"]}'.encode()).decode() + assert received == [f"Basic {expected}"]