http.c - quark - quark web server | |
git clone git://git.suckless.org/quark | |
Log | |
Files | |
Refs | |
LICENSE | |
--- | |
http.c (23865B) | |
--- | |
1 /* See LICENSE file for copyright and license details. */ | |
2 #include <arpa/inet.h> | |
3 #include <ctype.h> | |
4 #include <errno.h> | |
5 #include <limits.h> | |
6 #include <netinet/in.h> | |
7 #include <regex.h> | |
8 #include <stddef.h> | |
9 #include <stdint.h> | |
10 #include <stdio.h> | |
11 #include <string.h> | |
12 #include <strings.h> | |
13 #include <sys/socket.h> | |
14 #include <sys/stat.h> | |
15 #include <sys/types.h> | |
16 #include <time.h> | |
17 #include <unistd.h> | |
18 | |
19 #include "config.h" | |
20 #include "http.h" | |
21 #include "util.h" | |
22 | |
23 const char *req_field_str[] = { | |
24 [REQ_HOST] = "Host", | |
25 [REQ_RANGE] = "Range", | |
26 [REQ_IF_MODIFIED_SINCE] = "If-Modified-Since", | |
27 }; | |
28 | |
29 const char *req_method_str[] = { | |
30 [M_GET] = "GET", | |
31 [M_HEAD] = "HEAD", | |
32 }; | |
33 | |
34 const char *status_str[] = { | |
35 [S_OK] = "OK", | |
36 [S_PARTIAL_CONTENT] = "Partial Content", | |
37 [S_MOVED_PERMANENTLY] = "Moved Permanently", | |
38 [S_NOT_MODIFIED] = "Not Modified", | |
39 [S_BAD_REQUEST] = "Bad Request", | |
40 [S_FORBIDDEN] = "Forbidden", | |
41 [S_NOT_FOUND] = "Not Found", | |
42 [S_METHOD_NOT_ALLOWED] = "Method Not Allowed", | |
43 [S_REQUEST_TIMEOUT] = "Request Time-out", | |
44 [S_RANGE_NOT_SATISFIABLE] = "Range Not Satisfiable", | |
45 [S_REQUEST_TOO_LARGE] = "Request Header Fields Too Large", | |
46 [S_INTERNAL_SERVER_ERROR] = "Internal Server Error", | |
47 [S_VERSION_NOT_SUPPORTED] = "HTTP Version not supported", | |
48 }; | |
49 | |
50 const char *res_field_str[] = { | |
51 [RES_ACCEPT_RANGES] = "Accept-Ranges", | |
52 [RES_ALLOW] = "Allow", | |
53 [RES_LOCATION] = "Location", | |
54 [RES_LAST_MODIFIED] = "Last-Modified", | |
55 [RES_CONTENT_LENGTH] = "Content-Length", | |
56 [RES_CONTENT_RANGE] = "Content-Range", | |
57 [RES_CONTENT_TYPE] = "Content-Type", | |
58 }; | |
59 | |
60 enum status | |
61 http_prepare_header_buf(const struct response *res, struct buffer *buf) | |
62 { | |
63 char tstmp[FIELD_MAX]; | |
64 size_t i; | |
65 | |
66 /* reset buffer */ | |
67 memset(buf, 0, sizeof(*buf)); | |
68 | |
69 /* generate timestamp */ | |
70 if (timestamp(tstmp, sizeof(tstmp), time(NULL))) { | |
71 goto err; | |
72 } | |
73 | |
74 /* write data */ | |
75 if (buffer_appendf(buf, | |
76 "HTTP/1.1 %d %s\r\n" | |
77 "Date: %s\r\n" | |
78 "Connection: close\r\n", | |
79 res->status, status_str[res->status], tstmp))… | |
80 goto err; | |
81 } | |
82 | |
83 for (i = 0; i < NUM_RES_FIELDS; i++) { | |
84 if (res->field[i][0] != '\0' && | |
85 buffer_appendf(buf, "%s: %s\r\n", res_field_str[i], | |
86 res->field[i])) { | |
87 goto err; | |
88 } | |
89 } | |
90 | |
91 if (buffer_appendf(buf, "\r\n")) { | |
92 goto err; | |
93 } | |
94 | |
95 return 0; | |
96 err: | |
97 memset(buf, 0, sizeof(*buf)); | |
98 return S_INTERNAL_SERVER_ERROR; | |
99 } | |
100 | |
101 enum status | |
102 http_send_buf(int fd, struct buffer *buf) | |
103 { | |
104 ssize_t r; | |
105 | |
106 if (buf == NULL) { | |
107 return S_INTERNAL_SERVER_ERROR; | |
108 } | |
109 | |
110 while (buf->len > 0) { | |
111 if ((r = write(fd, buf->data, buf->len)) <= 0) { | |
112 if (errno == EAGAIN || errno == EWOULDBLOCK) { | |
113 /* | |
114 * socket is blocking, return normally. | |
115 * given the buffer still contains data, | |
116 * this indicates to the caller that we | |
117 * have been interrupted. | |
118 */ | |
119 return 0; | |
120 } else { | |
121 return S_REQUEST_TIMEOUT; | |
122 } | |
123 } | |
124 memmove(buf->data, buf->data + r, buf->len - r); | |
125 buf->len -= r; | |
126 } | |
127 | |
128 return 0; | |
129 } | |
130 | |
131 static void | |
132 decode(const char src[PATH_MAX], char dest[PATH_MAX]) | |
133 { | |
134 size_t i; | |
135 uint8_t n; | |
136 const char *s; | |
137 | |
138 for (s = src, i = 0; *s; i++) { | |
139 if (*s == '%' && isxdigit((unsigned char)s[1]) && | |
140 isxdigit((unsigned char)s[2])) { | |
141 sscanf(s + 1, "%2hhx", &n); | |
142 dest[i] = n; | |
143 s += 3; | |
144 } else { | |
145 dest[i] = *s++; | |
146 } | |
147 } | |
148 dest[i] = '\0'; | |
149 } | |
150 | |
151 enum status | |
152 http_recv_header(int fd, struct buffer *buf, int *done) | |
153 { | |
154 enum status s; | |
155 ssize_t r; | |
156 | |
157 while (1) { | |
158 if ((r = read(fd, buf->data + buf->len, | |
159 sizeof(buf->data) - buf->len)) < 0) { | |
160 if (errno == EAGAIN || errno == EWOULDBLOCK) { | |
161 /* | |
162 * socket is drained, return normally, | |
163 * but set done to zero | |
164 */ | |
165 *done = 0; | |
166 return 0; | |
167 } else { | |
168 s = S_REQUEST_TIMEOUT; | |
169 goto err; | |
170 } | |
171 } else if (r == 0) { | |
172 /* | |
173 * unexpected EOF because the client probably | |
174 * hung up. This is technically a bad request, | |
175 * because it's incomplete | |
176 */ | |
177 s = S_BAD_REQUEST; | |
178 goto err; | |
179 } | |
180 buf->len += r; | |
181 | |
182 /* check if we are done (header terminated) */ | |
183 if (buf->len >= 4 && !memcmp(buf->data + buf->len - 4, | |
184 "\r\n\r\n", 4)) { | |
185 break; | |
186 } | |
187 | |
188 /* buffer is full or read over, but header is not termin… | |
189 if (r == 0 || buf->len == sizeof(buf->data)) { | |
190 s = S_REQUEST_TOO_LARGE; | |
191 goto err; | |
192 } | |
193 } | |
194 | |
195 /* header is complete, remove last \r\n and set done */ | |
196 buf->len -= 2; | |
197 *done = 1; | |
198 | |
199 return 0; | |
200 err: | |
201 memset(buf, 0, sizeof(*buf)); | |
202 return s; | |
203 } | |
204 | |
205 enum status | |
206 http_parse_header(const char *h, struct request *req) | |
207 { | |
208 struct in6_addr addr; | |
209 size_t i, mlen; | |
210 const char *p, *q, *r, *s, *t; | |
211 char *m, *n; | |
212 | |
213 /* empty the request struct */ | |
214 memset(req, 0, sizeof(*req)); | |
215 | |
216 /* | |
217 * parse request line | |
218 */ | |
219 | |
220 /* METHOD */ | |
221 for (i = 0; i < NUM_REQ_METHODS; i++) { | |
222 mlen = strlen(req_method_str[i]); | |
223 if (!strncmp(req_method_str[i], h, mlen)) { | |
224 req->method = i; | |
225 break; | |
226 } | |
227 } | |
228 if (i == NUM_REQ_METHODS) { | |
229 return S_METHOD_NOT_ALLOWED; | |
230 } | |
231 | |
232 /* a single space must follow the method */ | |
233 if (h[mlen] != ' ') { | |
234 return S_BAD_REQUEST; | |
235 } | |
236 | |
237 /* basis for next step */ | |
238 p = h + mlen + 1; | |
239 | |
240 /* RESOURCE */ | |
241 | |
242 /* | |
243 * path?query#fragment | |
244 * ^ ^ ^ ^ | |
245 * | | | | | |
246 * p r s q | |
247 * | |
248 */ | |
249 if (!(q = strchr(p, ' '))) { | |
250 return S_BAD_REQUEST; | |
251 } | |
252 | |
253 /* search for first '?' */ | |
254 for (r = p; r < q; r++) { | |
255 if (!isprint(*r)) { | |
256 return S_BAD_REQUEST; | |
257 } | |
258 if (*r == '?') { | |
259 break; | |
260 } | |
261 } | |
262 if (r == q) { | |
263 /* not found */ | |
264 r = NULL; | |
265 } | |
266 | |
267 /* search for first '#' */ | |
268 for (s = p; s < q; s++) { | |
269 if (!isprint(*s)) { | |
270 return S_BAD_REQUEST; | |
271 } | |
272 if (*s == '#') { | |
273 break; | |
274 } | |
275 } | |
276 if (s == q) { | |
277 /* not found */ | |
278 s = NULL; | |
279 } | |
280 | |
281 if (r != NULL && s != NULL && s < r) { | |
282 /* | |
283 * '#' comes before '?' and thus the '?' is literal, | |
284 * because the query must come before the fragment | |
285 */ | |
286 r = NULL; | |
287 } | |
288 | |
289 /* write path using temporary endpointer t */ | |
290 if (r != NULL) { | |
291 /* resource contains a query, path ends at r */ | |
292 t = r; | |
293 } else if (s != NULL) { | |
294 /* resource contains only a fragment, path ends at s */ | |
295 t = s; | |
296 } else { | |
297 /* resource contains no queries, path ends at q */ | |
298 t = q; | |
299 } | |
300 if ((size_t)(t - p + 1) > LEN(req->path)) { | |
301 return S_REQUEST_TOO_LARGE; | |
302 } | |
303 memcpy(req->path, p, t - p); | |
304 req->path[t - p] = '\0'; | |
305 decode(req->path, req->path); | |
306 | |
307 /* write query if present */ | |
308 if (r != NULL) { | |
309 /* query ends either at s (if fragment present) or q */ | |
310 t = (s != NULL) ? s : q; | |
311 | |
312 if ((size_t)(t - (r + 1) + 1) > LEN(req->query)) { | |
313 return S_REQUEST_TOO_LARGE; | |
314 } | |
315 memcpy(req->query, r + 1, t - (r + 1)); | |
316 req->query[t - (r + 1)] = '\0'; | |
317 } | |
318 | |
319 /* write fragment if present */ | |
320 if (s != NULL) { | |
321 /* the fragment always starts at s + 1 and ends at q */ | |
322 if ((size_t)(q - (s + 1) + 1) > LEN(req->fragment)) { | |
323 return S_REQUEST_TOO_LARGE; | |
324 } | |
325 memcpy(req->fragment, s + 1, q - (s + 1)); | |
326 req->fragment[q - (s + 1)] = '\0'; | |
327 } | |
328 | |
329 /* basis for next step */ | |
330 p = q + 1; | |
331 | |
332 /* HTTP-VERSION */ | |
333 if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) { | |
334 return S_BAD_REQUEST; | |
335 } | |
336 p += sizeof("HTTP/") - 1; | |
337 if (strncmp(p, "1.0", sizeof("1.0") - 1) && | |
338 strncmp(p, "1.1", sizeof("1.1") - 1)) { | |
339 return S_VERSION_NOT_SUPPORTED; | |
340 } | |
341 p += sizeof("1.*") - 1; | |
342 | |
343 /* check terminator */ | |
344 if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) { | |
345 return S_BAD_REQUEST; | |
346 } | |
347 | |
348 /* basis for next step */ | |
349 p += sizeof("\r\n") - 1; | |
350 | |
351 /* | |
352 * parse request-fields | |
353 */ | |
354 | |
355 /* match field type */ | |
356 for (; *p != '\0';) { | |
357 for (i = 0; i < NUM_REQ_FIELDS; i++) { | |
358 if (!strncasecmp(p, req_field_str[i], | |
359 strlen(req_field_str[i]))) { | |
360 break; | |
361 } | |
362 } | |
363 if (i == NUM_REQ_FIELDS) { | |
364 /* unmatched field, skip this line */ | |
365 if (!(q = strstr(p, "\r\n"))) { | |
366 return S_BAD_REQUEST; | |
367 } | |
368 p = q + (sizeof("\r\n") - 1); | |
369 continue; | |
370 } | |
371 | |
372 p += strlen(req_field_str[i]); | |
373 | |
374 /* a single colon must follow the field name */ | |
375 if (*p != ':') { | |
376 return S_BAD_REQUEST; | |
377 } | |
378 | |
379 /* skip whitespace */ | |
380 for (++p; *p == ' ' || *p == '\t'; p++) | |
381 ; | |
382 | |
383 /* extract field content */ | |
384 if (!(q = strstr(p, "\r\n"))) { | |
385 return S_BAD_REQUEST; | |
386 } | |
387 if ((size_t)(q - p + 1) > LEN(req->field[i])) { | |
388 return S_REQUEST_TOO_LARGE; | |
389 } | |
390 memcpy(req->field[i], p, q - p); | |
391 req->field[i][q - p] = '\0'; | |
392 | |
393 /* go to next line */ | |
394 p = q + (sizeof("\r\n") - 1); | |
395 } | |
396 | |
397 /* | |
398 * clean up host | |
399 */ | |
400 | |
401 m = strrchr(req->field[REQ_HOST], ':'); | |
402 n = strrchr(req->field[REQ_HOST], ']'); | |
403 | |
404 /* strip port suffix but don't interfere with IPv6 bracket notat… | |
405 * as per RFC 2732 */ | |
406 if (m && (!n || m > n)) { | |
407 /* port suffix must not be empty */ | |
408 if (*(m + 1) == '\0') { | |
409 return S_BAD_REQUEST; | |
410 } | |
411 *m = '\0'; | |
412 } | |
413 | |
414 /* strip the brackets from the IPv6 notation and validate the ad… | |
415 if (n) { | |
416 /* brackets must be on the outside */ | |
417 if (req->field[REQ_HOST][0] != '[' || *(n + 1) != '\0') { | |
418 return S_BAD_REQUEST; | |
419 } | |
420 | |
421 /* remove the right bracket */ | |
422 *n = '\0'; | |
423 m = req->field[REQ_HOST] + 1; | |
424 | |
425 /* validate the contained IPv6 address */ | |
426 if (inet_pton(AF_INET6, m, &addr) != 1) { | |
427 return S_BAD_REQUEST; | |
428 } | |
429 | |
430 /* copy it into the host field */ | |
431 memmove(req->field[REQ_HOST], m, n - m + 1); | |
432 } | |
433 | |
434 return 0; | |
435 } | |
436 | |
437 static void | |
438 encode(const char src[PATH_MAX], char dest[PATH_MAX]) | |
439 { | |
440 size_t i; | |
441 const char *s; | |
442 | |
443 for (s = src, i = 0; *s && i < (PATH_MAX - 4); s++) { | |
444 if (iscntrl(*s) || (unsigned char)*s > 127) { | |
445 i += snprintf(dest + i, PATH_MAX - i, "%%%02X", | |
446 (unsigned char)*s); | |
447 } else { | |
448 dest[i] = *s; | |
449 i++; | |
450 } | |
451 } | |
452 dest[i] = '\0'; | |
453 } | |
454 | |
455 static enum status | |
456 path_normalize(char *uri, int *redirect) | |
457 { | |
458 size_t len; | |
459 int last = 0; | |
460 char *p, *q; | |
461 | |
462 /* require and skip first slash */ | |
463 if (uri[0] != '/') { | |
464 return S_BAD_REQUEST; | |
465 } | |
466 p = uri + 1; | |
467 | |
468 /* get length of URI */ | |
469 len = strlen(p); | |
470 | |
471 for (; !last; ) { | |
472 /* bound uri component within (p,q) */ | |
473 if (!(q = strchr(p, '/'))) { | |
474 q = strchr(p, '\0'); | |
475 last = 1; | |
476 } | |
477 | |
478 if (*p == '\0') { | |
479 break; | |
480 } else if (p == q || (q - p == 1 && p[0] == '.')) { | |
481 /* "/" or "./" */ | |
482 goto squash; | |
483 } else if (q - p == 2 && p[0] == '.' && p[1] == '.') { | |
484 /* "../" */ | |
485 if (p != uri + 1) { | |
486 /* place p right after the previous / */ | |
487 for (p -= 2; p > uri && *p != '/'; p--); | |
488 p++; | |
489 } | |
490 goto squash; | |
491 } else { | |
492 /* move on */ | |
493 p = q + 1; | |
494 continue; | |
495 } | |
496 squash: | |
497 /* squash (p,q) into void */ | |
498 if (last) { | |
499 *p = '\0'; | |
500 len = p - uri; | |
501 } else { | |
502 memmove(p, q + 1, len - ((q + 1) - uri) + 2); | |
503 len -= (q + 1) - p; | |
504 } | |
505 if (redirect != NULL) { | |
506 *redirect = 1; | |
507 } | |
508 } | |
509 | |
510 return 0; | |
511 } | |
512 | |
513 static enum status | |
514 path_add_vhost_prefix(char uri[PATH_MAX], int *redirect, | |
515 const struct server *srv, const struct response *re… | |
516 { | |
517 if (srv->vhost && res->vhost && res->vhost->prefix) { | |
518 if (prepend(uri, PATH_MAX, res->vhost->prefix)) { | |
519 return S_REQUEST_TOO_LARGE; | |
520 } | |
521 if (redirect != NULL) { | |
522 *redirect = 1; | |
523 } | |
524 } | |
525 | |
526 return 0; | |
527 } | |
528 | |
529 static enum status | |
530 path_apply_prefix_mapping(char uri[PATH_MAX], int *redirect, | |
531 const struct server *srv, const struct response… | |
532 { | |
533 size_t i, len; | |
534 | |
535 for (i = 0; i < srv->map_len; i++) { | |
536 len = strlen(srv->map[i].from); | |
537 if (!strncmp(uri, srv->map[i].from, len)) { | |
538 /* | |
539 * if vhosts are enabled only apply mappings | |
540 * defined for the current canonical host | |
541 */ | |
542 if (srv->vhost && res->vhost && srv->map[i].chos… | |
543 strcmp(srv->map[i].chost, res->vhost->chost)… | |
544 continue; | |
545 } | |
546 | |
547 /* swap out URI prefix */ | |
548 memmove(uri, uri + len, strlen(uri) + 1); | |
549 if (prepend(uri, PATH_MAX, srv->map[i].to)) { | |
550 return S_REQUEST_TOO_LARGE; | |
551 } | |
552 | |
553 if (redirect != NULL) { | |
554 *redirect = 1; | |
555 } | |
556 | |
557 /* break so we don't possibly hit an infinite lo… | |
558 break; | |
559 } | |
560 } | |
561 | |
562 return 0; | |
563 } | |
564 | |
565 static enum status | |
566 path_ensure_dirslash(char uri[PATH_MAX], int *redirect) | |
567 { | |
568 size_t len; | |
569 | |
570 /* append '/' to URI if not present */ | |
571 len = strlen(uri); | |
572 if (len + 1 + 1 > PATH_MAX) { | |
573 return S_REQUEST_TOO_LARGE; | |
574 } | |
575 if (len > 0 && uri[len - 1] != '/') { | |
576 uri[len] = '/'; | |
577 uri[len + 1] = '\0'; | |
578 if (redirect != NULL) { | |
579 *redirect = 1; | |
580 } | |
581 } | |
582 | |
583 return 0; | |
584 } | |
585 | |
586 static enum status | |
587 parse_range(const char *str, size_t size, size_t *lower, size_t *upper) | |
588 { | |
589 char first[FIELD_MAX], last[FIELD_MAX]; | |
590 const char *p, *q, *r, *err; | |
591 | |
592 /* default to the complete range */ | |
593 *lower = 0; | |
594 *upper = size - 1; | |
595 | |
596 /* done if no range-string is given */ | |
597 if (str == NULL || *str == '\0') { | |
598 return 0; | |
599 } | |
600 | |
601 /* skip opening statement */ | |
602 if (strncmp(str, "bytes=", sizeof("bytes=") - 1)) { | |
603 return S_BAD_REQUEST; | |
604 } | |
605 p = str + (sizeof("bytes=") - 1); | |
606 | |
607 /* check string (should only contain numbers and a hyphen) */ | |
608 for (r = p, q = NULL; *r != '\0'; r++) { | |
609 if (*r < '0' || *r > '9') { | |
610 if (*r == '-') { | |
611 if (q != NULL) { | |
612 /* we have already seen a hyphen… | |
613 return S_BAD_REQUEST; | |
614 } else { | |
615 /* place q after the hyphen */ | |
616 q = r + 1; | |
617 } | |
618 } else if (*r == ',' && r > p) { | |
619 /* | |
620 * we refuse to accept range-lists out | |
621 * of spite towards this horrible part | |
622 * of the spec | |
623 */ | |
624 return S_RANGE_NOT_SATISFIABLE; | |
625 } else { | |
626 return S_BAD_REQUEST; | |
627 } | |
628 } | |
629 } | |
630 if (q == NULL) { | |
631 /* the input string must contain a hyphen */ | |
632 return S_BAD_REQUEST; | |
633 } | |
634 r = q + strlen(q); | |
635 | |
636 /* | |
637 * byte-range=first-last\0 | |
638 * ^ ^ ^ | |
639 * | | | | |
640 * p q r | |
641 */ | |
642 | |
643 /* copy 'first' and 'last' to their respective arrays */ | |
644 if ((size_t)((q - 1) - p + 1) > sizeof(first) || | |
645 (size_t)(r - q + 1) > sizeof(last)) { | |
646 return S_REQUEST_TOO_LARGE; | |
647 } | |
648 memcpy(first, p, (q - 1) - p); | |
649 first[(q - 1) - p] = '\0'; | |
650 memcpy(last, q, r - q); | |
651 last[r - q] = '\0'; | |
652 | |
653 if (first[0] != '\0') { | |
654 /* | |
655 * range has format "first-last" or "first-", | |
656 * i.e. return bytes 'first' to 'last' (or the | |
657 * last byte if 'last' is not given), | |
658 * inclusively, and byte-numbering beginning at 0 | |
659 */ | |
660 *lower = strtonum(first, 0, MIN(SIZE_MAX, LLONG_MAX), | |
661 &err); | |
662 if (!err) { | |
663 if (last[0] != '\0') { | |
664 *upper = strtonum(last, 0, | |
665 MIN(SIZE_MAX, LLONG_MA… | |
666 &err); | |
667 } else { | |
668 *upper = size - 1; | |
669 } | |
670 } | |
671 if (err) { | |
672 /* one of the strtonum()'s failed */ | |
673 return S_BAD_REQUEST; | |
674 } | |
675 | |
676 /* check ranges */ | |
677 if (*lower > *upper || *lower >= size) { | |
678 return S_RANGE_NOT_SATISFIABLE; | |
679 } | |
680 | |
681 /* adjust upper limit to be at most the last byte */ | |
682 *upper = MIN(*upper, size - 1); | |
683 } else { | |
684 /* last must not also be empty */ | |
685 if (last[0] == '\0') { | |
686 return S_BAD_REQUEST; | |
687 } | |
688 | |
689 /* | |
690 * Range has format "-num", i.e. return the 'num' | |
691 * last bytes | |
692 */ | |
693 | |
694 /* | |
695 * use upper as a temporary storage for 'num', | |
696 * as we know 'upper' is size - 1 | |
697 */ | |
698 *upper = strtonum(last, 0, MIN(SIZE_MAX, LLONG_MAX), &er… | |
699 if (err) { | |
700 return S_BAD_REQUEST; | |
701 } | |
702 | |
703 /* determine lower */ | |
704 if (*upper > size) { | |
705 /* more bytes requested than we have */ | |
706 *lower = 0; | |
707 } else { | |
708 *lower = size - *upper; | |
709 } | |
710 | |
711 /* set upper to the correct value */ | |
712 *upper = size - 1; | |
713 } | |
714 | |
715 return 0; | |
716 } | |
717 | |
718 void | |
719 http_prepare_response(const struct request *req, struct response *res, | |
720 const struct server *srv) | |
721 { | |
722 enum status s, tmps; | |
723 struct in6_addr addr; | |
724 struct stat st; | |
725 struct tm tm = { 0 }; | |
726 size_t i; | |
727 int redirect, hasport, ipv6host; | |
728 static char tmppath[PATH_MAX]; | |
729 char *p, *mime; | |
730 | |
731 /* empty all response fields */ | |
732 memset(res, 0, sizeof(*res)); | |
733 | |
734 /* determine virtual host */ | |
735 if (srv->vhost) { | |
736 for (i = 0; i < srv->vhost_len; i++) { | |
737 if (!regexec(&(srv->vhost[i].re), | |
738 req->field[REQ_HOST], 0, NULL, 0)) { | |
739 /* we have a matching vhost */ | |
740 res->vhost = &(srv->vhost[i]); | |
741 break; | |
742 } | |
743 } | |
744 if (i == srv->vhost_len) { | |
745 s = S_NOT_FOUND; | |
746 goto err; | |
747 } | |
748 } | |
749 | |
750 /* copy request-path to response-path and clean it up */ | |
751 redirect = 0; | |
752 memcpy(res->path, req->path, MIN(sizeof(res->path), sizeof(req->… | |
753 if ((tmps = path_normalize(res->path, &redirect)) || | |
754 (tmps = path_add_vhost_prefix(res->path, &redirect, srv, res… | |
755 (tmps = path_apply_prefix_mapping(res->path, &redirect, srv,… | |
756 (tmps = path_normalize(res->path, &redirect))) { | |
757 s = tmps; | |
758 goto err; | |
759 } | |
760 | |
761 /* redirect all non-canonical hosts to their canonical forms */ | |
762 if (srv->vhost && res->vhost && | |
763 strcmp(req->field[REQ_HOST], res->vhost->chost)) { | |
764 redirect = 1; | |
765 } | |
766 | |
767 /* reject all non-well-known hidden targets (see RFC 8615) */ | |
768 if (strstr(res->path, "/.") && strncmp(res->path, "/.well-known/… | |
769 sizeof("/.well-known/") - 1)) { | |
770 s = S_FORBIDDEN; | |
771 goto err; | |
772 } | |
773 | |
774 /* | |
775 * generate and stat internal path based on the cleaned up reque… | |
776 * path and the virtual host while ignoring query and fragment | |
777 * (valid according to RFC 3986) | |
778 */ | |
779 if (esnprintf(res->internal_path, sizeof(res->internal_path), "/… | |
780 (srv->vhost && res->vhost) ? res->vhost->dir : "", | |
781 res->path)) { | |
782 s = S_REQUEST_TOO_LARGE; | |
783 goto err; | |
784 } | |
785 if ((tmps = path_normalize(res->internal_path, NULL))) { | |
786 s = tmps; | |
787 goto err; | |
788 } | |
789 if (stat(res->internal_path, &st) < 0) { | |
790 s = (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND; | |
791 goto err; | |
792 } | |
793 | |
794 /* | |
795 * if the path points at a directory, make sure both the path | |
796 * and internal path have a trailing slash | |
797 */ | |
798 if (S_ISDIR(st.st_mode)) { | |
799 if ((tmps = path_ensure_dirslash(res->path, &redirect)) … | |
800 (tmps = path_ensure_dirslash(res->internal_path, NUL… | |
801 s = tmps; | |
802 goto err; | |
803 } | |
804 } | |
805 | |
806 /* redirect if the path-cleanup necessitated it earlier */ | |
807 if (redirect) { | |
808 res->status = S_MOVED_PERMANENTLY; | |
809 | |
810 /* encode path */ | |
811 encode(res->path, tmppath); | |
812 | |
813 /* determine target location */ | |
814 if (srv->vhost && res->vhost) { | |
815 /* absolute redirection URL */ | |
816 | |
817 /* do we need to add a port to the Location? */ | |
818 hasport = srv->port && strcmp(srv->port, "80"); | |
819 | |
820 /* RFC 2732 specifies to use brackets for IPv6-a… | |
821 * in URLs, so we need to check if our host is o… | |
822 * honor that later when we fill the "Location"-… | |
823 if ((ipv6host = inet_pton(AF_INET6, res->vhost->… | |
824 &addr)) < 0) { | |
825 s = S_INTERNAL_SERVER_ERROR; | |
826 goto err; | |
827 } | |
828 | |
829 /* | |
830 * write location to response struct (re-includi… | |
831 * the query and fragment, if present) | |
832 */ | |
833 if (esnprintf(res->field[RES_LOCATION], | |
834 sizeof(res->field[RES_LOCATION]), | |
835 "//%s%s%s%s%s%s%s%s%s%s", | |
836 ipv6host ? "[" : "", | |
837 res->vhost->chost, | |
838 ipv6host ? "]" : "", | |
839 hasport ? ":" : "", | |
840 hasport ? srv->port : "", | |
841 tmppath, | |
842 req->query[0] ? "?" : "", | |
843 req->query, | |
844 req->fragment[0] ? "#" : "", | |
845 req->fragment)) { | |
846 s = S_REQUEST_TOO_LARGE; | |
847 goto err; | |
848 } | |
849 } else { | |
850 /* | |
851 * write relative redirection URI to response st… | |
852 * (re-including the query and fragment, if pres… | |
853 */ | |
854 if (esnprintf(res->field[RES_LOCATION], | |
855 sizeof(res->field[RES_LOCATION]), | |
856 "%s%s%s%s%s", | |
857 tmppath, | |
858 req->query[0] ? "?" : "", | |
859 req->query, | |
860 req->fragment[0] ? "#" : "", | |
861 req->fragment)) { | |
862 s = S_REQUEST_TOO_LARGE; | |
863 goto err; | |
864 } | |
865 } | |
866 | |
867 return; | |
868 } | |
869 | |
870 if (S_ISDIR(st.st_mode)) { | |
871 /* | |
872 * when we serve a directory, we first check if there | |
873 * exists a directory index. If not, we either make | |
874 * a directory listing (if enabled) or send an error | |
875 */ | |
876 | |
877 /* | |
878 * append docindex to internal_path temporarily | |
879 * (internal_path is guaranteed to end with '/') | |
880 */ | |
881 if (esnprintf(tmppath, sizeof(tmppath), "%s%s", | |
882 res->internal_path, srv->docindex)) { | |
883 s = S_REQUEST_TOO_LARGE; | |
884 goto err; | |
885 } | |
886 | |
887 /* stat the temporary path, which must be a regular file… | |
888 if (stat(tmppath, &st) < 0 || !S_ISREG(st.st_mode)) { | |
889 if (srv->listdirs) { | |
890 /* serve directory listing */ | |
891 | |
892 /* check if directory is accessible */ | |
893 if (access(res->internal_path, R_OK) != … | |
894 s = S_FORBIDDEN; | |
895 goto err; | |
896 } else { | |
897 res->status = S_OK; | |
898 } | |
899 res->type = RESTYPE_DIRLISTING; | |
900 | |
901 if (esnprintf(res->field[RES_CONTENT_TYP… | |
902 sizeof(res->field[RES_CONT… | |
903 "%s", "text/html; charset=… | |
904 s = S_INTERNAL_SERVER_ERROR; | |
905 goto err; | |
906 } | |
907 | |
908 return; | |
909 } else { | |
910 /* reject */ | |
911 s = (!S_ISREG(st.st_mode) || errno == EA… | |
912 S_FORBIDDEN : S_NOT_FOUND; | |
913 goto err; | |
914 } | |
915 } else { | |
916 /* the docindex exists; copy tmppath to internal… | |
917 if (esnprintf(res->internal_path, | |
918 sizeof(res->internal_path), "%s", | |
919 tmppath)) { | |
920 s = S_REQUEST_TOO_LARGE; | |
921 goto err; | |
922 } | |
923 } | |
924 } | |
925 | |
926 /* modified since */ | |
927 if (req->field[REQ_IF_MODIFIED_SINCE][0]) { | |
928 /* parse field */ | |
929 if (!strptime(req->field[REQ_IF_MODIFIED_SINCE], | |
930 "%a, %d %b %Y %T GMT", &tm)) { | |
931 s = S_BAD_REQUEST; | |
932 goto err; | |
933 } | |
934 | |
935 /* compare with last modification date of the file */ | |
936 if (difftime(st.st_mtim.tv_sec, timegm(&tm)) <= 0) { | |
937 res->status = S_NOT_MODIFIED; | |
938 return; | |
939 } | |
940 } | |
941 | |
942 /* range */ | |
943 if ((s = parse_range(req->field[REQ_RANGE], st.st_size, | |
944 &(res->file.lower), &(res->file.upper)))) { | |
945 if (s == S_RANGE_NOT_SATISFIABLE) { | |
946 res->status = S_RANGE_NOT_SATISFIABLE; | |
947 | |
948 if (esnprintf(res->field[RES_CONTENT_RANGE], | |
949 sizeof(res->field[RES_CONTENT_RANG… | |
950 "bytes */%zu", st.st_size)) { | |
951 s = S_INTERNAL_SERVER_ERROR; | |
952 goto err; | |
953 } | |
954 | |
955 return; | |
956 } else { | |
957 goto err; | |
958 } | |
959 } | |
960 | |
961 /* mime */ | |
962 mime = "application/octet-stream"; | |
963 if ((p = strrchr(res->internal_path, '.'))) { | |
964 for (i = 0; i < LEN(mimes); i++) { | |
965 if (!strcmp(mimes[i].ext, p + 1)) { | |
966 mime = mimes[i].type; | |
967 break; | |
968 } | |
969 } | |
970 } | |
971 | |
972 /* fill response struct */ | |
973 res->type = RESTYPE_FILE; | |
974 | |
975 /* check if file is readable */ | |
976 res->status = (access(res->internal_path, R_OK)) ? S_FORBIDDEN : | |
977 (req->field[REQ_RANGE][0] != '\0') ? | |
978 S_PARTIAL_CONTENT : S_OK; | |
979 | |
980 if (esnprintf(res->field[RES_ACCEPT_RANGES], | |
981 sizeof(res->field[RES_ACCEPT_RANGES]), | |
982 "%s", "bytes")) { | |
983 s = S_INTERNAL_SERVER_ERROR; | |
984 goto err; | |
985 } | |
986 | |
987 if (esnprintf(res->field[RES_CONTENT_LENGTH], | |
988 sizeof(res->field[RES_CONTENT_LENGTH]), | |
989 "%zu", res->file.upper - res->file.lower + 1)) { | |
990 s = S_INTERNAL_SERVER_ERROR; | |
991 goto err; | |
992 } | |
993 if (req->field[REQ_RANGE][0] != '\0') { | |
994 if (esnprintf(res->field[RES_CONTENT_RANGE], | |
995 sizeof(res->field[RES_CONTENT_RANGE]), | |
996 "bytes %zd-%zd/%zu", res->file.lower, | |
997 res->file.upper, st.st_size)) { | |
998 s = S_INTERNAL_SERVER_ERROR; | |
999 goto err; | |
1000 } | |
1001 } | |
1002 if (esnprintf(res->field[RES_CONTENT_TYPE], | |
1003 sizeof(res->field[RES_CONTENT_TYPE]), | |
1004 "%s", mime)) { | |
1005 s = S_INTERNAL_SERVER_ERROR; | |
1006 goto err; | |
1007 } | |
1008 if (timestamp(res->field[RES_LAST_MODIFIED], | |
1009 sizeof(res->field[RES_LAST_MODIFIED]), | |
1010 st.st_mtim.tv_sec)) { | |
1011 s = S_INTERNAL_SERVER_ERROR; | |
1012 goto err; | |
1013 } | |
1014 | |
1015 return; | |
1016 err: | |
1017 http_prepare_error_response(req, res, s); | |
1018 } | |
1019 | |
1020 void | |
1021 http_prepare_error_response(const struct request *req, | |
1022 struct response *res, enum status s) | |
1023 { | |
1024 /* used later */ | |
1025 (void)req; | |
1026 | |
1027 /* empty all response fields */ | |
1028 memset(res, 0, sizeof(*res)); | |
1029 | |
1030 res->type = RESTYPE_ERROR; | |
1031 res->status = s; | |
1032 | |
1033 if (esnprintf(res->field[RES_CONTENT_TYPE], | |
1034 sizeof(res->field[RES_CONTENT_TYPE]), | |
1035 "text/html; charset=utf-8")) { | |
1036 res->status = S_INTERNAL_SERVER_ERROR; | |
1037 } | |
1038 | |
1039 if (res->status == S_METHOD_NOT_ALLOWED) { | |
1040 if (esnprintf(res->field[RES_ALLOW], | |
1041 sizeof(res->field[RES_ALLOW]), | |
1042 "Allow: GET, HEAD")) { | |
1043 res->status = S_INTERNAL_SERVER_ERROR; | |
1044 } | |
1045 } | |
1046 } |