commit fb50c4d

uint  ·  2026-04-28 21:20:23 +0000 UTC
parent 2161d48
reorganise http_handle into route_dispatch
1 files changed,  +281, -229
+281, -229
  1@@ -34,14 +34,19 @@ struct response {
  2 
  3 static void auth_delay_sleep(void);
  4 static int cors_build(char* out, size_t outsz, const char* hdr, int preflight);
  5+static int cors_origin_allowed(const char* origin);
  6 static const struct item* find_item(uint64_t id);
  7 static int item_path_for_id(char out[4096], uint64_t id, const struct user* u);
  8 static const char* mime_from_path(const char* path);
  9 static int parse_hex64(const char* s, uint64_t* out);
 10-static int path_item(struct item* it, const char* path, const char* prefix, const struct user* u, int c, const char* hdr);
 11 static int parse_range(const char* hdr, size_t total, size_t* start, size_t* end);
 12 static int read_request(int c, char* method, size_t msz, char* path, size_t psz, char* hdr, size_t hsz);
 13 static void reply(int c, const char* hdr, const struct response* res);
 14+static int route_dispatch(int c, const char* hdr, const char* path, const struct user* u, int head_only);
 15+static int route_item_path(struct item* it, const char* path, const char* prefix, const struct user* u, int c, const char* hdr);
 16+static int route_library(int c, const char* hdr, const struct user* u, int head_only);
 17+static int route_method(int c, const char* hdr, const char* method, int* head_only);
 18+static int route_rescan(int c, const char* hdr, const struct user* u);
 19 static int stat_item(const struct item* it, struct stat* st);
 20 static int stream_file(int c, const struct item* it, const char* hdr, int head_only);
 21 
 22@@ -325,66 +330,6 @@ static int parse_hex64(const char* s, uint64_t* out)
 23 	return 0;
 24 }
 25 
 26-/**
 27- * @brief Parse route id and resolve to an item
 28- *
 29- * @param it Output item
 30- * @param path Request path
 31- * @param prefix Route prefix
 32- * @param u Authenticated user filter
 33- * @param c Connected client socket file descriptor
 34- * @param hdr Request header block
 35- *
 36- * @return 0=Success, 1=Response sent, -1=Failure
 37- */
 38-static int path_item(struct item* it, const char* path, const char* prefix, const struct user* u, int c, const char* hdr)
 39-{
 40-	uint64_t id;
 41-	int fr;
 42-
 43-	if (parse_hex64(path + strlen(prefix), &id) < 0) {
 44-		reply(c, hdr, &(struct response){
 45-			.status = HTTP_400,
 46-			.ctype = HTTP_TEXT,
 47-			.extra = NULL,
 48-			.body = "Bad Request\n",
 49-			.len = sizeof("Bad Request\n") - 1,
 50-			.send_body = 1,
 51-			.preflight = 0,
 52-		});
 53-		return 1;
 54-	}
 55-
 56-	fr = item_path_for_id(it->path, id, u);
 57-	if (fr == 1) {
 58-		reply(c, hdr, &(struct response){
 59-			.status = HTTP_404,
 60-			.ctype = HTTP_TEXT,
 61-			.extra = NULL,
 62-			.body = "Not Found\n",
 63-			.len = sizeof("Not Found\n") - 1,
 64-			.send_body = 1,
 65-			.preflight = 0,
 66-		});
 67-		return 1;
 68-	}
 69-	if (fr < 0) {
 70-		reply(c, hdr, &(struct response){
 71-			.status = HTTP_500,
 72-			.ctype = HTTP_TEXT,
 73-			.extra = NULL,
 74-			.body = "Server Error\n",
 75-			.len = sizeof("Server Error\n") - 1,
 76-			.send_body = 1,
 77-			.preflight = 0,
 78-		});
 79-		return -1;
 80-	}
 81-
 82-	it->id = id;
 83-	return 0;
 84-}
 85-
 86 /**
 87  * @brief Parse HTTP Range header
 88  *
 89@@ -588,6 +533,279 @@ static void reply(int c, const char* hdr, const struct response* res)
 90 		(void)write_all(c, res->body, res->len);
 91 }
 92 
 93+/**
 94+ * @brief Dispatch request path to route handler
 95+ *
 96+ * @param c Connected client socket file descriptor
 97+ * @param hdr Request header block
 98+ * @param path Request path
 99+ * @param u Authenticated user filter
100+ * @param head_only Whether request method is HEAD
101+ *
102+ * @return 0=Handled, -1=Failure
103+ */
104+static int route_dispatch(int c, const char* hdr, const char* path, const struct user* u, int head_only)
105+{
106+	if (strcmp(path, "/ping") == 0) {
107+		LOG(verbose_log, "HTTP", "Route              /ping");
108+		reply(c, hdr, &(struct response){ HTTP_200, HTTP_TEXT, NULL, "OK\n", sizeof("OK\n") - 1, 1, 0 });
109+		return 0;
110+	}
111+
112+	if (strcmp(path, "/library") == 0)
113+		return route_library(c, hdr, u, head_only);
114+
115+	if (strcmp(path, "/rescan") == 0)
116+		return route_rescan(c, hdr, u);
117+
118+	if (strncmp(path, "/stream/", 8) == 0) {
119+		struct item tmp;
120+		char rel[4096];
121+
122+		LOG(verbose_log, "HTTP", "Route              /stream");
123+		tmp.path = rel;
124+
125+		int rc = route_item_path(&tmp, path, "/stream/", u, c, hdr);
126+		if (rc != 0)
127+			return rc < 0 ? -1 : 0;
128+
129+		return stream_file(c, &tmp, hdr, head_only);
130+	}
131+
132+	if (strncmp(path, "/meta/", 6) == 0) {
133+		struct item tmp;
134+		char rel[4096];
135+		struct stat st;
136+		const char* type;
137+		struct json j;
138+
139+		LOG(verbose_log, "HTTP", "Route              /meta");
140+		tmp.path = rel;
141+
142+		int rc = route_item_path(&tmp, path, "/meta/", u, c, hdr);
143+		if (rc != 0)
144+			return rc < 0 ? -1 : 0;
145+
146+		if (stat_item(&tmp, &st) < 0) {
147+			reply(c, hdr, &(struct response){ HTTP_404, HTTP_TEXT, NULL, "Not Found\n", sizeof("Not Found\n") - 1, 1, 0 });
148+			return 0;
149+		}
150+
151+		type = mime_from_path(tmp.path);
152+		if (json_meta(&j, &tmp, (size_t)st.st_size, (long)st.st_mtime, type) < 0) {
153+			reply(c, hdr, &(struct response){ HTTP_500, HTTP_TEXT, NULL, "JSON Failed\n", sizeof("JSON Failed\n") - 1, 1, 0 });
154+			return -1;
155+		}
156+
157+		reply(c, hdr, &(struct response){ HTTP_200, HTTP_JSON, NULL, j.buf, j.len, !head_only, 0 });
158+		json_free(&j);
159+		return 0;
160+	}
161+
162+	LOG(verbose_log, "HTTP", "Route not found    %s", path);
163+	reply(c, hdr, &(struct response){ HTTP_404, HTTP_TEXT, NULL, "Not Found\n", sizeof("Not Found\n") - 1, 1, 0 });
164+	return 0;
165+}
166+
167+/**
168+ * @brief Parse route id and resolve to an item
169+ *
170+ * @param it Output item
171+ * @param path Request path
172+ * @param prefix Route prefix
173+ * @param u Authenticated user filter
174+ * @param c Connected client socket file descriptor
175+ * @param hdr Request header block
176+ *
177+ * @return 0=Success, 1=Response sent, -1=Failure
178+ */
179+static int route_item_path(struct item* it, const char* path, const char* prefix, const struct user* u, int c, const char* hdr)
180+{
181+	uint64_t id;
182+	int fr;
183+
184+	if (parse_hex64(path + strlen(prefix), &id) < 0) {
185+		reply(c, hdr, &(struct response){
186+			.status = HTTP_400,
187+			.ctype = HTTP_TEXT,
188+			.extra = NULL,
189+			.body = "Bad Request\n",
190+			.len = sizeof("Bad Request\n") - 1,
191+			.send_body = 1,
192+			.preflight = 0,
193+		});
194+		return 1;
195+	}
196+
197+	fr = item_path_for_id(it->path, id, u);
198+	if (fr == 1) {
199+		reply(c, hdr, &(struct response){
200+			.status = HTTP_404,
201+			.ctype = HTTP_TEXT,
202+			.extra = NULL,
203+			.body = "Not Found\n",
204+			.len = sizeof("Not Found\n") - 1,
205+			.send_body = 1,
206+			.preflight = 0,
207+		});
208+		return 1;
209+	}
210+	if (fr < 0) {
211+		reply(c, hdr, &(struct response){
212+			.status = HTTP_500,
213+			.ctype = HTTP_TEXT,
214+			.extra = NULL,
215+			.body = "Server Error\n",
216+			.len = sizeof("Server Error\n") - 1,
217+			.send_body = 1,
218+			.preflight = 0,
219+		});
220+		return -1;
221+	}
222+
223+	it->id = id;
224+	return 0;
225+}
226+
227+/**
228+ * @brief Handle /library route
229+ *
230+ * @param c Connected client socket file descriptor
231+ * @param hdr Request header block
232+ * @param u Authenticated user filter
233+ * @param head_only Whether request method is HEAD
234+ *
235+ * @return 0=Handled, -1=Failure
236+ */
237+static int route_library(int c, const char* hdr, const struct user* u, int head_only)
238+{
239+	struct json j;
240+	struct library view;
241+
242+	LOG(verbose_log, "HTTP", "Route              /library");
243+	mtx_lock(&lib_lock);
244+	memset(&view, 0, sizeof(view));
245+
246+	if (!u) {
247+		view = lib;
248+	}
249+	else {
250+		size_t i;
251+
252+		if (lib.len > 0) {
253+			view.items = calloc(lib.len, sizeof(*view.items));
254+			if (!view.items) {
255+				mtx_unlock(&lib_lock);
256+				reply(c, hdr, &(struct response){ HTTP_500, HTTP_TEXT, NULL, "Server Error\n", sizeof("Server Error\n") - 1, 1, 0 });
257+				return -1;
258+			}
259+		}
260+
261+		for (i = 0; i < lib.len; i++) {
262+			if (!user_allows_path(u, lib.items[i].path))
263+				continue;
264+			view.items[view.len++] = lib.items[i];
265+		}
266+		view.cap = view.len;
267+	}
268+
269+	if (json_library(&j, &view) < 0) {
270+		if (u)
271+			free(view.items);
272+
273+		mtx_unlock(&lib_lock);
274+		LOG(verbose_log, "JSON", "Encode     FAILED");
275+		reply(c, hdr, &(struct response){ HTTP_500, HTTP_TEXT, NULL, "JSON Encode Failed\n", sizeof("JSON Encode Failed\n") - 1, 1, 0 });
276+		return -1;
277+	}
278+
279+	mtx_unlock(&lib_lock);
280+	LOG(verbose_log, "JSON", "Encoded bytes      %zu bytes", j.len);
281+	reply(c, hdr, &(struct response){ HTTP_200, HTTP_JSON, NULL, j.buf, j.len, !head_only, 0 });
282+	json_free(&j);
283+
284+	if (u)
285+		free(view.items);
286+	return 0;
287+}
288+
289+/**
290+ * @brief Handle request method and OPTIONS
291+ *
292+ * @param c Connected client socket file descriptor
293+ * @param hdr Request header block
294+ * @param method HTTP method
295+ * @param head_only Output HEAD flag
296+ *
297+ * @return 0=Continue, 1=Response sent
298+ */
299+static int route_method(int c, const char* hdr, const char* method, int* head_only)
300+{
301+	if (strcmp(method, "GET") == 0) {
302+		*head_only = 0;
303+		return 0;
304+	}
305+	if (strcmp(method, "HEAD") == 0) {
306+		*head_only = 1;
307+		return 0;
308+	}
309+	if (strcmp(method, "OPTIONS") == 0) {
310+		reply(c, hdr, &(struct response){ HTTP_204, NULL, NULL, NULL, 0, 0, 1 });
311+		return 1;
312+	}
313+
314+	LOG(verbose_log, "HTTP", "Method forbidden   %s", method);
315+	reply(c, hdr, &(struct response){ HTTP_405, HTTP_TEXT, NULL, "Method Not Allowed\n", sizeof("Method Not Allowed\n") - 1, 1, 0 });
316+	return 1;
317+}
318+
319+/**
320+ * @brief Handle /rescan route
321+ *
322+ * @param c Connected client socket file descriptor
323+ * @param hdr Request header block
324+ * @param u Authenticated user
325+ *
326+ * @return 0=Handled, -1=Failure
327+ */
328+static int route_rescan(int c, const char* hdr, const struct user* u)
329+{
330+	struct timespec t0;
331+	struct timespec t1;
332+	size_t before = 0;
333+	size_t after = 0;
334+
335+	LOG(verbose_log, "HTTP", "Route              /rescan");
336+	if (users.len == 0) {
337+		LOG(true, "HTTP", "Rescan forbidden   Auth disabled");
338+		reply(c, hdr, &(struct response){ HTTP_403, HTTP_TEXT, NULL, "Forbidden\n", sizeof("Forbidden\n") - 1, 1, 0 });
339+		return 0;
340+	}
341+
342+	LOG(true, "SCAN", "Rescan requested   %s", u->name);
343+	clock_gettime(CLOCK_MONOTONIC, &t0);
344+
345+	mtx_lock(&lib_lock);
346+	before = lib.len;
347+	LOG(verbose_log, "SCAN", "Rescan begin       %s (%zu items)", media_dir, before);
348+	int ok = scan_library_rescan(&lib, media_dir);
349+	after = lib.len;
350+	mtx_unlock(&lib_lock);
351+
352+	clock_gettime(CLOCK_MONOTONIC, &t1);
353+	long ms = (t1.tv_sec - t0.tv_sec) * 1000L + (t1.tv_nsec - t0.tv_nsec) / 1000000L;
354+
355+	if (ok < 0) {
356+		LOG(true, "SCAN", "Rescan FAILED      %s (%ld ms)", media_dir, ms);
357+		reply(c, hdr, &(struct response){ HTTP_500, HTTP_TEXT, NULL, "Rescan Failed\n", sizeof("Rescan Failed\n") - 1, 1, 0 });
358+		return -1;
359+	}
360+
361+	LOG(true, "SCAN", "Rescan OK          %zu -> %zu (%ld ms)", before, after, ms);
362+	reply(c, hdr, &(struct response){ HTTP_200, HTTP_TEXT, NULL, "OK\n", sizeof("OK\n") - 1, 1, 0 });
363+	return 0;
364+}
365+
366 /**
367  * @brief Stat media item on disk
368  *
369@@ -800,27 +1018,8 @@ int http_handle(int c)
370 	LOG(verbose_log, "HTTP", "Request            %s %s", method, path);
371 
372 	int head_only = 0;
373-	if (strcmp(method, "GET") == 0) {
374-		head_only = 0;
375-	}
376-	else if (strcmp(method, "HEAD") == 0) {
377-		head_only = 1;
378-	}
379-	else if (strcmp(method, "OPTIONS") == 0) {
380-		reply(c, hdr, &(struct response){ HTTP_204, NULL, NULL, NULL, 0, 0, 1 });
381-		return 0;
382-	}
383-	else {
384-		LOG(verbose_log, "HTTP", "Method forbidden   %s", method);
385-		reply(c, hdr, &(struct response){ HTTP_405, HTTP_TEXT, NULL, "Method Not Allowed\n", sizeof("Method Not Allowed\n") - 1, 1, 0 });
386+	if (route_method(c, hdr, method, &head_only) != 0)
387 		return 0;
388-	}
389-
390-	if (strcmp(path, "/ping") == 0) {
391-		LOG(verbose_log, "HTTP", "Route              /ping");
392-		reply(c, hdr, &(struct response){ HTTP_200, HTTP_TEXT, NULL, "OK\n", sizeof("OK\n") - 1, 1, 0 });
393-		return 0;
394-	}
395 
396 	if (users.len > 0) {
397 		u = users_auth_from_hdr(hdr);
398@@ -839,153 +1038,6 @@ int http_handle(int c)
399 		}
400 	}
401 
402-	if (strcmp(path, "/library") == 0) {
403-		LOG(verbose_log, "HTTP", "Route              /library");
404-
405-		mtx_lock(&lib_lock);
406-
407-		struct json j;
408-		struct library view;
409-		memset(&view, 0, sizeof(view));
410-
411-		if (!u) {
412-			view = lib;
413-		}
414-		else {
415-			if (lib.len > 0) {
416-				view.items = calloc(lib.len, sizeof(*view.items));
417-				if (!view.items) {
418-					mtx_unlock(&lib_lock);
419-					reply(c, hdr, &(struct response){ HTTP_500, HTTP_TEXT, NULL, "Server Error\n", sizeof("Server Error\n") - 1, 1, 0 });
420-					return -1;
421-				}
422-			}
423-
424-			for (size_t i = 0; i < lib.len; i++) {
425-				if (!user_allows_path(u, lib.items[i].path))
426-					continue;
427-				view.items[view.len++] = lib.items[i];
428-			}
429-			view.cap = view.len;
430-		}
431-
432-		if (json_library(&j, &view) < 0) {
433-			if (u)
434-				free(view.items);
435-
436-			mtx_unlock(&lib_lock);
437-
438-			LOG(verbose_log, "JSON", "Encode     FAILED");
439-			reply(c, hdr, &(struct response){ HTTP_500, HTTP_TEXT, NULL, "JSON Encode Failed\n", sizeof("JSON Encode Failed\n") - 1, 1, 0 });
440-			return -1;
441-		}
442-
443-		mtx_unlock(&lib_lock);
444-
445-		LOG(verbose_log, "JSON", "Encoded bytes      %zu bytes", j.len);
446-		reply(c, hdr, &(struct response){ HTTP_200, HTTP_JSON, NULL, j.buf, j.len, !head_only, 0 });
447-		json_free(&j);
448-
449-		if (u)
450-			free(view.items);
451-
452-		return 0;
453-	}
454-
455-	if (strcmp(path, "/rescan") == 0) {
456-		LOG(verbose_log, "HTTP", "Route              /rescan");
457-
458-		if (users.len == 0) {
459-			LOG(true, "HTTP", "Rescan forbidden   Auth disabled");
460-			reply(c, hdr, &(struct response){ HTTP_403, HTTP_TEXT, NULL, "Forbidden\n", sizeof("Forbidden\n") - 1, 1, 0 });
461-			return 0;
462-		}
463-
464-		LOG(true, "SCAN", "Rescan requested   %s", u->name);
465-
466-		struct timespec t0;
467-		struct timespec t1;
468-		clock_gettime(CLOCK_MONOTONIC, &t0);
469-
470-		size_t before = 0;
471-		size_t after  = 0;
472-
473-		mtx_lock(&lib_lock);
474-
475-		before = lib.len;
476-		LOG(verbose_log, "SCAN", "Rescan begin       %s (%zu items)", media_dir, before);
477-
478-		int ok = scan_library_rescan(&lib, media_dir);
479-
480-		after = lib.len;
481-
482-		mtx_unlock(&lib_lock);
483-
484-		clock_gettime(CLOCK_MONOTONIC, &t1);
485-
486-		long ms = (t1.tv_sec - t0.tv_sec) * 1000L +
487-			(t1.tv_nsec - t0.tv_nsec) / 1000000L;
488-
489-		if (ok < 0) {
490-			LOG(true, "SCAN", "Rescan FAILED      %s (%ld ms)", media_dir, ms);
491-			reply(c, hdr, &(struct response){ HTTP_500, HTTP_TEXT, NULL, "Rescan Failed\n", sizeof("Rescan Failed\n") - 1, 1, 0 });
492-			return -1;
493-		}
494-
495-		LOG(true, "SCAN", "Rescan OK          %zu -> %zu (%ld ms)", before, after, ms);
496-
497-		reply(c, hdr, &(struct response){ HTTP_200, HTTP_TEXT, NULL, "OK\n", sizeof("OK\n") - 1, 1, 0 });
498-		return 0;
499-	}
500-
501-	if (strncmp(path, "/stream/", 8) == 0) {
502-		LOG(verbose_log, "HTTP", "Route              /stream");
503-
504-		struct item tmp;
505-		char rel[4096];
506-		tmp.path = rel;
507-
508-		int rc = path_item(&tmp, path, "/stream/", u, c, hdr);
509-		if (rc != 0)
510-			return rc < 0 ? -1 : 0;
511-
512-		return stream_file(c, &tmp, hdr, head_only);
513-	}
514-
515-	if (strncmp(path, "/meta/", 6) == 0) {
516-		LOG(verbose_log, "HTTP", "Route              /meta");
517-
518-		struct item tmp;
519-		char rel[4096];
520-		tmp.path = rel;
521-
522-		int rc = path_item(&tmp, path, "/meta/", u, c, hdr);
523-		if (rc != 0)
524-			return rc < 0 ? -1 : 0;
525-
526-		struct stat st;
527-		if (stat_item(&tmp, &st) < 0) {
528-			reply(c, hdr, &(struct response){ HTTP_404, HTTP_TEXT, NULL, "Not Found\n", sizeof("Not Found\n") - 1, 1, 0 });
529-			return 0;
530-		}
531-
532-		const char* type = mime_from_path(tmp.path);
533-
534-		struct json j;
535-		if (json_meta(&j, &tmp, (size_t)st.st_size, (long)st.st_mtime, type) < 0) {
536-			reply(c, hdr, &(struct response){ HTTP_500, HTTP_TEXT, NULL, "JSON Failed\n", sizeof("JSON Failed\n") - 1, 1, 0 });
537-			return -1;
538-		}
539-
540-		reply(c, hdr, &(struct response){ HTTP_200, HTTP_JSON, NULL, j.buf, j.len, !head_only, 0 });
541-		json_free(&j);
542-
543-		return 0;
544-	}
545-
546-	LOG(verbose_log, "HTTP", "Route not found    %s", path);
547-	reply(c, hdr, &(struct response){ HTTP_404, HTTP_TEXT, NULL, "Not Found\n", sizeof("Not Found\n") - 1, 1, 0 });
548-
549-	return 0;
550+	return route_dispatch(c, hdr, path, u, head_only);
551 }
552