commit 16f2b2a

uint  ·  2026-01-26 19:50:10 +0000 UTC
parent 6bb5302
add CORS header for more access
3 files changed,  +192, -27
+190, -27
  1@@ -1,6 +1,7 @@
  2 #include <ctype.h>
  3 #include <errno.h>
  4 #include <fcntl.h>
  5+#include <limits.h>
  6 #include <stddef.h>
  7 #include <stdint.h>
  8 #include <stdio.h>
  9@@ -18,14 +19,16 @@
 10 #include "scan.h"
 11 
 12 static const char* cistrstr(const char* hay, const char* nee);
 13+static int cors_build(char* out, size_t outsz, const char* hdr, int preflight);
 14 static const struct item* find_item(uint64_t id);
 15 static int join_path(char* out, size_t outsz, const char* a, const char* b);
 16 static const char* mime_from_path(const char* path);
 17 static int parse_hex64(const char* s, uint64_t* out);
 18 static int parse_range(const char* hdr, size_t total, size_t* start, size_t* end);
 19 static int read_request(int c, char* method, size_t msz, char* path, size_t psz, char* hdr, size_t hsz);
 20-static void reply_json(int c, const char* status, const char* body, size_t len, int send_body);
 21-static void reply_text(int c, const char* status, const char* body);
 22+static void reply_json(int c, const char* hdr, const char* status, const char* body, size_t len, int send_body);
 23+static void reply_preflight(int c, const char* hdr);
 24+static void reply_text(int c, const char* hdr, const char* status, const char* body);
 25 static int stat_item(const struct item* it, struct stat* st);
 26 static int stream_file(int c, const struct item* it, const char* hdr, int head_only);
 27 static int write_all(int fd, const void* buf, size_t n);
 28@@ -62,6 +65,121 @@ static const char* cistrstr(const char* hay, const char* nee)
 29 	return NULL;
 30 }
 31 
 32+static int cors_origin_allowed(const char* origin)
 33+{
 34+	if (!cors_origin || cors_origin[0] == '\0')
 35+		return 0;
 36+
 37+	if (strcmp(cors_origin, "*") == 0)
 38+		return 1;
 39+
 40+	/* comma/space separated allowlist */
 41+	const char* p = cors_origin;
 42+	while (*p) {
 43+		while (*p == ' ' || *p == '\t' || *p == ',')
 44+			p++;
 45+
 46+		const char* s = p;
 47+		while (*p && *p != ',')
 48+			p++;
 49+
 50+		const char* e = p;
 51+		while (e > s && (e[-1] == ' ' || e[-1] == '\t'))
 52+			e--;
 53+
 54+		size_t n = (size_t)(e - s);
 55+		if (n > 0 && strlen(origin) == n && strncmp(origin, s, n) == 0)
 56+			return 1;
 57+
 58+		if (*p == ',')
 59+			p++;
 60+	}
 61+
 62+	return 0;
 63+}
 64+
 65+static int cors_get_origin(char out[512], const char* hdr)
 66+{
 67+	const char* p = cistrstr(hdr, "\norigin:");
 68+	if (!p)
 69+		p = cistrstr(hdr, "\rorigin:");
 70+	if (!p)
 71+		p = cistrstr(hdr, "origin:");
 72+	if (!p)
 73+		return -1;
 74+
 75+	p = strchr(p, ':');
 76+	if (!p)
 77+		return -1;
 78+	p++;
 79+
 80+	while (*p == ' ' || *p == '\t')
 81+		p++;
 82+
 83+	size_t n = 0;
 84+	while (*p && *p != '\r' && *p != '\n') {
 85+		if (n + 1 >= 512)
 86+			return -1;
 87+		out[n++] = *p++;
 88+	}
 89+	out[n] = '\0';
 90+
 91+	if (n == 0)
 92+		return -1;
 93+
 94+	return 0;
 95+}
 96+
 97+static int cors_build(char* out, size_t outsz, const char* hdr, int preflight)
 98+{
 99+	char origin[512];
100+
101+	if (!out || outsz == 0)
102+		return 0;
103+
104+	out[0] = '\0';
105+
106+	if (!cors_origin || cors_origin[0] == '\0')
107+		return 0;
108+
109+	if (cors_get_origin(origin, hdr) < 0)
110+		return 0;
111+
112+	if (!cors_origin_allowed(origin))
113+		return 0;
114+
115+	int n;
116+	const char* ao = (strcmp(cors_origin, "*") == 0) ? "*" : origin;
117+	if (preflight) {
118+		n = snprintf(
119+			out, outsz,
120+			"Access-Control-Allow-Origin: %s\r\n"
121+			"Vary: Origin\r\n"
122+			"Access-Control-Allow-Methods: GET, HEAD, OPTIONS\r\n"
123+			"Access-Control-Allow-Headers: Range, Content-Type\r\n"
124+			"Access-Control-Max-Age: 600\r\n"
125+			"Access-Control-Expose-Headers: Content-Length, Content-Range, Accept-Ranges, Content-Type\r\n",
126+			ao
127+		);
128+	}
129+	else {
130+		n = snprintf(
131+			out, outsz,
132+			"Access-Control-Allow-Origin: %s\r\n"
133+			"Vary: Origin\r\n"
134+			"Access-Control-Expose-Headers: Content-Length, Content-Range, Accept-Ranges, Content-Type\r\n",
135+			ao
136+		);
137+	}
138+
139+	if (n < 0)
140+		return 0;
141+	if ((size_t)n >= outsz)
142+		out[outsz - 1] = '\0';
143+
144+	return 1;
145+}
146+
147 static const struct item* find_item(uint64_t id)
148 {
149 	for (size_t i = 0; i < lib.len; i++)
150@@ -279,19 +397,23 @@ static int read_request(int c, char* method, size_t msz, char* path, size_t psz,
151 	return 0;
152 }
153 
154-static void reply_json(int c, const char* status, const char* body, size_t len, int send_body)
155+static void reply_json(int c, const char* hdr, const char* status, const char* body, size_t len, int send_body)
156 {
157 	char resp[HTTP_RESP_MAX];
158+	char cors[512];
159+
160+	(void)cors_build(cors, sizeof(cors), hdr, 0);
161 	int n = snprintf(
162 		resp, sizeof(resp),
163 
164+		"%s"
165 		"%s"
166 		HTTP_JSON
167 		HTTP_LENGTH
168 		HTTP_CLOSE
169 		"\r\n",
170 
171-		status, len
172+		status, cors, len
173 	);
174 
175 	if (n < 0)
176@@ -306,12 +428,43 @@ static void reply_json(int c, const char* status, const char* body, size_t len,
177 		(void)write_all(c, body, len);
178 }
179 
180-static void reply_text(int c, const char* status, const char* body)
181+static void reply_preflight(int c, const char* hdr)
182 {
183 	char resp[HTTP_RESP_MAX];
184+	char cors[512];
185+
186+	(void)cors_build(cors, sizeof(cors), hdr, 1);
187+
188 	int n = snprintf(
189 		resp, sizeof(resp),
190+		"%s"
191+		"%s"
192+		HTTP_LENGTH
193+		HTTP_CLOSE
194+		"\r\n",
195+		HTTP_204,
196+		cors,
197+		(size_t)0
198+	);
199+
200+	if (n < 0)
201+		return;
202+	if ((size_t)n >= sizeof(resp))
203+		n = (int)(sizeof(resp) - 1);
204 
205+	(void)write_all(c, resp, (size_t)n);
206+}
207+
208+static void reply_text(int c, const char* hdr, const char* status, const char* body)
209+{
210+	char resp[HTTP_RESP_MAX];
211+	char cors[512];
212+
213+	(void)cors_build(cors, sizeof(cors), hdr, 0);
214+	int n = snprintf(
215+		resp, sizeof(resp),
216+
217+		"%s"
218 		"%s"
219 		HTTP_TEXT
220 		HTTP_LENGTH
221@@ -320,6 +473,7 @@ static void reply_text(int c, const char* status, const char* body)
222 		"%s",
223 
224 		status,
225+		cors,
226 		strlen(body), body
227 	);
228 
229@@ -355,7 +509,7 @@ static int stream_file(int c, const struct item* it, const char* hdr, int head_o
230 	/* build absolute path */
231 	if (join_path(full, sizeof(full), media_dir, it->path) < 0) {
232 		LOG(verbose_log, "HTTP", "path too long");
233-		reply_text(c, HTTP_500, "server error\n");
234+		reply_text(c, hdr, HTTP_500, "server error\n");
235 		return -1;
236 	}
237 
238@@ -363,7 +517,7 @@ static int stream_file(int c, const struct item* it, const char* hdr, int head_o
239 	int fd = open(full, O_RDONLY);
240 	if (fd < 0) {
241 		LOG(verbose_log, "HTTP", "open failed: %s", it->path);
242-		reply_text(c, HTTP_404, "not found\n");
243+		reply_text(c, hdr, HTTP_404, "not found\n");
244 		return 0;
245 	}
246 
247@@ -371,14 +525,14 @@ static int stream_file(int c, const struct item* it, const char* hdr, int head_o
248 	struct stat st;
249 	if (fstat(fd, &st) < 0) {
250 		close(fd);
251-		reply_text(c, HTTP_500, "server error\n");
252+		reply_text(c, hdr, HTTP_500, "server error\n");
253 		return -1;
254 	}
255 
256 	/* reject non-regular files */
257 	if (!S_ISREG(st.st_mode)) {
258 		close(fd);
259-		reply_text(c, HTTP_404, "not found\n");
260+		reply_text(c, hdr, HTTP_404, "not found\n");
261 		return 0;
262 	}
263 
264@@ -386,24 +540,27 @@ static int stream_file(int c, const struct item* it, const char* hdr, int head_o
265 	size_t start = 0;
266 	size_t end = total ? (total - 1) : 0;
267 	const char* type = mime_from_path(it->path);
268+	char cors[512];
269+
270+	(void)cors_build(cors, sizeof(cors), hdr, 0);
271 
272 	int partial = parse_range(hdr, total, &start, &end);
273 	if (partial == RANGE_BAD) {
274 		close(fd);
275-		reply_text(c, HTTP_400, "bad range\n");
276+		reply_text(c, hdr, HTTP_400, "bad range\n");
277 		return 0;
278 	}
279 
280 	if (partial == RANGE_UNSAT) {
281 		close(fd);
282-		reply_text(c, HTTP_416, "range not satisfiable\n");
283+		reply_text(c, hdr, HTTP_416, "range not satisfiable\n");
284 		return 0;
285 	}
286 
287 	if (partial == RANGE_OK) {
288 		if (lseek(fd, (off_t)start, SEEK_SET) < 0) {
289 			close(fd);
290-			reply_text(c, HTTP_500, "server error\n");
291+			reply_text(c, hdr, HTTP_500, "server error\n");
292 			return -1;
293 		}
294 	}
295@@ -420,12 +577,13 @@ static int stream_file(int c, const struct item* it, const char* hdr, int head_o
296 			resp, sizeof(resp),
297 			"%s"
298 			HTTP_CTYPE
299+			"%s"
300 			HTTP_RANGE
301 			HTTP_CRG
302 			HTTP_LENGTH
303 			HTTP_CLOSE
304 			"\r\n",
305-			HTTP_206, type, start, end, total, len
306+			HTTP_206, type, cors, start, end, total, len
307 		);
308 	}
309 	else {
310@@ -434,11 +592,12 @@ static int stream_file(int c, const struct item* it, const char* hdr, int head_o
311 			resp, sizeof(resp),
312 			"%s"
313 			HTTP_CTYPE
314+			"%s"
315 			HTTP_RANGE
316 			HTTP_LENGTH
317 			HTTP_CLOSE
318 			"\r\n",
319-			HTTP_200, type, len
320+			HTTP_200, type, cors, len
321 		);
322 	}
323 
324@@ -526,7 +685,7 @@ int http_handle(int c)
325 	char hdr[HTTP_REQ_MAX];
326 
327 	if (read_request(c, method, sizeof(method), path, sizeof(path), hdr, sizeof(hdr)) < 0) {
328-		reply_text(c, HTTP_400, "bad request\n");
329+		reply_text(c, hdr, HTTP_400, "bad request\n");
330 		return -1;
331 	}
332 	LOG(verbose_log, "HTTP", "request: %s %s", method, path);
333@@ -538,15 +697,19 @@ int http_handle(int c)
334 	else if (strcmp(method, "HEAD") == 0) {
335 		head_only = 1;
336 	}
337+	else if (strcmp(method, "OPTIONS") == 0) {
338+		reply_preflight(c, hdr);
339+		return 0;
340+	}
341 	else {
342 		LOG(verbose_log, "HTTP", "method not allowed: %s", method);
343-		reply_text(c, HTTP_405, "method not allowed\n");
344+		reply_text(c, hdr, HTTP_405, "method not allowed\n");
345 		return 0;
346 	}
347 
348 	if (strcmp(path, "/ping") == 0) {
349 		LOG(verbose_log, "HTTP", "route /ping");
350-		reply_text(c, HTTP_200, "ok\n");
351+		reply_text(c, hdr, HTTP_200, "ok\n");
352 		return 0;
353 	}
354 
355@@ -556,12 +719,12 @@ int http_handle(int c)
356 
357 		if (json_library(&j, &lib) < 0) {
358 			LOG(verbose_log, "JSON", "encode failed");
359-			reply_text(c, HTTP_500, "json failed\n");
360+			reply_text(c, hdr, HTTP_500, "json failed\n");
361 			return -1;
362 		}
363 		LOG(verbose_log, "JSON", "encoded %zu bytes", j.len);
364 
365-		reply_json(c, HTTP_200, j.buf, j.len, !head_only);
366+		reply_json(c, hdr, HTTP_200, j.buf, j.len, !head_only);
367 		json_free(&j);
368 
369 		return 0;
370@@ -572,13 +735,13 @@ int http_handle(int c)
371 
372 		uint64_t id;
373 		if (parse_hex64(path + 8, &id) < 0) {
374-			reply_text(c, HTTP_400, "bad request\n");
375+			reply_text(c, hdr, HTTP_400, "bad request\n");
376 			return 0;
377 		}
378 
379 		const struct item* it = find_item(id);
380 		if (!it) {
381-			reply_text(c, HTTP_404, "not found\n");
382+			reply_text(c, hdr, HTTP_404, "not found\n");
383 			return 0;
384 		}
385 
386@@ -590,19 +753,19 @@ int http_handle(int c)
387 
388 		uint64_t id;
389 		if (parse_hex64(path + 6, &id) < 0) {
390-			reply_text(c, HTTP_400, "bad request\n");
391+			reply_text(c, hdr, HTTP_400, "bad request\n");
392 			return 0;
393 		}
394 
395 		const struct item* it = find_item(id);
396 		if (!it) {
397-			reply_text(c, HTTP_404, "not found\n");
398+			reply_text(c, hdr, HTTP_404, "not found\n");
399 			return 0;
400 		}
401 
402 		struct stat st;
403 		if (stat_item(it, &st) < 0) {
404-			reply_text(c, HTTP_404, "not found\n");
405+			reply_text(c, hdr, HTTP_404, "not found\n");
406 			return 0;
407 		}
408 
409@@ -610,18 +773,18 @@ int http_handle(int c)
410 
411 		struct json j;
412 		if (json_meta(&j, it, (size_t)st.st_size, type) < 0) {
413-			reply_text(c, HTTP_500, "json failed\n");
414+			reply_text(c, hdr, HTTP_500, "json failed\n");
415 			return -1;
416 		}
417 
418-		reply_json(c, HTTP_200, j.buf, j.len, !head_only);
419+		reply_json(c, hdr, HTTP_200, j.buf, j.len, !head_only);
420 		json_free(&j);
421 
422 		return 0;
423 	}
424 
425 	LOG(verbose_log, "HTTP", "route not found: %s", path);
426-	reply_text(c, HTTP_404, "not found\n");
427+	reply_text(c, hdr, HTTP_404, "not found\n");
428 
429 	return 0;
430 }
+1, -0
1@@ -7,6 +7,7 @@ static const char*  media_dir     = "/path/to/media";
2 static const char*  server_addr   = "0.0.0.0";
3 static const int    server_port   = 8088;
4 static const bool   verbose_log   = true;
5+static const char*  cors_origin   = "";
6 
7 enum {
8 	HTTP_REQ_MAX   = 2048,
+1, -0
1@@ -2,6 +2,7 @@
2 #define HTTP_H
3 
4 #define HTTP_200    "HTTP/1.1 200 OK\r\n"
5+#define HTTP_204    "HTTP/1.1 204 No Content\r\n"
6 #define HTTP_206    "HTTP/1.1 206 Partial Content\r\n"
7 #define HTTP_400    "HTTP/1.1 400 Bad Request\r\n"
8 #define HTTP_404    "HTTP/1.1 404 Not Found\r\n"