Skip to main content

Test proxy credential encoding offline

Updated 5 min readMarkdown

Prepared with AI assistance for Portproof, which sells proxy traffic and mobile-port rentals. Every credential in this article is a fixed synthetic fixture. The checks run offline; they do not authenticate to a gateway or measure a service.

A configuration migration can change a password without showing an obvious typo. One client accepts separate authentication fields; another expects credentials inside a URL. Reusing the same string in both places can turn an encoded component into the literal password sent by the second client.

Test that representation boundary before involving a network. The useful question is whether the original credential survives serialization and parsing, rather than whether a permissive parser accepts the URL.

Choose one input contract

The helper below accepts original text values. It does not accept a partly encoded component or a completed proxy URL. A literal %40 in an original password is three characters, so its percent sign must be encoded. It is not permission to substitute an @.

Python's URL quoting documentation distinguishes component quoting from form-query quoting. Here, quote uses an empty safe set and UTF-8. Slashes are encoded, spaces become %20, and a plus sign stays distinguishable from a space. The test reads the serialized components back and explicitly decodes them for comparison with the inputs.

The example fixes the gateway to proxy.example.invalid:8080 so the test cannot accidentally use a working service. It rejects control characters, non-text values and a missing username as its own input policy. Leading and trailing spaces remain data. This policy is illustrative: a real credential issuer and client may have different supported inputs.

Fixture Boundary being tested Expected result
Plain ASCII Baseline representation Exact round trip
Separators in both fields URI punctuation stays credential data Exact round trip
Literal %40 Existing percent text is not decoded first %2540 in the component
Space and plus Form encoding is not substituted %20 and %2B stay distinct
Non-ASCII text Encoding has an explicit byte basis UTF-8 round trip
Leading/trailing space Copy handling does not silently trim Space preserved
Newline Control input is rejected by this helper Defined error
Missing username Incomplete input is rejected Defined error
Non-text password No implicit conversion of None Defined error
Double-encoding control Encoding an encoded component changes the value Mismatch detected

Run the complete fixture

Save this as fixture.py and run python3 fixture.py. It uses Python 3.9 or newer and the standard library. The output contains case labels and pass states, not assembled credential-bearing URLs. Assertions intentionally fail the process if a case changes.

"""Offline credential serialization fixtures. All values are synthetic."""
from urllib.parse import quote, unquote, urlsplit
import json
import platform

HOST = "proxy.example.invalid"
PORT = 8080


def proxy_url(raw_user, raw_password):
    for value in (raw_user, raw_password):
        if not isinstance(value, str):
            raise ValueError("credential_not_text")
        if any(ord(char) < 32 or ord(char) == 127 for char in value):
            raise ValueError("credential_contains_control")
    if not raw_user:
        raise ValueError("username_missing")
    user = quote(raw_user, safe="", encoding="utf-8", errors="strict")
    password = quote(raw_password, safe="", encoding="utf-8", errors="strict")
    return f"http://{user}:{password}@{HOST}:{PORT}"


def check_case(label, raw_user, raw_password, expected_password):
    assembled = proxy_url(raw_user, raw_password)
    parts = urlsplit(assembled)
    assert parts.scheme == "http", label
    assert parts.hostname == HOST and parts.port == PORT, label
    assert unquote(parts.username, errors="strict") == raw_user, label
    assert unquote(parts.password, errors="strict") == raw_password, label
    encoded_password = assembled.split("@", 1)[0].split(":", 2)[2]
    assert encoded_password == expected_password, label
    return {"case": label, "result": "pass"}


def expect_rejected(label, user, password, expected_error):
    try:
        proxy_url(user, password)
    except ValueError as error:
        assert str(error) == expected_error, label
        return {"case": label, "result": "pass"}
    raise AssertionError(label)


def main():
    accepted = [
        ("ascii", "demo", "plain", "plain"),
        ("separators", "user:name", "p@ss/#:?", "p%40ss%2F%23%3A%3F"),
        ("literal_percent", "demo", "literal%40", "literal%2540"),
        ("space_and_plus", "demo", "two words+", "two%20words%2B"),
        ("unicode", "d\u00e9mo", "caf\u00e9", "caf%C3%A9"),
        ("preserved_whitespace", "demo", " pass ", "%20pass%20"),
    ]
    results = [check_case(*case) for case in accepted]
    results += [
        expect_rejected("newline", "demo", "line\nbreak", "credential_contains_control"),
        expect_rejected("missing_user", "", "demo", "username_missing"),
        expect_rejected("non_text", "demo", None, "credential_not_text"),
    ]
    # A negative control proves why re-encoding an assembled component is wrong.
    once = quote("p@ss", safe="")
    twice = quote(once, safe="")
    assert unquote(twice) != "p@ss"
    results.append({"case": "double_encoding_detected", "result": "pass"})
    print(json.dumps({
        "python": platform.python_version(),
        "scope": "offline synthetic serialization only; no network request",
        "checks": results,
        "passed": len(results),
    }, indent=2))


if __name__ == "__main__":
    main()

What the passing result establishes

The recorded run on 30 October 2026 passed ten checks. Six accepted inputs returned their original username and password after one serialization/parse/decode cycle. Three rejected inputs produced the expected error. The negative control demonstrated that encoding the already encoded p%40ss again does not preserve the original p@ss after one decode.

This is deliberately narrower than an integration test. urlsplit separates fields; it does not prove a particular application supports embedded proxy authentication. curl documents URL-decoding credentials in a proxy string, while Playwright documents separate proxy username and password fields. Use each client's contract and test the actual configured client separately with a bounded authorised request.

Keep production inputs separate from the fixed test vector list. Do not adapt this fixture into a command that prints real URLs, commits credentials or uploads them to a conversion website. RFC3986's userinfo discussion also explains why sensitive credential-bearing URIs need care when displayed or stored.

For the gateway/protocol decision and a comparison of raw fields with URI components, use Portproof's proxy URL format guide. The proxy traffic catalogue describes the actual product; this fixture neither buys traffic nor establishes gateway capacity. References were checked on 30 October 2026.

No comments yet. Be the first to comment.

More from this blog