blob: ee8463de3f47992ce8905da6d98b6fa1f54d1572 [file] [log] [blame] [edit]
"""Proxy tests.
Tests do modify `os.environ` global states. Each test must be run in separate
process. Must use `pytest --forked` or similar technique.
"""
import ipaddress
import os
import pytest
import random
import socket
import socks
from unittest import mock
import urllib
import httplib2
import tests
from tests import socks5
def _raise_name_not_known_error(*args, **kwargs):
raise socket.gaierror(socket.EAI_NONAME, "Name or service not known")
@pytest.mark.parametrize(
"url,kind,host,port,user,password",
[
# ("noscheme.means.http", socks.PROXY_TYPE_HTTP, "noscheme.means.http", 80, None, None),
("http://myproxy.example.com", socks.PROXY_TYPE_HTTP, "myproxy.example.com", 80, None, None),
("http://zoidberg:fish@someproxy:99", socks.PROXY_TYPE_HTTP, "someproxy", 99, "zoidberg", "fish"),
("http://[email protected]:1032", socks.PROXY_TYPE_HTTP, "fro.xy", 1032, "leila", None),
("http://[::1]:8888", socks.PROXY_TYPE_HTTP, "::1", 8888, None, None),
("socks4://myproxy.example.com", socks.PROXY_TYPE_SOCKS4, "myproxy.example.com", 80, None, None),
("socks4://zoidberg:fish@someproxy:99", socks.PROXY_TYPE_SOCKS4, "someproxy", 99, "zoidberg", "fish"),
("socks4://[email protected]:1032", socks.PROXY_TYPE_SOCKS4, "fro.xy", 1032, "leila", None),
("socks4://[::1]:8888", socks.PROXY_TYPE_SOCKS4, "::1", 8888, None, None),
("socks5://myproxy.example.com", socks.PROXY_TYPE_SOCKS5, "myproxy.example.com", 80, None, None),
("socks5://zoidberg:fish@someproxy:99", socks.PROXY_TYPE_SOCKS5, "someproxy", 99, "zoidberg", "fish"),
("socks5://[email protected]:1032", socks.PROXY_TYPE_SOCKS5, "fro.xy", 1032, "leila", None),
("socks5://[::1]:8888", socks.PROXY_TYPE_SOCKS5, "::1", 8888, None, None),
],
)
def test_from_url(url, kind, host, port, user, password):
pi = httplib2.proxy_info_from_url(url)
assert pi.proxy_type == kind, f"proxy_type expected={kind} invalid={pi.proxy_type}"
assert pi.proxy_host == host, f"proxy_host expected={host} invalid={pi.proxy_host}"
assert pi.proxy_port == port, f"proxy_port expected={port} invalid={pi.proxy_port}"
assert pi.proxy_user == user, f"proxy_user expected={user} invalid={pi.proxy_user}"
assert pi.proxy_pass == password, f"proxy_pass expected={password} invalid={pi.proxy_pass}"
@pytest.mark.forked
def test_from_env(monkeypatch):
assert os.environ.get("http_proxy") is None
monkeypatch.setenv("http_proxy", "http://myproxy.example.com:8080")
pi = httplib2.proxy_info_from_environment()
assert pi.proxy_host == "myproxy.example.com"
assert pi.proxy_port == 8080
@pytest.mark.forked
def test_from_env_https(monkeypatch):
assert os.environ.get("http_proxy") is None
monkeypatch.setenv("http_proxy", "http://myproxy.example.com:80")
monkeypatch.setenv("https_proxy", "http://myproxy.example.com:81")
pi = httplib2.proxy_info_from_environment("https")
assert pi.proxy_host == "myproxy.example.com"
assert pi.proxy_port == 81
@pytest.mark.forked
def test_from_env_none():
os.environ.clear()
pi = httplib2.proxy_info_from_environment()
assert pi is None
def test_from_env_other():
pi = httplib2.proxy_info_from_environment("foobar")
assert pi is None
def test_proxy_info_repr():
pi = httplib2.ProxyInfo(3, "pseudorandom", 8123, proxy_pass="secret")
r = repr(pi)
assert "pseudorandom" in r
assert "8123" in r
assert "secret" not in r
@pytest.mark.forked
def test_applies_to(monkeypatch):
monkeypatch.setenv("http_proxy", "http://myproxy.example.com:80")
monkeypatch.setenv("https_proxy", "http://myproxy.example.com:81")
monkeypatch.setenv("no_proxy", "localhost,example.com,.wildcard")
pi = httplib2.proxy_info_from_environment()
assert not pi.applies_to("localhost")
assert pi.applies_to("www.google.com")
assert pi.applies_to("prefixlocalhost")
assert pi.applies_to("www.example.com")
assert pi.applies_to("sub.example.com")
assert not pi.applies_to("sub.wildcard")
assert not pi.applies_to("pub.sub.wildcard")
@pytest.mark.forked
def test_noproxy_trailing_comma(monkeypatch):
monkeypatch.setenv("http_proxy", "http://myproxy.example.com:80")
monkeypatch.setenv("no_proxy", "localhost,other.host,")
pi = httplib2.proxy_info_from_environment()
assert not pi.applies_to("localhost")
assert not pi.applies_to("other.host")
assert pi.applies_to("example.domain")
@pytest.mark.forked
def test_noproxy_star(monkeypatch):
monkeypatch.setenv("http_proxy", "http://myproxy.example.com:80")
monkeypatch.setenv("NO_PROXY", "*")
pi = httplib2.proxy_info_from_environment()
for host in ("localhost", "169.254.38.192", "www.google.com"):
assert not pi.applies_to(host)
def test_headers():
headers = {"key0": "val0", "key1": "val1"}
pi = httplib2.ProxyInfo(httplib2.socks.PROXY_TYPE_HTTP, "localhost", 1234, proxy_headers=headers)
assert pi.proxy_headers == headers
@mock.patch("socket.socket.connect", spec=True)
def test_server_not_found_error_is_raised_for_invalid_hostname(mock_socket_connect):
"""Invalidates https://github.com/httplib2/httplib2/pull/100."""
mock_socket_connect.side_effect = _raise_name_not_known_error
http = httplib2.Http(proxy_info=httplib2.ProxyInfo(httplib2.socks.PROXY_TYPE_HTTP, "255.255.255.255", 8001))
try:
http.request("http://invalid.hostname.foo.bar/", "GET")
except httplib2.ServerNotFoundError:
pass
except Exception as e:
assert "name or service not known" in str(e).lower()
def test_auth_str_bytes():
# https://github.com/httplib2/httplib2/pull/115
# Proxy-Authorization b64encode() TypeError: a bytes-like object is required, not 'str'
with tests.server_const_http(request_count=2) as uri:
uri_parsed = urllib.parse.urlparse(uri)
http = httplib2.Http(
proxy_info=httplib2.ProxyInfo(
httplib2.socks.PROXY_TYPE_HTTP,
proxy_host=uri_parsed.hostname,
proxy_port=uri_parsed.port,
proxy_rdns=True,
proxy_user="user_str",
proxy_pass="pass_str",
)
)
response, _ = http.request(uri, "GET")
assert response.status == 200
with tests.server_const_http(request_count=2) as uri:
uri_parsed = urllib.parse.urlparse(uri)
http = httplib2.Http(
proxy_info=httplib2.ProxyInfo(
httplib2.socks.PROXY_TYPE_HTTP,
proxy_host=uri_parsed.hostname,
proxy_port=uri_parsed.port,
proxy_rdns=True,
proxy_user=b"user_bytes",
proxy_pass=b"pass_bytes",
)
)
response, _ = http.request(uri, "GET")
assert response.status == 200
def test_socks5_auth():
def proxy_conn(client, tick):
data = client.recv(64)
assert data == bytes(
[socks5.VERSION, 0x02, socks5.AUTH_NO_AUTHENTICATION_REQUIRED, socks5.AUTH_USERNAME_PASSWORD]
)
client.send(socks5.SERVER_GREETING_USER_PASS)
data = client.recv(64)
assert data == b"\x01\x08user_str\x08pass_str"
client.send(socks5.AUTH_FAILURE)
tick(None)
with tests.server_socket(proxy_conn) as uri:
uri_parsed = urllib.parse.urlparse(uri)
proxy_info = httplib2.ProxyInfo(
httplib2.socks.PROXY_TYPE_SOCKS5,
proxy_host=uri_parsed.hostname,
proxy_port=uri_parsed.port,
proxy_rdns=True,
proxy_user="user_str",
proxy_pass="pass_str",
)
http = httplib2.Http(proxy_info=proxy_info)
try:
http.request(uri, "GET")
assert False, "expected socks authentication error"
except httplib2.socks.SOCKS5AuthError:
pass
except Exception as e:
assert "authentication failed" in str(e)
@pytest.mark.forked
def test_functional_socks5(monkeypatch):
expect_body = "unique-{}".format(random.randint(1, 100)).encode("utf-8")
gserver = [None, b""]
glog = []
def proxy_conn(client, tick):
assert gserver[0] is not None
assert gserver[1]
data_hello = client.recv(64)
assert data_hello == bytes([socks5.VERSION, 1, socks5.AUTH_NO_AUTHENTICATION_REQUIRED])
client.send(socks5.SERVER_GREETING_NO_AUTH)
data_connect = client.recv(64)
assert data_connect == bytes([socks5.VERSION, socks5.CMD_CONNECT, 0x00]) + gserver[1]
http_endpoint = gserver[0] # TODO parse data_connect
backend = socket.create_connection(http_endpoint, timeout=5)
client.send(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00")
request_buf = tests.BufferedReader(client)
request = tests.HttpRequest.from_buffered(request_buf)
assert request is not None
glog.append(request)
backend.sendall(request.raw)
response_buf = tests.BufferedReader(backend)
backend_response = tests.HttpResponse.from_buffered(response_buf)
client.sendall(backend_response.raw)
tick(request)
with tests.server_socket(proxy_conn, scheme=None) as proxy_endpoint, tests.server_const_http(
body=expect_body, scheme=None
) as http_endpoint:
http_host, http_port = http_endpoint
http_ip_version = check_ip_version(http_host)
http_endpoint_socks_format = b""
if http_ip_version == 4:
http_endpoint_socks_format = b"\x01" + bytes(map(int, http_host.split(".")))
elif http_ip_version == 6:
pass # TODO
else:
http_endpoint_socks_format = b"\x03" + bytes([len(http_host)]) + http_host.encode("ascii")
assert http_endpoint_socks_format
http_endpoint_socks_format += http_port.to_bytes(2, byteorder="big")
gserver[0] = http_endpoint
gserver[1] = http_endpoint_socks_format
proxy_uri = "socks5://{}:{}".format(*proxy_endpoint)
monkeypatch.setenv("http_proxy", proxy_uri)
uri = "http://{}:{}/".format(*http_endpoint)
http = httplib2.Http()
response, body = http.request(uri, "GET")
assert response.status == 200
assert body == expect_body
def check_ip_version(s: str):
try:
ip = ipaddress.ip_address(s)
if isinstance(ip, ipaddress.IPv4Address):
return 4
elif isinstance(ip, ipaddress.IPv6Address):
return 6
else:
raise NotImplementedError
except ValueError:
return None
@pytest.mark.forked
def test_functional_noproxy_star_http(monkeypatch):
def handler(request):
if request.method == "CONNECT":
return tests.http_response_bytes(
status="400 Expected direct",
headers={"connection": "close"},
)
return tests.http_response_bytes()
with tests.server_request(handler) as uri:
monkeypatch.setenv("http_proxy", uri)
monkeypatch.setenv("no_proxy", "*")
http = httplib2.Http()
response, _ = http.request(uri, "GET")
assert response.status == 200
@pytest.mark.forked
def test_functional_noproxy_star_https(monkeypatch):
def handler(request):
if request.method == "CONNECT":
return tests.http_response_bytes(
status="400 Expected direct",
headers={"connection": "close"},
)
return tests.http_response_bytes()
with tests.server_request(handler, tls=True) as uri:
monkeypatch.setenv("https_proxy", uri)
monkeypatch.setenv("no_proxy", "*")
http = httplib2.Http(ca_certs=tests.CA_CERTS)
response, _ = http.request(uri, "GET")
assert response.status == 200