commit 11cd826
uint
·
2026-01-28 02:46:15 +0000 UTC
parent 65eaa44
add basic authentication
5 files changed,
+475,
-12
+32,
-2
1@@ -1,6 +1,8 @@
2-# parados.conf
3+# parados.conf
4 # This is the parados configuration file.
5-# See parados(1) for more information.
6+#
7+# See parados(8) for more information on the
8+# daemon.
9
10 # Core configuration
11 #
12@@ -38,3 +40,31 @@ verbose_log=true
13 #cors_origin=http://127.0.0.1:8000
14 cors_origin=
15
16+# Authentication
17+#
18+# Each entry consists of one `user`, `pass`, and (multiple) `allow` directives.
19+# Multiple users are permitted.
20+#
21+# For example:
22+#
23+# user=glenda
24+# pass=pass123
25+# allow=Media/
26+# allow=Music/
27+#
28+# user=bob
29+# pass=
30+# allow=TV/NBA/
31+#
32+# is valid.
33+#
34+# `pass` may be empty to disable authentication for that entry.
35+#
36+# `allow` specifies which directories a user is permitted to access.
37+# Use "*" to allow all directories, or multiple `allow` lines with
38+# each directory specified.
39+#
40+user=admin
41+pass=
42+allow=*
43+
+15,
-1
1@@ -5,6 +5,7 @@
2
3 #include "config.h"
4 #include "log.h"
5+#include "users.h"
6
7 static int load_path(const char* path);
8 static int parse_bool(const char* s, bool* out);
9@@ -73,7 +74,7 @@ static int parse_bool(const char* s, bool* out)
10
11 static int set_kv(const char* k, const char* v)
12 {
13- /* cmp keys */
14+ /* regular */
15 if (strcmp(k, "media_dir") == 0) {
16 snprintf(media_dir, sizeof(media_dir), "%s", v);
17 return 0;
18@@ -103,11 +104,24 @@ static int set_kv(const char* k, const char* v)
19 return 0;
20 }
21
22+ /* auth */
23 if (strcmp(k, "cors_origin") == 0) {
24 snprintf(cors_origin, sizeof(cors_origin), "%s", v);
25 return 0;
26 }
27
28+ if (strcmp(k, "user") == 0) {
29+ return users_push(v);
30+ }
31+
32+ if (strcmp(k, "pass") == 0) {
33+ return users_set_pass(v);
34+ }
35+
36+ if (strcmp(k, "allow") == 0) {
37+ return users_add_allow(v);
38+ }
39+
40 /* unknown key */
41 return 0;
42 }
+97,
-9
1@@ -16,6 +16,7 @@
2 #include "json.h"
3 #include "log.h"
4 #include "scan.h"
5+#include "users.h"
6 #include "util.h"
7
8 static int cors_build(char* out, size_t outsz, const char* hdr, int preflight);
9@@ -29,7 +30,8 @@ static void reply_json(int c, const char* hdr, const char* status, const char* b
10 static void reply_m3u(int c, const char* hdr, const char* status, size_t len);
11 static void reply_preflight(int c, const char* hdr);
12 static void reply_text(int c, const char* hdr, const char* status, const char* body);
13-static int queue_write(int c, const char* hdr, int head_only);
14+static void reply_unauth(int c, const char* hdr, int send_body);
15+static int queue_write(int c, const char* hdr, int head_only, const struct user* u);
16 static int stat_item(const struct item* it, struct stat* st);
17 static int stream_file(int c, const struct item* it, const char* hdr, int head_only);
18
19@@ -91,12 +93,13 @@ static int cors_build(char* out, size_t outsz, const char* hdr, int preflight)
20 if (preflight) {
21 n = snprintf(
22 out, outsz,
23+
24 "Access-Control-Allow-Origin: %s\r\n"
25 "Vary: Origin\r\n"
26 "Access-Control-Allow-Methods: GET, HEAD, OPTIONS\r\n"
27- "Access-Control-Allow-Headers: Range, Content-Type\r\n"
28- "Access-Control-Max-Age: 600\r\n"
29- "Access-Control-Expose-Headers: Content-Length, Content-Range, Accept-Ranges, Content-Type\r\n",
30+ "Access-Control-Allow-Headers: Range, Content-Type, Authorization\r\n"
31+ "Access-Control-Max-Age: 600\r\n",
32+
33 ao
34 );
35 }
36@@ -374,7 +377,6 @@ static void reply_hdr(int c, const char* hdr, const char* status, const char* ct
37 (void)write_all(c, resp, (size_t)n);
38 }
39
40-
41 static void reply_json(int c, const char* hdr, const char* status, const char* body, size_t len, int send_body)
42 {
43 reply_hdr(c, hdr, status, HTTP_JSON, len, 0);
44@@ -402,7 +404,39 @@ static void reply_text(int c, const char* hdr, const char* status, const char* b
45 (void)write_all(c, body, len);
46 }
47
48-static int queue_write(int c, const char* hdr, int head_only)
49+static void reply_unauth(int c, const char* hdr, int send_body)
50+{
51+ const char* body = "unauthorized\n";
52+ size_t blen = strlen(body);
53+
54+ char cors[512];
55+ (void)cors_build(cors, sizeof(cors), hdr, 0);
56+
57+ char resp[HTTP_RESP_MAX];
58+ int n = snprintf(
59+ resp, sizeof(resp),
60+ "HTTP/1.1 401 Unauthorized\r\n"
61+ "%s"
62+ "WWW-Authenticate: Basic realm=\"parados\"\r\n"
63+ HTTP_TEXT
64+ HTTP_LENGTH
65+ HTTP_CLOSE
66+ "\r\n",
67+ cors,
68+ blen
69+ );
70+
71+ if (n < 0)
72+ return;
73+ if ((size_t)n >= sizeof(resp))
74+ n = (int)(sizeof(resp) - 1);
75+
76+ (void)write_all(c, resp, (size_t)n);
77+ if (send_body)
78+ (void)write_all(c, body, blen);
79+}
80+
81+static int queue_write(int c, const char* hdr, int head_only, const struct user* u)
82 {
83 char host[512];
84 char base[768];
85@@ -421,6 +455,9 @@ static int queue_write(int c, const char* hdr, int head_only)
86 len += 8; /* "#EXTM3U\n" */
87
88 for (size_t i = 0; i < lib.len; i++) {
89+ if (u && !user_allows_path(u, lib.items[i].path))
90+ continue;
91+
92 int n = snprintf(
93 NULL, 0,
94 "%s/stream/%016llx\n",
95@@ -443,8 +480,10 @@ static int queue_write(int c, const char* hdr, int head_only)
96 return -1;
97
98 for (size_t i = 0; i < lib.len; i++) {
99- char line[2048];
100+ if (u && !user_allows_path(u, lib.items[i].path))
101+ continue;
102
103+ char line[2048];
104 int n = snprintf(
105 line, sizeof(line),
106 "%s/stream/%016llx\n",
107@@ -643,6 +682,11 @@ int http_handle(int c)
108 char path[1024];
109 char hdr[HTTP_REQ_MAX];
110
111+ const struct user* u = NULL;
112+ method[0] = '\0';
113+ path[0] = '\0';
114+ hdr[0] = '\0';
115+
116 if (read_request(c, method, sizeof(method), path, sizeof(path), hdr, sizeof(hdr)) < 0) {
117 reply_text(c, hdr, HTTP_400, "bad request\n");
118 return -1;
119@@ -672,11 +716,44 @@ int http_handle(int c)
120 return 0;
121 }
122
123+ if (users.len > 0) {
124+ u = users_auth_from_hdr(hdr);
125+ if (!u) {
126+ reply_unauth(c, hdr, !head_only);
127+ return 0;
128+ }
129+ }
130+
131 if (strcmp(path, "/library") == 0) {
132 LOG(verbose_log, "HTTP", "route /library");
133 struct json j;
134+ struct library view;
135+ memset(&view, 0, sizeof(view));
136+
137+ if (!u) {
138+ view = lib;
139+ }
140+ else {
141+ if (lib.len > 0) {
142+ view.items = calloc(lib.len, sizeof(*view.items));
143+ if (!view.items) {
144+ reply_text(c, hdr, HTTP_500, "server error\n");
145+ return -1;
146+ }
147+ }
148+
149+ for (size_t i = 0; i < lib.len; i++) {
150+ if (!user_allows_path(u, lib.items[i].path))
151+ continue;
152+ view.items[view.len++] = lib.items[i];
153+ }
154+ view.cap = view.len;
155+ }
156+
157+ if (json_library(&j, &view) < 0) {
158+ if (u)
159+ free(view.items);
160
161- if (json_library(&j, &lib) < 0) {
162 LOG(verbose_log, "JSON", "encode failed");
163 reply_text(c, hdr, HTTP_500, "json failed\n");
164 return -1;
165@@ -686,6 +763,9 @@ int http_handle(int c)
166 reply_json(c, hdr, HTTP_200, j.buf, j.len, !head_only);
167 json_free(&j);
168
169+ if (u)
170+ free(view.items);
171+
172 return 0;
173 }
174
175@@ -703,6 +783,10 @@ int http_handle(int c)
176 reply_text(c, hdr, HTTP_404, "not found\n");
177 return 0;
178 }
179+ if (u && !user_allows_path(u, it->path)) {
180+ reply_text(c, hdr, HTTP_404, "not found\n");
181+ return 0;
182+ }
183
184 return stream_file(c, it, hdr, head_only);
185 }
186@@ -721,6 +805,10 @@ int http_handle(int c)
187 reply_text(c, hdr, HTTP_404, "not found\n");
188 return 0;
189 }
190+ if (u && !user_allows_path(u, it->path)) {
191+ reply_text(c, hdr, HTTP_404, "not found\n");
192+ return 0;
193+ }
194
195 struct stat st;
196 if (stat_item(it, &st) < 0) {
197@@ -745,7 +833,7 @@ int http_handle(int c)
198 if (strcmp(path, "/queue") == 0) {
199 LOG(verbose_log, "HTTP", "route /queue");
200
201- if (queue_write(c, hdr, head_only) < 0) {
202+ if (queue_write(c, hdr, head_only, u) < 0) {
203 reply_text(c, hdr, HTTP_500, "server error\n");
204 return -1;
205 }
+32,
-0
1@@ -0,0 +1,32 @@
2+#ifndef USERS_H
3+#define USERS_H
4+
5+#include <stdbool.h>
6+#include <stddef.h>
7+
8+struct user {
9+ char name[64];
10+ char pass[128];
11+
12+ char** allow;
13+ size_t allow_len;
14+ size_t allow_cap;
15+};
16+
17+struct users {
18+ struct user* v;
19+ size_t len;
20+ size_t cap;
21+};
22+
23+extern struct users users;
24+
25+bool user_allows_path(const struct user* u, const char* relpath);
26+int users_add_allow(const char* prefix);
27+const struct user* users_auth_from_hdr(const char* hdr);
28+void users_free(void);
29+int users_push(const char* name);
30+int users_set_pass(const char* pass);
31+
32+#endif /* USERS_H */
33+
+299,
-0
1@@ -0,0 +1,299 @@
2+#include <stdlib.h>
3+#include <string.h>
4+#include <strings.h>
5+
6+#include "log.h"
7+#include "users.h"
8+#include "util.h"
9+
10+struct users users;
11+
12+static int allow_push(struct user* u, const char* s);
13+static int b64_decode(unsigned char* out, size_t outsz, const char* in);
14+static int b64_val(int c);
15+static int ct_equal(const char* a, const char* b);
16+static const struct user* find_user(const char* name);
17+
18+static int allow_push(struct user* u, const char* s)
19+{
20+ if (u->allow_len == u->allow_cap) {
21+ size_t ncap = u->allow_cap ? (u->allow_cap * 2) : 8;
22+ char** nv = realloc(u->allow, ncap * sizeof(*nv));
23+ if (!nv)
24+ return -1;
25+
26+ u->allow = nv;
27+ u->allow_cap = ncap;
28+ }
29+
30+ char* p = strdup(s);
31+ if (!p)
32+ return -1;
33+
34+ u->allow[u->allow_len++] = p;
35+ return 0;
36+}
37+
38+static int b64_decode(unsigned char* out, size_t outsz, const char* in)
39+{
40+ size_t olen = 0;
41+ int buf = 0;
42+ int bits = 0;
43+
44+ for (; *in; in++) {
45+ int v = b64_val((unsigned char)*in);
46+ if (v == -1)
47+ continue; /* skip whitespace/junk */
48+
49+ if (v == -2)
50+ break; /* padding */
51+
52+ buf = (buf << 6) | v; /* append 6 bits */
53+ bits += 6;
54+
55+ if (bits >= 8) {
56+ bits -= 8;
57+ if (olen + 1 >= outsz)
58+ return -1; /* overflow */
59+
60+ out[olen++] = (unsigned char)((buf >> bits) & 0xff);
61+ }
62+ }
63+
64+ if (olen >= outsz)
65+ return -1;
66+
67+ out[olen] = '\0';
68+ return 0;
69+}
70+
71+static int b64_val(int c)
72+{
73+ if (c >= 'A' && c <= 'Z')
74+ return c - 'A';
75+ if (c >= 'a' && c <= 'z')
76+ return c - 'a' + 26;
77+ if (c >= '0' && c <= '9')
78+ return c - '0' + 52;
79+ if (c == '+')
80+ return 62;
81+ if (c == '/')
82+ return 63;
83+ if (c == '=')
84+ return -2;
85+
86+ return -1;
87+}
88+static int ct_equal(const char* a, const char* b)
89+{
90+ size_t la = strlen(a);
91+ size_t lb = strlen(b);
92+ size_t n = (la > lb) ? la : lb;
93+
94+ unsigned char diff = 0;
95+
96+ for (size_t i = 0; i < n; i++) {
97+ unsigned char ca = (i < la) ? (unsigned char)a[i] : 0;
98+ unsigned char cb = (i < lb) ? (unsigned char)b[i] : 0;
99+ diff |= (unsigned char)(ca ^ cb);
100+ }
101+
102+ return (diff == 0) && (la == lb);
103+}
104+
105+static const struct user* find_user(const char* name)
106+{
107+ for (size_t i = 0; i < users.len; i++)
108+ if (strcmp(users.v[i].name, name) == 0)
109+ return &users.v[i];
110+
111+ return NULL;
112+}
113+
114+static int prefix_ok(const char* path, const char* pre)
115+{
116+ size_t n = strlen(pre);
117+ if (n == 0)
118+ return 0;
119+
120+ if (strcmp(pre, "*") == 0)
121+ return 1;
122+
123+ if (strncmp(path, pre, n) != 0)
124+ return 0;
125+
126+ /* allow match or dir boundary */
127+ if (path[n] == '\0')
128+ return 1;
129+ if (pre[n - 1] == '/')
130+ return 1;
131+ if (path[n] == '/')
132+ return 1;
133+
134+ return 0;
135+}
136+
137+bool user_allows_path(const struct user* u, const char* relpath)
138+{
139+ if (!u || !relpath)
140+ return false;
141+
142+ /* no allow list -> allow nothing */
143+ if (u->allow_len == 0)
144+ return false;
145+
146+ for (size_t i = 0; i < u->allow_len; i++)
147+ if (prefix_ok(relpath, u->allow[i]))
148+ return true;
149+
150+ return false;
151+}
152+
153+int users_add_allow(const char* prefix)
154+{
155+ if (users.len == 0 || !prefix) {
156+ LOG(true, "AUTH", "users_add_allow: missing user or prefix");
157+ return -1;
158+ }
159+
160+ LOG(verbose_log, "AUTH", "allow for %s: %s", users.v[users.len - 1].name, prefix);
161+
162+ return allow_push(&users.v[users.len - 1], prefix);
163+}
164+
165+const struct user* users_auth_from_hdr(const char* hdr)
166+{
167+ if (users.len == 0)
168+ return NULL;
169+
170+ char auth[512];
171+ if (hdr_get_value(auth, hdr, "authorization") < 0) {
172+ LOG(true, "AUTH", "no Authorization header");
173+ return NULL;
174+ }
175+
176+ /* given "Basic XXXX" */
177+ const char* p = auth;
178+ while (*p == ' ' || *p == '\t')
179+ p++;
180+
181+ if (strncasecmp(p, "Basic", 5) != 0) {
182+ LOG(verbose_log, "AUTH", "Authorization not Basic");
183+ return NULL;
184+ }
185+
186+ p += 5;
187+ while (*p == ' ' || *p == '\t')
188+ p++;
189+
190+ if (*p == '\0') {
191+ LOG(verbose_log, "AUTH", "Basic auth missing token");
192+ return NULL;
193+ }
194+
195+ unsigned char dec[512];
196+ if (b64_decode(dec, sizeof(dec), p) < 0) {
197+ LOG(verbose_log, "AUTH", "Basic auth b64 decode failed");
198+ return NULL;
199+ }
200+
201+ char* sep = strchr((char*)dec, ':');
202+ if (!sep) {
203+ LOG(verbose_log, "AUTH", "Basic auth decoded but missing ':'");
204+ return NULL;
205+ }
206+
207+ *sep = '\0';
208+ const char* user = (const char*)dec;
209+ const char* pass = (const char*)(sep + 1);
210+
211+ const struct user* u = find_user(user);
212+ if (!u) {
213+ LOG(true, "AUTH", "auth failed: unknown user '%s'", user);
214+ return NULL;
215+ }
216+
217+ /* empty password -> passwordless account */
218+ if (u->pass[0] == '\0') {
219+ if (pass[0] == '\0') {
220+ LOG(true, "AUTH", "auth ok: %s (passwordless)", user);
221+ return u;
222+ }
223+ LOG(true, "AUTH", "auth failed: %s (passwordless) but pass given", user);
224+ return NULL;
225+ }
226+
227+ if (!ct_equal(u->pass, pass)) {
228+ LOG(true, "AUTH", "auth failed: bad password for %s", user);
229+ return NULL;
230+ }
231+
232+ LOG(verbose_log, "AUTH", "auth ok: %s", user);
233+ return u;
234+}
235+
236+void users_free(void)
237+{
238+ for (size_t i = 0; i < users.len; i++) {
239+ for (size_t j = 0; j < users.v[i].allow_len; j++)
240+ free(users.v[i].allow[j]);
241+ free(users.v[i].allow);
242+
243+ users.v[i].allow = NULL;
244+ users.v[i].allow_len = 0;
245+ users.v[i].allow_cap = 0;
246+ }
247+
248+ free(users.v);
249+ users.v = NULL;
250+ users.len = 0;
251+ users.cap = 0;
252+}
253+
254+int users_push(const char* name)
255+{
256+ if (!name || name[0] == '\0') {
257+ LOG(true, "AUTH", "empty username");
258+ return -1;
259+ }
260+
261+ if (users.len == users.cap) {
262+ size_t ncap = users.cap ? (users.cap * 2) : 8;
263+ struct user* nv = realloc(users.v, ncap * sizeof(*nv));
264+ if (!nv) {
265+ LOG(true, "AUTH", "realloc failed (cap=%zu -> %zu)", users.cap, ncap);
266+ return -1;
267+ }
268+ users.v = nv;
269+ users.cap = ncap;
270+ }
271+
272+ memset(&users.v[users.len], 0, sizeof(users.v[users.len]));
273+ snprintf(users.v[users.len].name, sizeof(users.v[users.len].name), "%s", name);
274+
275+ LOG(verbose_log, "AUTH", "user added: %s", users.v[users.len].name);
276+
277+ users.len++;
278+ return 0;
279+}
280+
281+int users_set_pass(const char* pass)
282+{
283+ if (users.len == 0) {
284+ LOG(true, "AUTH", "no user yet");
285+ return -1;
286+ }
287+
288+ if (!pass)
289+ pass = "";
290+
291+ /* copy pass->user.pass */
292+ snprintf(users.v[users.len - 1].pass, sizeof(users.v[users.len - 1].pass), "%s", pass);
293+
294+ LOG(
295+ verbose_log, "AUTH", "pass set for user: %s (%s)",
296+ users.v[users.len - 1].name, (pass[0] == '\0') ? "empty" : "non-empty"
297+ );
298+ return 0;
299+}
300+