Skip to content

Commit 71a3977

Browse files
hyperpolymathclaude
andcommitted
feat: VoluMod real audio capture + HAR security manager
VoluMod: PipeWire/PulseAudio capture via pw-record/parec, ring buffer, monitor source discovery, device enumeration. Was 15% stub, now functional. HAR security: API key validation (SHA-256 hashed, tier-aware), token-bucket rate limiting (per-IP, configurable, auto-refill), X.509 certificate validation (expiry, chain, DER/PEM). Two new Plugs wired into router. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7cdbdc4 commit 71a3977

6 files changed

Lines changed: 1583 additions & 93 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
#
4+
# Plug middleware for API key authentication on HAR API endpoints.
5+
#
6+
# Reads the `X-HAR-API-Key` header and validates it against the
7+
# configured allow-list via HAR.Security.Manager.validate_api_key/1.
8+
# Rejects unauthenticated requests with 401 Unauthorized.
9+
10+
defmodule HAR.Security.ApiKeyPlug do
11+
@moduledoc """
12+
Plug middleware that enforces API key authentication.
13+
14+
Extracts the `X-HAR-API-Key` header from incoming requests and
15+
validates it via `HAR.Security.Manager.validate_api_key/1`.
16+
17+
## Usage in a Phoenix pipeline
18+
19+
pipeline :authenticated_api do
20+
plug :accepts, ["json"]
21+
plug HAR.Security.ApiKeyPlug
22+
end
23+
24+
## Options
25+
26+
- `:header` - the header name to read (default: `"x-har-api-key"`)
27+
- `:optional` - if `true`, missing keys pass through (default: `false`)
28+
"""
29+
30+
@behaviour Plug
31+
32+
import Plug.Conn
33+
require Logger
34+
35+
@default_header "x-har-api-key"
36+
37+
@impl true
38+
def init(opts) do
39+
%{
40+
header: Keyword.get(opts, :header, @default_header),
41+
optional: Keyword.get(opts, :optional, false)
42+
}
43+
end
44+
45+
@impl true
46+
def call(conn, %{header: header, optional: optional}) do
47+
key =
48+
conn
49+
|> get_req_header(header)
50+
|> List.first()
51+
52+
cond do
53+
is_nil(key) and optional ->
54+
conn
55+
56+
is_nil(key) ->
57+
Logger.warning("API request rejected — missing #{header} header")
58+
59+
conn
60+
|> put_resp_content_type("application/json")
61+
|> send_resp(401, Jason.encode!(%{error: "missing_api_key", message: "X-HAR-API-Key header is required"}))
62+
|> halt()
63+
64+
true ->
65+
case HAR.Security.Manager.validate_api_key(key) do
66+
{:ok, :valid} ->
67+
conn
68+
|> assign(:authenticated, true)
69+
70+
{:error, reason} ->
71+
Logger.warning("API request rejected — invalid API key: #{inspect(reason)}")
72+
73+
conn
74+
|> put_resp_content_type("application/json")
75+
|> send_resp(401, Jason.encode!(%{error: "invalid_api_key", message: "Invalid API key"}))
76+
|> halt()
77+
end
78+
end
79+
end
80+
end

0 commit comments

Comments
 (0)