fix(http): forward plain HTTP proxy requests

This commit is contained in:
Lain Soykaf 2026-07-22 20:35:51 +04:00
commit d090aeb2bb
No known key found for this signature in database
8 changed files with 201 additions and 11 deletions

View file

@ -88,6 +88,26 @@ defmodule Pleroma.Gun.ConnTest do
stop_conn(conn)
end
test "uses HTTP/1 for a TLS forwarding proxy" do
conn = spawn_link(fn -> Process.sleep(:infinity) end)
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8443, opts ->
assert opts.transport == :tls
assert opts.tls_opts == [verify: :verify_none]
assert opts.protocols == [:http]
{:ok, conn}
end)
assert {:ok, ^conn, :http, :forward_proxy} =
Conn.open(URI.parse("http://origin.example/inbox"),
proxy: {~c"proxy.example", 8443},
transport: :tls,
proxy_tls_opts: [verify: :verify_none]
)
stop_conn(conn)
end
test "passes authentication and remote DNS destination to a SOCKS5 proxy" do
conn = spawn_link(fn -> Process.sleep(:infinity) end)
@ -130,6 +150,21 @@ defmodule Pleroma.Gun.ConnTest do
)
end
test "returns the proxy status when CONNECT is forbidden" do
conn = spawn_link(fn -> Process.sleep(:infinity) end)
stream = make_ref()
expect(Pleroma.GunMock, :open, fn _, _, _ -> {:ok, conn} end)
expect(Pleroma.GunMock, :connect, fn ^conn, _ -> stream end)
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :fin, 403, []} end)
expect(Pleroma.GunMock, :close, fn ^conn -> Process.exit(conn, :normal) end)
assert {:error, {:proxy_connect_failed, 403}} =
Conn.open(URI.parse("https://origin.example/inbox"),
proxy: {~c"proxy.example", 8080}
)
end
test "rejects SOCKS4 before opening a socket" do
assert {:error, :socks4_unsupported} =
Conn.open(URI.parse("https://origin.example/inbox"),

View file

@ -14,16 +14,22 @@ defmodule Pleroma.HTTP.GunIntegrationTest do
origin = start_tls_origin(self())
connect_proxy = start_connect_proxy(self())
forward_proxy = start_forward_proxy(self())
socks_proxy = start_socks5_proxy(self())
on_exit(fn ->
Process.sleep(20)
stop_server(socks_proxy)
stop_server(forward_proxy)
stop_server(connect_proxy)
stop_server(origin)
end)
{:ok, origin: origin, connect_proxy: connect_proxy, socks_proxy: socks_proxy}
{:ok,
origin: origin,
connect_proxy: connect_proxy,
forward_proxy: forward_proxy,
socks_proxy: socks_proxy}
end
test "requests HTTPS directly on a non-standard port", %{origin: origin} do
@ -66,6 +72,28 @@ defmodule Pleroma.HTTP.GunIntegrationTest do
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = result
end
test "requests HTTP through a forwarding proxy", %{forward_proxy: proxy} do
proxy_opts = [proxy: {~c"127.0.0.1", proxy.port}]
url = "http://origin.example:8080/final?key=value"
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
assert_receive {:forward_proxy, ^url, "origin.example:8080", nil}
end
test "authenticates HTTP requests to a forwarding proxy", %{forward_proxy: proxy} do
proxy_opts = [
proxy: {~c"127.0.0.1", proxy.port},
proxy_auth: {"alice", "secret"}
]
url = "http://origin.example/final"
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
expected = "Basic " <> Base.encode64("alice:secret")
assert_receive {:forward_proxy, ^url, "origin.example", ^expected}
end
test "opens a request-ready authenticated CONNECT tunnel", %{
origin: origin,
connect_proxy: proxy
@ -215,6 +243,32 @@ defmodule Pleroma.HTTP.GunIntegrationTest do
assert origin_port == origin.port
end
test "requests through unauthenticated SOCKS5 with an IP proxy host", %{
origin: origin,
socks_proxy: proxy
} do
proxy_opts = [proxy: {:socks5, {127, 0, 0, 1}, proxy.port}]
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
request(origin.base_url <> "/final", proxy_opts)
assert_receive {:socks5_proxy, nil, nil, "127.0.0.1", origin_port}
assert origin_port == origin.port
end
test "requests through unauthenticated SOCKS5 with a localhost proxy host", %{
origin: origin,
socks_proxy: proxy
} do
proxy_opts = [proxy: {:socks5, ~c"localhost", proxy.port}]
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
request(origin.base_url <> "/final", proxy_opts)
assert_receive {:socks5_proxy, nil, nil, "127.0.0.1", origin_port}
assert origin_port == origin.port
end
defp request(url, proxy_opts \\ [], follow_redirects \\ false) do
middleware =
if follow_redirects do
@ -433,6 +487,37 @@ defmodule Pleroma.HTTP.GunIntegrationTest do
end
end
defp start_forward_proxy(parent) do
start_tcp_server(fn socket -> serve_forward_proxy(socket, parent) end)
end
defp serve_forward_proxy(socket, parent) do
with {:ok, request} <- recv_headers(:gen_tcp, socket) do
{headers, _buffered_body} = split_headers(request)
target = request_path(headers)
host = header(headers, "host")
auth = header(headers, "proxy-authorization")
if String.starts_with?(target, "http://") do
send(parent, {:forward_proxy, target, host, auth})
:gen_tcp.send(
socket,
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok"
)
serve_forward_proxy(socket, parent)
else
:gen_tcp.send(
socket,
"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
)
:gen_tcp.close(socket)
end
end
end
defp start_socks5_proxy(parent) do
start_tcp_server(fn socket -> serve_socks5_proxy(socket, parent) end)
end
@ -440,10 +525,7 @@ defmodule Pleroma.HTTP.GunIntegrationTest do
defp serve_socks5_proxy(socket, parent) do
with {:ok, <<5, method_count>>} <- :gen_tcp.recv(socket, 2, 2_000),
{:ok, methods} <- :gen_tcp.recv(socket, method_count, 2_000),
true <- :binary.match(methods, <<2>>) != :nomatch,
:ok <- :gen_tcp.send(socket, <<5, 2>>),
{:ok, username, password} <- recv_socks5_auth(socket),
:ok <- :gen_tcp.send(socket, <<1, 0>>),
{:ok, username, password} <- negotiate_socks5_auth(socket, methods),
{:ok, host, port} <- recv_socks5_destination(socket),
{:ok, upstream} <- tcp_connect(host, port),
:ok <- :gen_tcp.send(socket, <<5, 0, 0, 1, 0, 0, 0, 0, 0, 0>>) do
@ -455,6 +537,25 @@ defmodule Pleroma.HTTP.GunIntegrationTest do
end
end
defp negotiate_socks5_auth(socket, methods) do
cond do
:binary.match(methods, <<2>>) != :nomatch ->
with :ok <- :gen_tcp.send(socket, <<5, 2>>),
{:ok, username, password} <- recv_socks5_auth(socket),
:ok <- :gen_tcp.send(socket, <<1, 0>>) do
{:ok, username, password}
end
:binary.match(methods, <<0>>) != :nomatch ->
with :ok <- :gen_tcp.send(socket, <<5, 0>>) do
{:ok, nil, nil}
end
true ->
{:error, :unsupported_auth}
end
end
defp recv_socks5_auth(socket) do
with {:ok, <<1, username_length>>} <- :gen_tcp.recv(socket, 2, 2_000),
{:ok, username} <- :gen_tcp.recv(socket, username_length, 2_000),