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+