Skip to content

Commit 95452f7

Browse files
committed
Add client identifier header to HTTP requests
Creates an anonymized repository identifier based on the SHA of the first commit, hashed with SHA256 for additional privacy. The identifier is sent as an `x-hex-client-id` header when available.
1 parent 7da33db commit 95452f7

File tree

4 files changed

+78
-1
lines changed

4 files changed

+78
-1
lines changed

lib/hex/http.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ defmodule Hex.HTTP do
4848
defp build_headers(headers) do
4949
default_headers = %{"user-agent" => user_agent()}
5050

51-
Map.merge(default_headers, headers)
51+
default_headers
52+
|> add_client_identifier_header()
53+
|> Map.merge(headers)
5254
end
5355

5456
defp build_http_opts(url, timeout) do
@@ -271,6 +273,13 @@ defmodule Hex.HTTP do
271273
host
272274
end
273275

276+
defp add_client_identifier_header(headers) do
277+
case Hex.Utils.client_identifier() do
278+
nil -> headers
279+
identifier -> Map.put(headers, "x-hex-client-id", identifier)
280+
end
281+
end
282+
274283
defp user_agent do
275284
ci = if Hex.State.fetch!(:ci), do: " (CI)", else: ""
276285
"Hex/#{Hex.version()} (Elixir/#{System.version()}) (OTP/#{Hex.Utils.otp_version()})#{ci}"

lib/hex/utils.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,24 @@ defmodule Hex.Utils do
324324
{app, req, opts}
325325
end)
326326
end
327+
328+
@doc """
329+
Gets an anonymized identifier for the current git repository.
330+
331+
This function finds the SHA of the first commit in the repository and hashes it once more for
332+
anonymization.
333+
334+
Returns `nil` if git is not available or the directory is not a git repository.
335+
"""
336+
def client_identifier do
337+
with path when is_binary(path) <- System.find_executable("git"),
338+
{output, 0} <- System.cmd("git", ["rev-list", "--max-parents=0", "HEAD"]) do
339+
output
340+
|> String.trim()
341+
|> then(&:crypto.hash(:sha256, &1))
342+
|> Base.encode16(case: :lower)
343+
else
344+
_ -> nil
345+
end
346+
end
327347
end

test/hex/http_test.exs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,25 @@ defmodule Hex.HTTPTest do
115115
)
116116
end)
117117
end
118+
119+
test "request includes identifier header when available", %{bypass: bypass} do
120+
in_tmp(fn ->
121+
# Initialize a git repository with a commit
122+
System.cmd("git", ["init", "--initial-branch=main"])
123+
System.cmd("git", ["config", "user.email", "test@example.com"])
124+
System.cmd("git", ["config", "user.name", "Test User"])
125+
File.write!("test.txt", "test content")
126+
System.cmd("git", ["add", "test.txt"])
127+
System.cmd("git", ["commit", "-m", "Initial commit"])
128+
129+
Bypass.expect(bypass, fn conn ->
130+
assert [client_id] = Plug.Conn.get_req_header(conn, "x-hex-client-id")
131+
assert client_id =~ ~r/^[a-f0-9]{64}$/
132+
133+
Plug.Conn.resp(conn, 200, "")
134+
end)
135+
136+
Hex.HTTP.request(:get, "http://localhost:#{bypass.port}", %{}, nil)
137+
end)
138+
end
118139
end

test/hex/utils_test.exs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
defmodule Hex.UtilsTest do
2+
use ExUnit.Case
3+
4+
describe "client_identifier/0" do
5+
test "an identifier is included within a repository" do
6+
assert Hex.Utils.client_identifier() =~ ~r/^[a-f0-9]{64}$/
7+
end
8+
9+
test "identifier is nil outside of a repository" do
10+
# The tmp_dir resolves at hex/test/tmp, which allows git to traverse up to the repository
11+
# root and find a commit. We're creating a temporary directory to simulate being outside of
12+
# a repository instead.
13+
dir =
14+
"../../.."
15+
|> Path.expand(__DIR__)
16+
|> Path.join("empty-directory")
17+
18+
try do
19+
File.mkdir!(dir)
20+
21+
File.cd!(dir, fn -> refute Hex.Utils.client_identifier() end)
22+
after
23+
File.rmdir(dir)
24+
end
25+
end
26+
end
27+
end

0 commit comments

Comments
 (0)