quark

quark web server
git clone git://git.suckless.org/quark
Log | Files | Refs | LICENSE

http.c (14754B)


      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 "resp.h"
     22 #include "util.h"
     23 
     24 const char *req_field_str[] = {
     25 	[REQ_HOST]    = "Host",
     26 	[REQ_RANGE]   = "Range",
     27 	[REQ_MOD]     = "If-Modified-Since",
     28 };
     29 
     30 const char *req_method_str[] = {
     31 	[M_GET]  = "GET",
     32 	[M_HEAD] = "HEAD",
     33 };
     34 
     35 const char *status_str[] = {
     36 	[S_OK]                    = "OK",
     37 	[S_PARTIAL_CONTENT]       = "Partial Content",
     38 	[S_MOVED_PERMANENTLY]     = "Moved Permanently",
     39 	[S_NOT_MODIFIED]          = "Not Modified",
     40 	[S_BAD_REQUEST]           = "Bad Request",
     41 	[S_FORBIDDEN]             = "Forbidden",
     42 	[S_NOT_FOUND]             = "Not Found",
     43 	[S_METHOD_NOT_ALLOWED]    = "Method Not Allowed",
     44 	[S_REQUEST_TIMEOUT]       = "Request Time-out",
     45 	[S_RANGE_NOT_SATISFIABLE] = "Range Not Satisfiable",
     46 	[S_REQUEST_TOO_LARGE]     = "Request Header Fields Too Large",
     47 	[S_INTERNAL_SERVER_ERROR] = "Internal Server Error",
     48 	[S_VERSION_NOT_SUPPORTED] = "HTTP Version not supported",
     49 };
     50 
     51 enum status
     52 http_send_status(int fd, enum status s)
     53 {
     54 	static char t[TIMESTAMP_LEN];
     55 
     56 	if (dprintf(fd,
     57 	            "HTTP/1.1 %d %s\r\n"
     58 	            "Date: %s\r\n"
     59 	            "Connection: close\r\n"
     60 	            "%s"
     61 	            "Content-Type: text/html; charset=utf-8\r\n"
     62 	            "\r\n"
     63 	            "<!DOCTYPE html>\n<html>\n\t<head>\n"
     64 	            "\t\t<title>%d %s</title>\n\t</head>\n\t<body>\n"
     65 	            "\t\t<h1>%d %s</h1>\n\t</body>\n</html>\n",
     66 	            s, status_str[s], timestamp(time(NULL), t),
     67 	            (s == S_METHOD_NOT_ALLOWED) ? "Allow: HEAD, GET\r\n" : "",
     68 	            s, status_str[s], s, status_str[s]) < 0) {
     69 		return S_REQUEST_TIMEOUT;
     70 	}
     71 
     72 	return s;
     73 }
     74 
     75 static void
     76 decode(char src[PATH_MAX], char dest[PATH_MAX])
     77 {
     78 	size_t i;
     79 	uint8_t n;
     80 	char *s;
     81 
     82 	for (s = src, i = 0; *s; s++, i++) {
     83 		if (*s == '%' && (sscanf(s + 1, "%2hhx", &n) == 1)) {
     84 			dest[i] = n;
     85 			s += 2;
     86 		} else {
     87 			dest[i] = *s;
     88 		}
     89 	}
     90 	dest[i] = '\0';
     91 }
     92 
     93 int
     94 http_get_request(int fd, struct request *r)
     95 {
     96 	struct in6_addr res;
     97 	size_t hlen, i, mlen;
     98 	ssize_t off;
     99 	char h[HEADER_MAX], *p, *q;
    100 
    101 	/* empty all fields */
    102 	memset(r, 0, sizeof(*r));
    103 
    104 	/*
    105 	 * receive header
    106 	 */
    107 	for (hlen = 0; ;) {
    108 		if ((off = read(fd, h + hlen, sizeof(h) - hlen)) < 0) {
    109 			return http_send_status(fd, S_REQUEST_TIMEOUT);
    110 		} else if (off == 0) {
    111 			break;
    112 		}
    113 		hlen += off;
    114 		if (hlen >= 4 && !memcmp(h + hlen - 4, "\r\n\r\n", 4)) {
    115 			break;
    116 		}
    117 		if (hlen == sizeof(h)) {
    118 			return http_send_status(fd, S_REQUEST_TOO_LARGE);
    119 		}
    120 	}
    121 
    122 	/* remove terminating empty line */
    123 	if (hlen < 2) {
    124 		return http_send_status(fd, S_BAD_REQUEST);
    125 	}
    126 	hlen -= 2;
    127 
    128 	/* null-terminate the header */
    129 	h[hlen] = '\0';
    130 
    131 	/*
    132 	 * parse request line
    133 	 */
    134 
    135 	/* METHOD */
    136 	for (i = 0; i < NUM_REQ_METHODS; i++) {
    137 		mlen = strlen(req_method_str[i]);
    138 		if (!strncmp(req_method_str[i], h, mlen)) {
    139 			r->method = i;
    140 			break;
    141 		}
    142 	}
    143 	if (i == NUM_REQ_METHODS) {
    144 		return http_send_status(fd, S_METHOD_NOT_ALLOWED);
    145 	}
    146 
    147 	/* a single space must follow the method */
    148 	if (h[mlen] != ' ') {
    149 		return http_send_status(fd, S_BAD_REQUEST);
    150 	}
    151 
    152 	/* basis for next step */
    153 	p = h + mlen + 1;
    154 
    155 	/* TARGET */
    156 	if (!(q = strchr(p, ' '))) {
    157 		return http_send_status(fd, S_BAD_REQUEST);
    158 	}
    159 	*q = '\0';
    160 	if (q - p + 1 > PATH_MAX) {
    161 		return http_send_status(fd, S_REQUEST_TOO_LARGE);
    162 	}
    163 	memcpy(r->target, p, q - p + 1);
    164 	decode(r->target, r->target);
    165 
    166 	/* basis for next step */
    167 	p = q + 1;
    168 
    169 	/* HTTP-VERSION */
    170 	if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) {
    171 		return http_send_status(fd, S_BAD_REQUEST);
    172 	}
    173 	p += sizeof("HTTP/") - 1;
    174 	if (strncmp(p, "1.0", sizeof("1.0") - 1) &&
    175 	    strncmp(p, "1.1", sizeof("1.1") - 1)) {
    176 		return http_send_status(fd, S_VERSION_NOT_SUPPORTED);
    177 	}
    178 	p += sizeof("1.*") - 1;
    179 
    180 	/* check terminator */
    181 	if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) {
    182 		return http_send_status(fd, S_BAD_REQUEST);
    183 	}
    184 
    185 	/* basis for next step */
    186 	p += sizeof("\r\n") - 1;
    187 
    188 	/*
    189 	 * parse request-fields
    190 	 */
    191 
    192 	/* match field type */
    193 	for (; *p != '\0';) {
    194 		for (i = 0; i < NUM_REQ_FIELDS; i++) {
    195 			if (!strncasecmp(p, req_field_str[i],
    196 			                 strlen(req_field_str[i]))) {
    197 				break;
    198 			}
    199 		}
    200 		if (i == NUM_REQ_FIELDS) {
    201 			/* unmatched field, skip this line */
    202 			if (!(q = strstr(p, "\r\n"))) {
    203 				return http_send_status(fd, S_BAD_REQUEST);
    204 			}
    205 			p = q + (sizeof("\r\n") - 1);
    206 			continue;
    207 		}
    208 
    209 		p += strlen(req_field_str[i]);
    210 
    211 		/* a single colon must follow the field name */
    212 		if (*p != ':') {
    213 			return http_send_status(fd, S_BAD_REQUEST);
    214 		}
    215 
    216 		/* skip whitespace */
    217 		for (++p; *p == ' ' || *p == '\t'; p++)
    218 			;
    219 
    220 		/* extract field content */
    221 		if (!(q = strstr(p, "\r\n"))) {
    222 			return http_send_status(fd, S_BAD_REQUEST);
    223 		}
    224 		*q = '\0';
    225 		if (q - p + 1 > FIELD_MAX) {
    226 			return http_send_status(fd, S_REQUEST_TOO_LARGE);
    227 		}
    228 		memcpy(r->field[i], p, q - p + 1);
    229 
    230 		/* go to next line */
    231 		p = q + (sizeof("\r\n") - 1);
    232 	}
    233 
    234 	/*
    235 	 * clean up host
    236 	 */
    237 
    238 	p = strrchr(r->field[REQ_HOST], ':');
    239 	q = strrchr(r->field[REQ_HOST], ']');
    240 
    241 	/* strip port suffix but don't interfere with IPv6 bracket notation
    242 	 * as per RFC 2732 */
    243 	if (p && (!q || p > q)) {
    244 		/* port suffix must not be empty */
    245 		if (*(p + 1) == '\0') {
    246 			return http_send_status(fd, S_BAD_REQUEST);
    247 		}
    248 		*p = '\0';
    249 	}
    250 
    251 	/* strip the brackets from the IPv6 notation and validate the address */
    252 	if (q) {
    253 		/* brackets must be on the outside */
    254 		if (r->field[REQ_HOST][0] != '[' || *(q + 1) != '\0') {
    255 			return http_send_status(fd, S_BAD_REQUEST);
    256 		}
    257 
    258 		/* remove the right bracket */
    259 		*q = '\0';
    260 		p = r->field[REQ_HOST] + 1;
    261 
    262 		/* validate the contained IPv6 address */
    263 		if (inet_pton(AF_INET6, p, &res) != 1) {
    264 			return http_send_status(fd, S_BAD_REQUEST);
    265 		}
    266 
    267 		/* copy it into the host field */
    268 		memmove(r->field[REQ_HOST], p, q - p + 1);
    269 	}
    270 
    271 	return 0;
    272 }
    273 
    274 static void
    275 encode(char src[PATH_MAX], char dest[PATH_MAX])
    276 {
    277 	size_t i;
    278 	char *s;
    279 
    280 	for (s = src, i = 0; *s && i < (PATH_MAX - 4); s++) {
    281 		if (iscntrl(*s) || (unsigned char)*s > 127) {
    282 			i += snprintf(dest + i, PATH_MAX - i, "%%%02X",
    283 			              (unsigned char)*s);
    284 		} else {
    285 			dest[i] = *s;
    286 			i++;
    287 		}
    288 	}
    289 	dest[i] = '\0';
    290 }
    291 
    292 static int
    293 normabspath(char *path)
    294 {
    295 	size_t len;
    296 	int last = 0;
    297 	char *p, *q;
    298 
    299 	/* require and skip first slash */
    300 	if (path[0] != '/') {
    301 		return 1;
    302 	}
    303 	p = path + 1;
    304 
    305 	/* get length of path */
    306 	len = strlen(p);
    307 
    308 	for (; !last; ) {
    309 		/* bound path component within (p,q) */
    310 		if (!(q = strchr(p, '/'))) {
    311 			q = strchr(p, '\0');
    312 			last = 1;
    313 		}
    314 
    315 		if (p == q || (q - p == 1 && p[0] == '.')) {
    316 			/* "/" or "./" */
    317 			goto squash;
    318 		} else if (q - p == 2 && p[0] == '.' && p[1] == '.') {
    319 			/* "../" */
    320 			if (p != path + 1) {
    321 				/* place p right after the previous / */
    322 				for (p -= 2; p > path && *p != '/'; p--);
    323 				p++;
    324 			}
    325 			goto squash;
    326 		} else {
    327 			/* move on */
    328 			p = q + 1;
    329 			continue;
    330 		}
    331 squash:
    332 		/* squash (p,q) into void */
    333 		if (last) {
    334 			*p = '\0';
    335 			len = p - path;
    336 		} else {
    337 			memmove(p, q + 1, len - ((q + 1) - path) + 2);
    338 			len -= (q + 1) - p;
    339 		}
    340 	}
    341 
    342 	return 0;
    343 }
    344 
    345 #undef RELPATH
    346 #define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1))
    347 
    348 enum status
    349 http_send_response(int fd, struct request *r)
    350 {
    351 	struct in6_addr res;
    352 	struct stat st;
    353 	struct tm tm;
    354 	size_t len, i;
    355 	off_t lower, upper;
    356 	int hasport, ipv6host;
    357 	static char realtarget[PATH_MAX], tmptarget[PATH_MAX], t[TIMESTAMP_LEN];
    358 	char *p, *q, *mime;
    359 	const char *vhostmatch, *targethost, *err;
    360 
    361 	/* make a working copy of the target */
    362 	memcpy(realtarget, r->target, sizeof(realtarget));
    363 
    364 	/* match vhost */
    365 	vhostmatch = NULL;
    366 	if (s.vhost) {
    367 		for (i = 0; i < s.vhost_len; i++) {
    368 			/* switch to vhost directory if there is a match */
    369 			if (!regexec(&s.vhost[i].re, r->field[REQ_HOST], 0,
    370 			             NULL, 0)) {
    371 				if (chdir(s.vhost[i].dir) < 0) {
    372 					return http_send_status(fd, (errno == EACCES) ?
    373 					                        S_FORBIDDEN : S_NOT_FOUND);
    374 				}
    375 				vhostmatch = s.vhost[i].chost;
    376 				break;
    377 			}
    378 		}
    379 		if (i == s.vhost_len) {
    380 			return http_send_status(fd, S_NOT_FOUND);
    381 		}
    382 
    383 		/* if we have a vhost prefix, prepend it to the target */
    384 		if (s.vhost[i].prefix) {
    385 			if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s",
    386 			              s.vhost[i].prefix, realtarget)) {
    387 				return http_send_status(fd, S_REQUEST_TOO_LARGE);
    388 			}
    389 			memcpy(realtarget, tmptarget, sizeof(realtarget));
    390 		}
    391 	}
    392 
    393 	/* apply target prefix mapping */
    394 	for (i = 0; i < s.map_len; i++) {
    395 		len = strlen(s.map[i].from);
    396 		if (!strncmp(realtarget, s.map[i].from, len)) {
    397 			/* match canonical host if vhosts are enabled and
    398 			 * the mapping specifies a canonical host */
    399 			if (s.vhost && s.map[i].chost &&
    400 			    strcmp(s.map[i].chost, vhostmatch)) {
    401 				continue;
    402 			}
    403 
    404 			/* swap out target prefix */
    405 			if (esnprintf(tmptarget, sizeof(tmptarget), "%s%s",
    406 			              s.map[i].to, realtarget + len)) {
    407 				return http_send_status(fd, S_REQUEST_TOO_LARGE);
    408 			}
    409 			memcpy(realtarget, tmptarget, sizeof(realtarget));
    410 			break;
    411 		}
    412 	}
    413 
    414 	/* normalize target */
    415 	if (normabspath(realtarget)) {
    416 		return http_send_status(fd, S_BAD_REQUEST);
    417 	}
    418 
    419 	/* reject hidden target */
    420 	if (realtarget[0] == '.' || strstr(realtarget, "/.")) {
    421 		return http_send_status(fd, S_FORBIDDEN);
    422 	}
    423 
    424 	/* stat the target */
    425 	if (stat(RELPATH(realtarget), &st) < 0) {
    426 		return http_send_status(fd, (errno == EACCES) ?
    427 		                        S_FORBIDDEN : S_NOT_FOUND);
    428 	}
    429 
    430 	if (S_ISDIR(st.st_mode)) {
    431 		/* add / to target if not present */
    432 		len = strlen(realtarget);
    433 		if (len >= PATH_MAX - 2) {
    434 			return http_send_status(fd, S_REQUEST_TOO_LARGE);
    435 		}
    436 		if (len && realtarget[len - 1] != '/') {
    437 			realtarget[len] = '/';
    438 			realtarget[len + 1] = '\0';
    439 		}
    440 	}
    441 
    442 	/* redirect if targets differ, host is non-canonical or we prefixed */
    443 	if (strcmp(r->target, realtarget) || (s.vhost && vhostmatch &&
    444 	    strcmp(r->field[REQ_HOST], vhostmatch))) {
    445 		/* encode realtarget */
    446 		encode(realtarget, tmptarget);
    447 
    448 		/* send redirection header */
    449 		if (s.vhost) {
    450 			/* absolute redirection URL */
    451 			targethost = r->field[REQ_HOST][0] ? vhostmatch ?
    452 			             vhostmatch : r->field[REQ_HOST] : s.host ?
    453 			             s.host : "localhost";
    454 
    455 			/* do we need to add a port to the Location? */
    456 			hasport = s.port && strcmp(s.port, "80");
    457 
    458 			/* RFC 2732 specifies to use brackets for IPv6-addresses
    459 			 * in URLs, so we need to check if our host is one and
    460 			 * honor that later when we fill the "Location"-field */
    461 			if ((ipv6host = inet_pton(AF_INET6, targethost,
    462 			                          &res)) < 0) {
    463 				return http_send_status(fd,
    464 				                        S_INTERNAL_SERVER_ERROR);
    465 			}
    466 
    467 			if (dprintf(fd,
    468 			            "HTTP/1.1 %d %s\r\n"
    469 			            "Date: %s\r\n"
    470 			            "Connection: close\r\n"
    471 			            "Location: //%s%s%s%s%s%s\r\n"
    472 			            "\r\n",
    473 			            S_MOVED_PERMANENTLY,
    474 			            status_str[S_MOVED_PERMANENTLY],
    475 				    timestamp(time(NULL), t),
    476 			            ipv6host ? "[" : "",
    477 				    targethost,
    478 			            ipv6host ? "]" : "", hasport ? ":" : "",
    479 			            hasport ? s.port : "", tmptarget) < 0) {
    480 				return S_REQUEST_TIMEOUT;
    481 			}
    482 		} else {
    483 			/* relative redirection URL */
    484 			if (dprintf(fd,
    485 			            "HTTP/1.1 %d %s\r\n"
    486 			            "Date: %s\r\n"
    487 			            "Connection: close\r\n"
    488 			            "Location: %s\r\n"
    489 			            "\r\n",
    490 			            S_MOVED_PERMANENTLY,
    491 			            status_str[S_MOVED_PERMANENTLY],
    492 				    timestamp(time(NULL), t),
    493 				    tmptarget) < 0) {
    494 				return S_REQUEST_TIMEOUT;
    495 			}
    496 		}
    497 
    498 		return S_MOVED_PERMANENTLY;
    499 	}
    500 
    501 	if (S_ISDIR(st.st_mode)) {
    502 		/* append docindex to target */
    503 		if (esnprintf(realtarget, sizeof(realtarget), "%s%s",
    504 		              r->target, s.docindex)) {
    505 			return http_send_status(fd, S_REQUEST_TOO_LARGE);
    506 		}
    507 
    508 		/* stat the docindex, which must be a regular file */
    509 		if (stat(RELPATH(realtarget), &st) < 0 || !S_ISREG(st.st_mode)) {
    510 			if (s.listdirs) {
    511 				/* remove index suffix and serve dir */
    512 				realtarget[strlen(realtarget) -
    513 				           strlen(s.docindex)] = '\0';
    514 				return resp_dir(fd, RELPATH(realtarget), r);
    515 			} else {
    516 				/* reject */
    517 				if (!S_ISREG(st.st_mode) || errno == EACCES) {
    518 					return http_send_status(fd, S_FORBIDDEN);
    519 				} else {
    520 					return http_send_status(fd, S_NOT_FOUND);
    521 				}
    522 			}
    523 		}
    524 	}
    525 
    526 	/* modified since */
    527 	if (r->field[REQ_MOD][0]) {
    528 		/* parse field */
    529 		if (!strptime(r->field[REQ_MOD], "%a, %d %b %Y %T GMT", &tm)) {
    530 			return http_send_status(fd, S_BAD_REQUEST);
    531 		}
    532 
    533 		/* compare with last modification date of the file */
    534 		if (difftime(st.st_mtim.tv_sec, mktime(&tm)) <= 0) {
    535 			if (dprintf(fd,
    536 			            "HTTP/1.1 %d %s\r\n"
    537 			            "Date: %s\r\n"
    538 			            "Connection: close\r\n"
    539 				    "\r\n",
    540 			            S_NOT_MODIFIED, status_str[S_NOT_MODIFIED],
    541 			            timestamp(time(NULL), t)) < 0) {
    542 				return S_REQUEST_TIMEOUT;
    543 			}
    544 		}
    545 	}
    546 
    547 	/* range */
    548 	lower = 0;
    549 	upper = st.st_size - 1;
    550 	if (r->field[REQ_RANGE][0]) {
    551 		/* parse field */
    552 		p = r->field[REQ_RANGE];
    553 		err = NULL;
    554 
    555 		if (strncmp(p, "bytes=", sizeof("bytes=") - 1)) {
    556 			return http_send_status(fd, S_BAD_REQUEST);
    557 		}
    558 		p += sizeof("bytes=") - 1;
    559 
    560 		if (!(q = strchr(p, '-'))) {
    561 			return http_send_status(fd, S_BAD_REQUEST);
    562 		}
    563 		*(q++) = '\0';
    564 		if (p[0]) {
    565 			lower = strtonum(p, 0, LLONG_MAX, &err);
    566 		}
    567 		if (!err && q[0]) {
    568 			upper = strtonum(q, 0, LLONG_MAX, &err);
    569 		}
    570 		if (err) {
    571 			return http_send_status(fd, S_BAD_REQUEST);
    572 		}
    573 
    574 		/* check range */
    575 		if (lower < 0 || upper < 0 || lower > upper) {
    576 			if (dprintf(fd,
    577 			            "HTTP/1.1 %d %s\r\n"
    578 			            "Date: %s\r\n"
    579 			            "Content-Range: bytes */%zu\r\n"
    580 			            "Connection: close\r\n"
    581 			            "\r\n",
    582 			            S_RANGE_NOT_SATISFIABLE,
    583 			            status_str[S_RANGE_NOT_SATISFIABLE],
    584 			            timestamp(time(NULL), t),
    585 			            st.st_size) < 0) {
    586 				return S_REQUEST_TIMEOUT;
    587 			}
    588 			return S_RANGE_NOT_SATISFIABLE;
    589 		}
    590 
    591 		/* adjust upper limit */
    592 		if (upper >= st.st_size)
    593 			upper = st.st_size-1;
    594 	}
    595 
    596 	/* mime */
    597 	mime = "application/octet-stream";
    598 	if ((p = strrchr(realtarget, '.'))) {
    599 		for (i = 0; i < sizeof(mimes) / sizeof(*mimes); i++) {
    600 			if (!strcmp(mimes[i].ext, p + 1)) {
    601 				mime = mimes[i].type;
    602 				break;
    603 			}
    604 		}
    605 	}
    606 
    607 	return resp_file(fd, RELPATH(realtarget), r, &st, mime, lower, upper);
    608 }