dirl/http.c
Laslo Hunhold 6b55e36036 Introduce flag-centric usage
The config.h-interface has proven to be very effective for a lot of
suckless tools, but it just does not make too much sense for a web
server like quark.

 $ quark

If you run multiple instances of it, you want to see in the command line
(or top) what it does, and given the amount of options it's logical to
just express them as options given in the command line.
It also is a problem if you can modify quark via the config.h,
contradicting the manual. Just saying "Well, then don't touch config.h"
is also not good, as the vhost and map options were only exposed via
this interface.

What is left in config.h are mime-types and two constants relating to
the incoming HTTP-header-limits.

In order to introduce these changes, some structs and safe utility
functions were added and imported from OpenBSD respectively.
2018-03-05 00:14:25 +01:00

547 lines
13 KiB
C

/* See LICENSE file for copyright and license details. */
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <netinet/in.h>
#include <regex.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "config.h"
#include "http.h"
#include "resp.h"
#include "util.h"
const char *req_field_str[] = {
[REQ_HOST] = "Host",
[REQ_RANGE] = "Range",
[REQ_MOD] = "If-Modified-Since",
};
const char *req_method_str[] = {
[M_GET] = "GET",
[M_HEAD] = "HEAD",
};
const char *status_str[] = {
[S_OK] = "OK",
[S_PARTIAL_CONTENT] = "Partial Content",
[S_MOVED_PERMANENTLY] = "Moved Permanently",
[S_NOT_MODIFIED] = "Not Modified",
[S_BAD_REQUEST] = "Bad Request",
[S_FORBIDDEN] = "Forbidden",
[S_NOT_FOUND] = "Not Found",
[S_METHOD_NOT_ALLOWED] = "Method Not Allowed",
[S_REQUEST_TIMEOUT] = "Request Time-out",
[S_RANGE_NOT_SATISFIABLE] = "Range Not Satisfiable",
[S_REQUEST_TOO_LARGE] = "Request Header Fields Too Large",
[S_INTERNAL_SERVER_ERROR] = "Internal Server Error",
[S_VERSION_NOT_SUPPORTED] = "HTTP Version not supported",
};
enum status
http_send_status(int fd, enum status s)
{
static char t[TIMESTAMP_LEN];
if (dprintf(fd,
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"%s"
"Content-Type: text/html\r\n"
"\r\n"
"<!DOCTYPE html>\n<html>\n\t<head>\n"
"\t\t<title>%d %s</title>\n\t</head>\n\t<body>\n"
"\t\t<h1>%d %s</h1>\n\t</body>\n</html>\n",
s, status_str[s], timestamp(time(NULL), t),
(s == S_METHOD_NOT_ALLOWED) ? "Allow: HEAD, GET\r\n" : "",
s, status_str[s], s, status_str[s]) < 0) {
return S_REQUEST_TIMEOUT;
}
return s;
}
static void
decode(char src[PATH_MAX], char dest[PATH_MAX])
{
size_t i;
uint8_t n;
char *s;
for (s = src, i = 0; *s; s++, i++) {
if (*s == '+') {
dest[i] = ' ';
} else if (*s == '%' && (sscanf(s + 1, "%2hhx", &n) == 1)) {
dest[i] = n;
s += 2;
} else {
dest[i] = *s;
}
}
dest[i] = '\0';
}
int
http_get_request(int fd, struct request *r)
{
size_t hlen, i, mlen;
ssize_t off;
char h[HEADER_MAX], *p, *q;
/* empty all fields */
memset(r, 0, sizeof(*r));
/*
* receive header
*/
for (hlen = 0; ;) {
if ((off = read(fd, h + hlen, sizeof(h) - hlen)) < 0) {
return http_send_status(fd, S_REQUEST_TIMEOUT);
} else if (off == 0) {
break;
}
hlen += off;
if (hlen >= 4 && !memcmp(h + hlen - 4, "\r\n\r\n", 4)) {
break;
}
if (hlen == sizeof(h)) {
return http_send_status(fd, S_REQUEST_TOO_LARGE);
}
}
/* remove terminating empty line */
if (hlen < 2) {
return http_send_status(fd, S_BAD_REQUEST);
}
hlen -= 2;
/* null-terminate the header */
h[hlen] = '\0';
/*
* parse request line
*/
/* METHOD */
for (i = 0; i < NUM_REQ_METHODS; i++) {
mlen = strlen(req_method_str[i]);
if (!strncmp(req_method_str[i], h, mlen)) {
r->method = i;
break;
}
}
if (i == NUM_REQ_METHODS) {
return http_send_status(fd, S_METHOD_NOT_ALLOWED);
}
/* a single space must follow the method */
if (h[mlen] != ' ') {
return http_send_status(fd, S_BAD_REQUEST);
}
/* basis for next step */
p = h + mlen + 1;
/* TARGET */
if (!(q = strchr(p, ' '))) {
return http_send_status(fd, S_BAD_REQUEST);
}
*q = '\0';
if (q - p + 1 > PATH_MAX) {
return http_send_status(fd, S_REQUEST_TOO_LARGE);
}
memcpy(r->target, p, q - p + 1);
decode(r->target, r->target);
/* basis for next step */
p = q + 1;
/* HTTP-VERSION */
if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) {
return http_send_status(fd, S_BAD_REQUEST);
}
p += sizeof("HTTP/") - 1;
if (strncmp(p, "1.0", sizeof("1.0") - 1) &&
strncmp(p, "1.1", sizeof("1.1") - 1)) {
return http_send_status(fd, S_VERSION_NOT_SUPPORTED);
}
p += sizeof("1.*") - 1;
/* check terminator */
if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) {
return http_send_status(fd, S_BAD_REQUEST);
}
/* basis for next step */
p += sizeof("\r\n") - 1;
/*
* parse request-fields
*/
/* match field type */
for (; *p != '\0';) {
for (i = 0; i < NUM_REQ_FIELDS; i++) {
if (!strncasecmp(p, req_field_str[i],
strlen(req_field_str[i]))) {
break;
}
}
if (i == NUM_REQ_FIELDS) {
/* unmatched field, skip this line */
if (!(q = strstr(p, "\r\n"))) {
return http_send_status(fd, S_BAD_REQUEST);
}
p = q + (sizeof("\r\n") - 1);
continue;
}
p += strlen(req_field_str[i]);
/* a single colon must follow the field name */
if (*p != ':') {
return http_send_status(fd, S_BAD_REQUEST);
}
/* skip whitespace */
for (++p; *p == ' ' || *p == '\t'; p++)
;
/* extract field content */
if (!(q = strstr(p, "\r\n"))) {
return http_send_status(fd, S_BAD_REQUEST);
}
*q = '\0';
if (q - p + 1 > FIELD_MAX) {
return http_send_status(fd, S_REQUEST_TOO_LARGE);
}
memcpy(r->field[i], p, q - p + 1);
/* go to next line */
p = q + (sizeof("\r\n") - 1);
}
return 0;
}
static void
encode(char src[PATH_MAX], char dest[PATH_MAX])
{
size_t i;
char *s;
for (s = src, i = 0; *s && i < (PATH_MAX - 4); s++) {
if (iscntrl(*s) || (unsigned char)*s > 127) {
i += snprintf(dest + i, PATH_MAX - i, "%%%02X",
(unsigned char)*s);
} else {
dest[i] = *s;
i++;
}
}
dest[i] = '\0';
}
static int
normabspath(char *path)
{
size_t len;
int last = 0;
char *p, *q;
/* require and skip first slash */
if (path[0] != '/') {
return 1;
}
p = path + 1;
/* get length of path */
len = strlen(p);
for (; !last; ) {
/* bound path component within (p,q) */
if (!(q = strchr(p, '/'))) {
q = strchr(p, '\0');
last = 1;
}
if (p == q || (q - p == 1 && p[0] == '.')) {
/* "/" or "./" */
goto squash;
} else if (q - p == 2 && p[0] == '.' && p[1] == '.') {
/* "../" */
if (p != path + 1) {
/* place p right after the previous / */
for (p -= 2; p > path && *p != '/'; p--);
p++;
}
goto squash;
} else {
/* move on */
p = q + 1;
continue;
}
squash:
/* squash (p,q) into void */
if (last) {
*p = '\0';
len = p - path;
} else {
memmove(p, q + 1, len - ((q + 1) - path) + 2);
len -= (q + 1) - p;
}
}
return 0;
}
#undef RELPATH
#define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1))
enum status
http_send_response(int fd, struct request *r)
{
struct in6_addr res;
struct stat st;
struct tm tm;
size_t len, i;
off_t lower, upper;
int hasport, ipv6host;
static char realtarget[PATH_MAX], tmptarget[PATH_MAX], t[TIMESTAMP_LEN];
char *p, *q, *mime;
const char *vhostmatch, *err;
/* make a working copy of the target */
memcpy(realtarget, r->target, sizeof(realtarget));
/* match vhost */
vhostmatch = NULL;
if (s.vhost) {
for (i = 0; i < s.vhost_len; i++) {
/* switch to vhost directory if there is a match */
if (!regexec(&s.vhost[i].re, r->field[REQ_HOST], 0,
NULL, 0)) {
if (chdir(s.vhost[i].dir) < 0) {
return http_send_status(fd, (errno == EACCES) ?
S_FORBIDDEN : S_NOT_FOUND);
}
vhostmatch = s.vhost[i].name;
break;
}
}
if (i == s.vhost_len) {
return http_send_status(fd, S_NOT_FOUND);
}
/* if we have a vhost prefix, prepend it to the target */
if (s.vhost[i].prefix) {
if (snprintf(realtarget, sizeof(realtarget), "%s%s",
s.vhost[i].prefix, realtarget) >= sizeof(realtarget)) {
return http_send_status(fd, S_REQUEST_TOO_LARGE);
}
}
}
/* apply target prefix mapping */
for (i = 0; i < s.map_len; i++) {
len = strlen(s.map[i].from);
if (!strncmp(realtarget, s.map[i].from, len)) {
/* match canonical host if vhosts are enabled */
if (s.vhost && strcmp(s.map[i].chost, vhostmatch)) {
continue;
}
/* swap out target prefix */
if (snprintf(tmptarget, sizeof(tmptarget), "%s%s", s.map[i].to,
realtarget + len) >= sizeof(tmptarget)) {
return http_send_status(fd, S_REQUEST_TOO_LARGE);
}
memcpy(realtarget, tmptarget, sizeof(realtarget));
break;
}
}
/* normalize target */
if (normabspath(realtarget)) {
return http_send_status(fd, S_BAD_REQUEST);
}
/* reject hidden target */
if (realtarget[0] == '.' || strstr(realtarget, "/.")) {
return http_send_status(fd, S_FORBIDDEN);
}
/* stat the target */
if (stat(RELPATH(realtarget), &st) < 0) {
return http_send_status(fd, (errno == EACCES) ?
S_FORBIDDEN : S_NOT_FOUND);
}
if (S_ISDIR(st.st_mode)) {
/* add / to target if not present */
len = strlen(realtarget);
if (len == PATH_MAX - 2) {
return http_send_status(fd, S_REQUEST_TOO_LARGE);
}
if (len && realtarget[len - 1] != '/') {
realtarget[len] = '/';
realtarget[len + 1] = '\0';
}
}
/* redirect if targets differ, host is non-canonical or we prefixed */
if (strcmp(r->target, realtarget) || (s.vhost && vhostmatch &&
strcmp(r->field[REQ_HOST], vhostmatch))) {
/* do we need to add a port to the Location? */
hasport = s.port && strcmp(s.port, "80");
/* RFC 2732 specifies to use brackets for IPv6-addresses in
* URLs, so we need to check if our host is one and honor that
* later when we fill the "Location"-field */
if ((ipv6host = inet_pton(AF_INET6, r->field[REQ_HOST][0] ?
r->field[REQ_HOST] : s.host ? s.host :
"localhost", &res)) < 0) {
return http_send_status(fd, S_INTERNAL_SERVER_ERROR);
}
/* encode realtarget */
encode(realtarget, tmptarget);
/* send redirection header */
if (dprintf(fd,
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"Location: //%s%s%s%s%s%s\r\n"
"\r\n",
S_MOVED_PERMANENTLY,
status_str[S_MOVED_PERMANENTLY],
timestamp(time(NULL), t), ipv6host ? "[" : "",
r->field[REQ_HOST][0] ? (s.vhost && vhostmatch) ?
vhostmatch : r->field[REQ_HOST] : s.host ?
s.host : "localhost",
ipv6host ? "]" : "", hasport ? ":" : "",
hasport ? s.port : "", tmptarget) < 0) {
return S_REQUEST_TIMEOUT;
}
return S_MOVED_PERMANENTLY;
}
if (S_ISDIR(st.st_mode)) {
/* append docindex to target */
if (snprintf(realtarget, sizeof(realtarget), "%s%s",
r->target, s.docindex) >= sizeof(realtarget)) {
return http_send_status(fd, S_REQUEST_TOO_LARGE);
}
/* stat the docindex, which must be a regular file */
if (stat(RELPATH(realtarget), &st) < 0 || !S_ISREG(st.st_mode)) {
if (s.listdirs) {
/* remove index suffix and serve dir */
realtarget[strlen(realtarget) -
strlen(s.docindex)] = '\0';
return resp_dir(fd, RELPATH(realtarget), r);
} else {
/* reject */
if (!S_ISREG(st.st_mode) || errno == EACCES) {
return http_send_status(fd, S_FORBIDDEN);
} else {
return http_send_status(fd, S_NOT_FOUND);
}
}
}
}
/* modified since */
if (r->field[REQ_MOD][0]) {
/* parse field */
if (!strptime(r->field[REQ_MOD], "%a, %d %b %Y %T GMT", &tm)) {
return http_send_status(fd, S_BAD_REQUEST);
}
/* compare with last modification date of the file */
if (difftime(st.st_mtim.tv_sec, mktime(&tm)) <= 0) {
if (dprintf(fd,
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"\r\n",
S_NOT_MODIFIED, status_str[S_NOT_MODIFIED],
timestamp(time(NULL), t)) < 0) {
return S_REQUEST_TIMEOUT;
}
}
}
/* range */
lower = 0;
upper = st.st_size - 1;
if (r->field[REQ_RANGE][0]) {
/* parse field */
p = r->field[REQ_RANGE];
err = NULL;
if (strncmp(p, "bytes=", sizeof("bytes=") - 1)) {
return http_send_status(fd, S_BAD_REQUEST);
}
p += sizeof("bytes=") - 1;
if (!(q = strchr(p, '-'))) {
return http_send_status(fd, S_BAD_REQUEST);
}
*(q++) = '\0';
if (p[0]) {
lower = strtonum(p, 0, LLONG_MAX, &err);
}
if (!err && q[0]) {
upper = strtonum(q, 0, LLONG_MAX, &err);
}
if (err) {
return http_send_status(fd, S_BAD_REQUEST);
}
/* check range */
if (lower < 0 || upper < 0 || lower > upper) {
if (dprintf(fd,
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Content-Range: bytes */%zu\r\n"
"Connection: close\r\n"
"\r\n",
S_RANGE_NOT_SATISFIABLE,
status_str[S_RANGE_NOT_SATISFIABLE],
timestamp(time(NULL), t),
st.st_size) < 0) {
return S_REQUEST_TIMEOUT;
}
return S_RANGE_NOT_SATISFIABLE;
}
/* adjust upper limit */
if (upper >= st.st_size)
upper = st.st_size-1;
}
/* mime */
mime = "application/octet-stream";
if ((p = strrchr(realtarget, '.'))) {
for (i = 0; i < sizeof(mimes) / sizeof(*mimes); i++) {
if (!strcmp(mimes[i].ext, p + 1)) {
mime = mimes[i].type;
break;
}
}
}
return resp_file(fd, RELPATH(realtarget), r, &st, mime, lower, upper);
}