Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85651250
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
9 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/test/fixtures/server.pem b/test/fixtures/server.pem
new file mode 100644
index 000000000..8bd3d6d05
--- /dev/null
+++ b/test/fixtures/server.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIICpDCCAYwCCQC0vCQAnSoGdzANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
+b2NhbGhvc3QwHhcNMjYwMTE2MTY1ODE5WhcNMzYwMTE0MTY1ODE5WjAUMRIwEAYD
+VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq
+dZ4O2upZqwIo1eK5KrW1IIsjkfsFK8hE7Llh+4axcesiUKot0ib1CUhRSYiL1DLO
+CIYQOw8IKQDVSC4JWAX9SsnX4W8dwexMQuSQG7/IKX2auC1bNNySFvoqM6Gq3GL9
+MqBFonZGXDPZu8fmxsI/2p9+2GK13F+HXgoLlXSCoO3XELJaBmjv29tgxxWRxCiH
+m4u0briSxgUEx+CctpKPvGDmLaoIOIhjtuoG6OjkeWUOp6jDcteazO23VxPyF5cS
+NbRJgm8AckrTQ6wbWSnhyqF8rPEsIc0ZAlUdDEs5fL3sjugc566FvE+GOkZIEyDD
+tgWbc4Ne+Kp/nnt6oVxpAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADv+J1DTok8V
+MKVKo0hsRnHTeJQ2+EIgOspuYlEzez3PysOZH6diAQxO2lzuo9LKxP3hnmw17XO/
+P2oCzYyb9/P58VY/gr4UDIfuhgcE0cVfdsRhVId/I2FW6VP2f5q1TGbDUxSsVIlG
+6hufn1aLBu90LtEbDkHqbnD05yYPwdqzWg4TrOXbX+jBhQrXJJdB3W7KTgozjRQw
+F7+/2IyXoxXuxcwQBQlYhUbvGlsFqFpP/6cz2al5i5pNUkiNaSYwlRmuwa7zoTft
+tHf57dhfXIpXET2BaJM6DSjDOOG/QleRXkvkTI5J21q+Bo+XnOzo19p4cZKJpTFC
+SNgrftyNh3k=
+-----END CERTIFICATE-----
+
diff --git a/test/pleroma/http/hackney_follow_redirect_regression_test.exs b/test/pleroma/http/hackney_follow_redirect_regression_test.exs
new file mode 100644
index 000000000..ea631024c
--- /dev/null
+++ b/test/pleroma/http/hackney_follow_redirect_regression_test.exs
@@ -0,0 +1,325 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
+ use ExUnit.Case, async: false
+
+ setup do
+ {:ok, _} = Application.ensure_all_started(:hackney)
+
+ {:ok, tls_server} = start_tls_redirect_server()
+ {:ok, proxy} = start_connect_proxy()
+
+ on_exit(fn ->
+ stop_connect_proxy(proxy)
+ stop_tls_redirect_server(tls_server)
+ end)
+
+ {:ok, tls_server: tls_server, proxy: proxy}
+ end
+
+ test "hackney follow_redirect crashes behind CONNECT proxy on relative redirects", %{
+ tls_server: tls_server,
+ proxy: proxy
+ } do
+ url = "#{tls_server.base_url}/redirect"
+
+ opts = [
+ pool: :media,
+ proxy: proxy.proxy_url,
+ insecure: true,
+ connect_timeout: 1_000,
+ recv_timeout: 1_000,
+ follow_redirect: true,
+ force_redirect: true
+ ]
+
+ {pid, ref} = spawn_monitor(fn -> :hackney.request(:get, url, [], <<>>, opts) end)
+
+ assert_receive {:DOWN, ^ref, :process, ^pid, reason}, 5_000
+
+ assert match?({%FunctionClauseError{}, _}, reason) or match?(%FunctionClauseError{}, reason) or
+ match?({:function_clause, _}, reason)
+ end
+
+ test "redirects work via proxy when hackney follow_redirect is disabled", %{
+ tls_server: tls_server,
+ proxy: proxy
+ } do
+ url = "#{tls_server.base_url}/redirect"
+
+ adapter_opts = [
+ pool: :media,
+ proxy: proxy.proxy_url,
+ insecure: true,
+ connect_timeout: 1_000,
+ recv_timeout: 1_000,
+ follow_redirect: false,
+ force_redirect: false,
+ with_body: true
+ ]
+
+ client = Tesla.client([Tesla.Middleware.FollowRedirects], Tesla.Adapter.Hackney)
+
+ assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
+ Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts])
+ end
+
+ defp start_tls_redirect_server do
+ certfile = Path.expand("../../fixtures/server.pem", __DIR__)
+ keyfile = Path.expand("../../fixtures/private_key.pem", __DIR__)
+
+ {:ok, listener} =
+ :ssl.listen(0, [
+ :binary,
+ certfile: certfile,
+ keyfile: keyfile,
+ reuseaddr: true,
+ active: false,
+ packet: :raw,
+ ip: {127, 0, 0, 1}
+ ])
+
+ {:ok, {{127, 0, 0, 1}, port}} = :ssl.sockname(listener)
+
+ {:ok, acceptor} =
+ Task.start_link(fn ->
+ accept_tls_loop(listener)
+ end)
+
+ {:ok, %{listener: listener, acceptor: acceptor, base_url: "https://127.0.0.1:#{port}"}}
+ end
+
+ defp stop_tls_redirect_server(%{listener: listener, acceptor: acceptor}) do
+ :ok = :ssl.close(listener)
+
+ if Process.alive?(acceptor) do
+ Process.exit(acceptor, :normal)
+ end
+ end
+
+ defp accept_tls_loop(listener) do
+ case :ssl.transport_accept(listener) do
+ {:ok, socket} ->
+ _ = Task.start(fn -> serve_tls(socket) end)
+ accept_tls_loop(listener)
+
+ {:error, :closed} ->
+ :ok
+
+ {:error, _reason} ->
+ :ok
+ end
+ end
+
+ defp serve_tls(tcp_socket) do
+ with {:ok, ssl_socket} <- :ssl.handshake(tcp_socket, 2_000),
+ {:ok, data} <- recv_ssl_headers(ssl_socket),
+ {:ok, path} <- parse_path(data) do
+ case path do
+ "/redirect" ->
+ send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/final"}], "")
+
+ "/final" ->
+ send_ssl_response(ssl_socket, 200, "OK", [], "ok")
+
+ _ ->
+ send_ssl_response(ssl_socket, 404, "Not Found", [], "not found")
+ end
+
+ :ssl.close(ssl_socket)
+ else
+ _ ->
+ _ = :gen_tcp.close(tcp_socket)
+ :ok
+ end
+ end
+
+ defp recv_ssl_headers(socket, acc \\ <<>>) do
+ case :ssl.recv(socket, 0, 1_000) do
+ {:ok, data} ->
+ acc = acc <> data
+
+ if :binary.match(acc, "\r\n\r\n") != :nomatch do
+ {:ok, acc}
+ else
+ if byte_size(acc) > 8_192 do
+ {:error, :too_large}
+ else
+ recv_ssl_headers(socket, acc)
+ end
+ end
+
+ {:error, _} = error ->
+ error
+ end
+ end
+
+ defp send_ssl_response(socket, status, reason, headers, body) do
+ base_headers =
+ [
+ {"Content-Length", Integer.to_string(byte_size(body))},
+ {"Connection", "close"}
+ ] ++ headers
+
+ iodata =
+ [
+ "HTTP/1.1 ",
+ Integer.to_string(status),
+ " ",
+ reason,
+ "\r\n",
+ Enum.map(base_headers, fn {k, v} -> [k, ": ", v, "\r\n"] end),
+ "\r\n",
+ body
+ ]
+
+ :ssl.send(socket, iodata)
+ end
+
+ defp start_connect_proxy do
+ {:ok, listener} =
+ :gen_tcp.listen(0, [
+ :binary,
+ active: false,
+ packet: :raw,
+ reuseaddr: true,
+ ip: {127, 0, 0, 1}
+ ])
+
+ {:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener)
+
+ {:ok, acceptor} =
+ Task.start_link(fn ->
+ accept_proxy_loop(listener)
+ end)
+
+ {:ok, %{listener: listener, acceptor: acceptor, proxy_url: "127.0.0.1:#{port}"}}
+ end
+
+ defp stop_connect_proxy(%{listener: listener, acceptor: acceptor}) do
+ :ok = :gen_tcp.close(listener)
+
+ if Process.alive?(acceptor) do
+ Process.exit(acceptor, :normal)
+ end
+ end
+
+ defp accept_proxy_loop(listener) do
+ case :gen_tcp.accept(listener) do
+ {:ok, socket} ->
+ _ = Task.start(fn -> serve_proxy(socket) end)
+ accept_proxy_loop(listener)
+
+ {:error, :closed} ->
+ :ok
+
+ {:error, _reason} ->
+ :ok
+ end
+ end
+
+ defp serve_proxy(client_socket) do
+ with {:ok, {headers, rest}} <- recv_tcp_headers(client_socket),
+ {:ok, {host, port}} <- parse_connect(headers),
+ {:ok, upstream_socket} <- connect_upstream(host, port) do
+ :gen_tcp.send(client_socket, "HTTP/1.1 200 Connection established\r\n\r\n")
+
+ if rest != <<>> do
+ :gen_tcp.send(upstream_socket, rest)
+ end
+
+ tunnel(client_socket, upstream_socket)
+ else
+ _ ->
+ :gen_tcp.close(client_socket)
+ :ok
+ end
+ end
+
+ defp tunnel(client_socket, upstream_socket) do
+ parent = self()
+ _ = spawn_link(fn -> forward(client_socket, upstream_socket, parent) end)
+ _ = spawn_link(fn -> forward(upstream_socket, client_socket, parent) end)
+
+ receive do
+ :tunnel_closed -> :ok
+ after
+ 10_000 -> :ok
+ end
+
+ :gen_tcp.close(client_socket)
+ :gen_tcp.close(upstream_socket)
+ end
+
+ defp forward(from_socket, to_socket, parent) do
+ case :gen_tcp.recv(from_socket, 0, 10_000) do
+ {:ok, data} ->
+ _ = :gen_tcp.send(to_socket, data)
+ forward(from_socket, to_socket, parent)
+
+ {:error, _reason} ->
+ send(parent, :tunnel_closed)
+ :ok
+ end
+ end
+
+ defp recv_tcp_headers(socket, acc \\ <<>>) do
+ case :gen_tcp.recv(socket, 0, 1_000) do
+ {:ok, data} ->
+ acc = acc <> data
+
+ case :binary.match(acc, "\r\n\r\n") do
+ :nomatch ->
+ if byte_size(acc) > 8_192 do
+ {:error, :too_large}
+ else
+ recv_tcp_headers(socket, acc)
+ end
+
+ {idx, _len} ->
+ split_at = idx + 4
+ <<headers::binary-size(split_at), rest::binary>> = acc
+ {:ok, {headers, rest}}
+ end
+
+ {:error, _} = error ->
+ error
+ end
+ end
+
+ defp parse_connect(data) do
+ with [request_line | _] <- String.split(data, "\r\n", trim: true),
+ ["CONNECT", hostport | _] <- String.split(request_line, " ", parts: 3),
+ [host, port_str] <- String.split(hostport, ":", parts: 2),
+ {port, ""} <- Integer.parse(port_str) do
+ {:ok, {host, port}}
+ else
+ _ -> {:error, :invalid_connect}
+ end
+ end
+
+ defp connect_upstream(host, port) do
+ address =
+ case :inet.parse_address(String.to_charlist(host)) do
+ {:ok, ip} -> ip
+ {:error, _} -> String.to_charlist(host)
+ end
+
+ :gen_tcp.connect(address, port, [:binary, active: false, packet: :raw], 1_000)
+ end
+
+ defp parse_path(data) do
+ case String.split(data, "\r\n", parts: 2) do
+ [request_line | _] ->
+ case String.split(request_line, " ") do
+ [_method, path, _protocol] -> {:ok, path}
+ _ -> {:error, :invalid_request}
+ end
+
+ _ ->
+ {:error, :invalid_request}
+ end
+ end
+end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Aug 30, 3:08 PM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1738048
Default Alt Text
(9 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline
Log In to Comment