Skip to content

Commit 85fd39f

Browse files
Claudeclaude
andcommitted
tracker: fix HTTP header parsing dropped by a tuple-arity mismatch
read_request matched a 3-tuple against read_request_line's 4-tuple return, so the with/1 fell through and the header-reading step never ran: the raw header bytes were passed downstream as the "headers" value instead of a parsed map. This stayed hidden because every live HTTP test ran with keepalive disabled (short-circuiting the only map read on that value) and an empty real_ip_header. Thread the leftover buffer from the request line into read_headers and consume the correct 4-tuple. Adds live keep-alive and real-ip-header regression tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DqKy7xpT2kp7ZATjLKsdhT
1 parent f6bf158 commit 85fd39f

2 files changed

Lines changed: 84 additions & 2 deletions

File tree

tracker/lib/bento/tracker/http/frontend.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,8 +445,8 @@ defmodule Bento.Tracker.HTTP.Frontend do
445445
## HTTP/1.1 wire handling via :erlang.decode_packet
446446

447447
defp read_request(scheme, socket, timeout) do
448-
with {:ok, method, target} <- read_request_line(scheme, socket, timeout, <<>>),
449-
{:ok, headers} <- read_headers(scheme, socket, timeout, <<>>, %{}) do
448+
with {:ok, method, target, rest} <- read_request_line(scheme, socket, timeout, <<>>),
449+
{:ok, headers} <- read_headers(scheme, socket, timeout, rest, %{}) do
450450
{:ok, method, target, headers}
451451
end
452452
end

tracker/test/bento/tracker/http/frontend_test.exs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,88 @@ defmodule Bento.Tracker.HTTP.FrontendTest do
111111
assert http_get(port, "/nope") == "404 page not found\n"
112112
end
113113

114+
# Starts a second frontend with its own config; returns its port.
115+
defp start_frontend(config) do
116+
{:ok, store} = Storage.new("memory", %{shard_count: 4})
117+
logic = Logic.new(%{announce_interval: 1800, min_announce_interval: 900}, store)
118+
119+
base = %{addr: "127.0.0.1:0", announce_routes: ["/announce"], scrape_routes: ["/scrape"]}
120+
{:ok, pid} = Frontend.start_link({logic, Map.merge(base, config)})
121+
Process.unlink(pid)
122+
{:ok, {_ip, port}} = Frontend.listen_address(pid)
123+
124+
on_exit(fn ->
125+
if Process.alive?(pid) do
126+
try do
127+
GenServer.stop(pid)
128+
catch
129+
:exit, _reason -> :ok
130+
end
131+
end
132+
end)
133+
134+
port
135+
end
136+
137+
defp announce_query(peer_id) do
138+
"info_hash=#{@info_hash}&peer_id=#{peer_id}&port=6881&left=100&downloaded=0&uploaded=0"
139+
end
140+
141+
test "keep-alive serves several requests over one connection" do
142+
port = start_frontend(%{enable_keepalive: true})
143+
{:ok, socket} = :gen_tcp.connect(~c"127.0.0.1", port, [:binary, active: false], 5000)
144+
145+
for i <- 1..2 do
146+
peer_id = "-TEST0#{i}-6wfG2wk6wWLc"
147+
request = "GET /announce?#{announce_query(peer_id)} HTTP/1.1\r\nHost: t\r\n\r\n"
148+
:ok = :gen_tcp.send(socket, request)
149+
150+
response = recv_response(socket)
151+
assert [_headers, body] = :binary.split(response, "\r\n\r\n")
152+
assert {:ok, decoded} = Bento.decode(body)
153+
assert decoded["interval"] == 1800
154+
end
155+
156+
:gen_tcp.close(socket)
157+
end
158+
159+
test "the real ip header is honored by the live server" do
160+
port = start_frontend(%{real_ip_header: "x-real-ip"})
161+
{:ok, socket} = :gen_tcp.connect(~c"127.0.0.1", port, [:binary, active: false], 5000)
162+
163+
request =
164+
"GET /announce?#{announce_query(@peer_id)}&compact=1 HTTP/1.1\r\n" <>
165+
"Host: t\r\nX-Real-IP: 198.51.100.7\r\nConnection: close\r\n\r\n"
166+
167+
:ok = :gen_tcp.send(socket, request)
168+
response = recv_all(socket, <<>>)
169+
:gen_tcp.close(socket)
170+
171+
assert {:ok, decoded} = Bento.decode(split_body(response))
172+
# The lone peer echoed back must carry the header-provided IP.
173+
assert <<198, 51, 100, 7, 0x1A, 0xE1>> = decoded["peers"]
174+
end
175+
176+
# Reads one keep-alive response using its Content-Length.
177+
defp recv_response(socket, acc \\ <<>>) do
178+
case :binary.split(acc, "\r\n\r\n") do
179+
[headers, body] ->
180+
[_full, len] = Regex.run(~r/content-length: (\d+)/i, headers)
181+
need = String.to_integer(len) - byte_size(body)
182+
183+
if need > 0 do
184+
{:ok, more} = :gen_tcp.recv(socket, need, 5000)
185+
acc <> more
186+
else
187+
acc
188+
end
189+
190+
[_incomplete] ->
191+
{:ok, data} = :gen_tcp.recv(socket, 0, 5000)
192+
recv_response(socket, acc <> data)
193+
end
194+
end
195+
114196
test "a non-GET method on a known route returns 405", %{port: port} do
115197
assert status_line(port, "POST", "/announce") == "HTTP/1.1 405 Method Not Allowed"
116198
end

0 commit comments

Comments
 (0)