Skip to content

Commit 3839199

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 3839199

File tree

4 files changed

+66
-1
lines changed

4 files changed

+66
-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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,27 @@ 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+
opts = ["rev-list", "--max-parents=0", "HEAD"]
338+
339+
with path when is_binary(path) <- System.find_executable("git"),
340+
{output, 0} <- System.cmd("git", opts, stderr_to_stdout: true) do
341+
output
342+
|> String.trim()
343+
|> then(&:crypto.hash(:sha256, &1))
344+
|> Base.encode16(case: :lower)
345+
else
346+
_ ->
347+
nil
348+
end
349+
end
327350
end

test/hex/http_test.exs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,23 @@ 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"])
123+
File.write!("test.txt", "test content")
124+
System.cmd("git", ["add", "test.txt"])
125+
System.cmd("git", ["commit", "-m", "Initial commit"])
126+
127+
Bypass.expect(bypass, fn conn ->
128+
assert [client_id] = Plug.Conn.get_req_header(conn, "x-hex-client-id")
129+
assert client_id =~ ~r/^[a-f0-9]{64}$/
130+
131+
Plug.Conn.resp(conn, 200, "")
132+
end)
133+
134+
Hex.HTTP.request(:get, "http://localhost:#{bypass.port}", %{}, nil)
135+
end)
136+
end
118137
end

test/hex/utils_test.exs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
@tag :tmp_dir
10+
test "identifier is nil outside of a repository", %{tmp_dir: dir} do
11+
File.cd!(dir, fn -> refute Hex.Utils.client_identifier() end)
12+
end
13+
end
14+
end

0 commit comments

Comments
 (0)