#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #define REQ_BUF 4096 #define RESP_BUF 512 #define DEFAULT_PORT 8080 #define DEFAULT_MAX_CONN 128 #define DEFAULT_THREADS 4 #define DEFAULT_MAX_JOBS 1024 struct client { int fd; int used; time_t last; size_t len; char buf[REQ_BUF]; }; struct job { int fd; int idx; struct client *clients; int *cur_conn; char req[REQ_BUF]; struct job *next; }; static char g_root_buf[PATH_MAX] = "."; static const char *g_root = g_root_buf; static const char *g_index = "index.html"; static int g_max_conn = DEFAULT_MAX_CONN; static int g_dirlist = 0; static const char *g_css = NULL; static int g_plain = 0; static int g_server_hdr = 0; static int g_timeout = 10; static size_t g_max_hdr = REQ_BUF - 1; static const char *g_progname = KHTTPD_NAME; static int g_threads = DEFAULT_THREADS; static int g_max_jobs = DEFAULT_MAX_JOBS; static pthread_mutex_t g_job_mtx = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t g_job_cv = PTHREAD_COND_INITIALIZER; static struct job *g_job_head = NULL; static struct job *g_job_tail = NULL; static size_t g_job_len = 0; static pthread_mutex_t g_conn_mtx = PTHREAD_MUTEX_INITIALIZER; static void usage(void); static int set_nonblock(int); static uid_t parse_uid(const char *); static gid_t parse_gid(const char *); static int bad_path(const char *); static const char *status_msg(int); static int request_complete(const char *); static void write_hdr(int, const char *, size_t, int); static size_t xstrlcpy(char *, const char *, size_t); static size_t xstrlcat(char *, const char *, size_t); static int path_within_root(const char *); static int resolve_path_under_root(const char *, char *); static int write_all(int, const void *, size_t); static void write_html_escaped(int, const char *); static void write_url_escaped(int, const char *); static void parent_url(const char *, char *, size_t); static const char *mime_type(const char *); static int encoding_allowed(const char *, const char *); static int parse_range_header(const char *, off_t, off_t *, off_t *); static int month_idx(const char *); static long long days_from_civil(int, unsigned, unsigned); static int parse_http_date(const char *, time_t *); static void format_http_date(time_t, char *, size_t); static void make_etag(const struct stat *, char *, size_t); static int etag_matches(const char *, const char *); static void send_redirect(int, const char *); static void send_simple(int, int, const char *); static int serve_file(int, const char *, const char *, int, const char *, const char *, int, time_t, const char *); static int serve_dir(int, const char *, const char *, int); static void handle_request(int, const char *); static int setup_listener(const char *, int); static int parse_int(const char *, int, int); static void daemonize(void); static void close_client(struct client *, int, int *); static ssize_t read_request(int, struct client *); static void release_client(struct client *, int, int *); static int enqueue_job(int, struct client *, int, int *, const char *); static struct job *dequeue_job(void); static void *worker_main(void *); static void usage(void) { fprintf(stderr, "usage: %s [-DHMchl] [-a addr] [-b bytes] [--css file]\n" " [-g group] [-i index] [-m max] [-p port] [-r root]\n" " [-t seconds] [-u user] [-T threads]\n", g_progname); exit(1); } static int set_nonblock(int fd) { int flags; flags = fcntl(fd, F_GETFL, 0); if (flags < 0) return -1; return fcntl(fd, F_SETFL, flags | O_NONBLOCK); } static int parse_int(const char *s, int min, int max) { char *end; long v; errno = 0; v = strtol(s, &end, 10); if (errno || s == end || *end != '\0') errx(1, "invalid number: %s", s); if (v < min || v > max) errx(1, "number out of range: %s", s); return (int)v; } static size_t xstrlcpy(char *dst, const char *src, size_t dsize) { size_t i; size_t slen; slen = 0; while (src[slen] != '\0') slen++; if (dsize != 0) { i = 0; while (i + 1 < dsize && src[i] != '\0') { dst[i] = src[i]; i++; } dst[i] = '\0'; } return slen; } static size_t xstrlcat(char *dst, const char *src, size_t dsize) { size_t dlen; size_t slen; size_t i; dlen = 0; while (dlen < dsize && dst[dlen] != '\0') dlen++; slen = 0; while (src[slen] != '\0') slen++; if (dlen == dsize) return dsize + slen; i = 0; while (dlen + i + 1 < dsize && src[i] != '\0') { dst[dlen + i] = src[i]; i++; } dst[dlen + i] = '\0'; return dlen + slen; } static int path_within_root(const char *path) { size_t rlen; if (strcmp(g_root, "/") == 0) return 1; rlen = strlen(g_root); if (strncmp(path, g_root, rlen) != 0) return 0; if (path[rlen] == '\0' || path[rlen] == '/') return 1; return 0; } static int resolve_path_under_root(const char *in, char *out) { if (realpath(in, out) == NULL) { if (errno == ENAMETOOLONG) return 414; if (errno == ENOENT || errno == ENOTDIR) return 404; if (errno == EACCES) return 403; return 500; } if (!path_within_root(out)) return 403; return 0; } static void daemonize(void) { int fd; switch (fork()) { case -1: err(1, "fork"); case 0: break; default: _exit(0); } if (setsid() < 0) err(1, "setsid"); (void)chdir("/"); fd = open("/dev/null", O_RDWR); if (fd >= 0) { (void)dup2(fd, STDIN_FILENO); (void)dup2(fd, STDOUT_FILENO); (void)dup2(fd, STDERR_FILENO); if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO) close(fd); } } static uid_t parse_uid(const char *s) { char *end; unsigned long v; struct passwd *pw; errno = 0; v = strtoul(s, &end, 10); if (s[0] != '\0' && end != NULL && *end == '\0') { if (errno == ERANGE || (uid_t)v != v) errx(1, "uid out of range: %s", s); return (uid_t)v; } pw = getpwnam(s); if (pw == NULL) errx(1, "unknown user: %s", s); return pw->pw_uid; } static gid_t parse_gid(const char *s) { char *end; unsigned long v; struct group *gr; errno = 0; v = strtoul(s, &end, 10); if (s[0] != '\0' && end != NULL && *end == '\0') { if (errno == ERANGE || (gid_t)v != v) errx(1, "gid out of range: %s", s); return (gid_t)v; } gr = getgrnam(s); if (gr == NULL) errx(1, "unknown group: %s", s); return gr->gr_gid; } static int bad_path(const char *path) { const char *seg; const char *p; size_t len; if (*path != '/') return 1; seg = path + 1; for (p = path; *p != '\0'; p++) { if (*p == '\\') return 1; if ((unsigned char)*p < 0x20 || *p == 0x7f) return 1; if (*p == '/') { len = (size_t)(p - seg); if ((len == 1 && seg[0] == '.') || (len == 2 && seg[0] == '.' && seg[1] == '.')) return 1; seg = p + 1; } } len = (size_t)(p - seg); if ((len == 1 && seg[0] == '.') || (len == 2 && seg[0] == '.' && seg[1] == '.')) return 1; return 0; } static const char * status_msg(int code) { switch (code) { case 206: return KHTTPD_STATUS_PARTIAL_CONTENT; case 304: return KHTTPD_STATUS_NOT_MODIFIED; case 414: return KHTTPD_STATUS_URI_TOO_LONG; case 200: return KHTTPD_STATUS_OK; case 400: return KHTTPD_STATUS_BAD_REQUEST; case 403: return KHTTPD_STATUS_FORBIDDEN; case 404: return KHTTPD_STATUS_NOT_FOUND; case 405: return KHTTPD_STATUS_METHOD_NOT_ALLOWED; case 416: return KHTTPD_STATUS_RANGE_NOT_SATISFIABLE; case 505: return KHTTPD_STATUS_HTTP_UNSUPPORTED; case 500: return KHTTPD_STATUS_INTERNAL_ERROR; default: return KHTTPD_STATUS_ERROR; } } static int request_complete(const char *req) { return strstr(req, "\r\n\r\n") != NULL || strstr(req, "\n\n") != NULL; } static void write_hdr(int fd, const char *buf, size_t bufsz, int n) { size_t out; if (n <= 0) return; out = (size_t)n; if (out >= bufsz) out = bufsz - 1; write_all(fd, buf, out); } static int encoding_allowed(const char *hdr, const char *enc) { const char *p; size_t enclen; if (hdr == NULL || *hdr == '\0') return 0; p = hdr; enclen = strlen(enc); while (*p != '\0') { const char *tok; const char *end; size_t len; int qzero; while (*p == ' ' || *p == '\t' || *p == ',') p++; if (*p == '\0') break; tok = p; while (*p != '\0' && *p != ',' && *p != ';') p++; end = p; while (end > tok && (end[-1] == ' ' || end[-1] == '\t')) end--; len = (size_t)(end - tok); qzero = 0; while (*p != '\0' && *p != ',') { if ((p[0] == 'q' || p[0] == 'Q') && p[1] == '=') { const char *qv; qv = p + 2; while (*qv == ' ' || *qv == '\t') qv++; if (*qv == '0') qzero = 1; } p++; } if (*p == ',') p++; if (qzero) continue; if (len == enclen && strncasecmp(tok, enc, enclen) == 0) return 1; if (len == 1 && tok[0] == '*') return 1; } return 0; } static int parse_range_header(const char *hdr, off_t size, off_t *start, off_t *end) { const char *p; char *q; long long a; long long b; if (hdr == NULL) return 0; while (*hdr == ' ' || *hdr == '\t') hdr++; if (strncasecmp(hdr, "bytes=", 6) != 0) return 0; p = hdr + 6; if (size <= 0) return -1; if (strchr(p, ',') != NULL) return 0; if (*p == '-') { long long suf; p++; errno = 0; suf = strtoll(p, &q, 10); if (errno || q == p || *q != '\0' || suf <= 0) return 0; if ((off_t)suf >= size) { *start = 0; *end = size - 1; } else { *start = size - (off_t)suf; *end = size - 1; } return 1; } errno = 0; a = strtoll(p, &q, 10); if (errno || q == p || a < 0) return 0; if (*q != '-') return 0; p = q + 1; if (*p == '\0') { if ((off_t)a >= size) return -1; *start = (off_t)a; *end = size - 1; return 1; } errno = 0; b = strtoll(p, &q, 10); if (errno || q == p || *q != '\0' || b < a) return 0; if ((off_t)a >= size) return -1; *start = (off_t)a; *end = (off_t)b; if (*end >= size) *end = size - 1; return 1; } static int month_idx(const char *m) { static const char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; int i; for (i = 0; i < 12; i++) { if (strcasecmp(m, months[i]) == 0) return i + 1; } return 0; } static long long days_from_civil(int y, unsigned m, unsigned d) { int era; int mi; unsigned yoe; unsigned doy; unsigned doe; y -= m <= 2; era = (y >= 0 ? y : y - 399) / 400; yoe = (unsigned)(y - era * 400); mi = (int)m; doy = (153U * (unsigned)(mi + (mi > 2 ? -3 : 9)) + 2) / 5 + d - 1; doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; return (long long)era * 146097 + (long long)doe - 719468; } static int parse_http_date(const char *s, time_t *out) { char mon[4]; int day; int year; int hh; int mm; int ss; long long days; long long total; if (sscanf(s, "%*3s, %d %3s %d %d:%d:%d GMT", &day, mon, &year, &hh, &mm, &ss) != 6) return 0; mon[3] = '\0'; if (day < 1 || day > 31 || year < 1970 || hh < 0 || hh > 23 || mm < 0 || mm > 59 || ss < 0 || ss > 60) return 0; { int m; m = month_idx(mon); if (m == 0) return 0; days = days_from_civil(year, (unsigned)m, (unsigned)day); } total = days * 86400LL + hh * 3600LL + mm * 60LL + ss; if (total < 0) return 0; *out = (time_t)total; return 1; } static void format_http_date(time_t t, char *out, size_t outsz) { struct tm tmv; gmtime_r(&t, &tmv); if (strftime(out, outsz, "%a, %d %b %Y %H:%M:%S GMT", &tmv) == 0 && outsz > 0) out[0] = '\0'; } static void make_etag(const struct stat *st, char *out, size_t outsz) { snprintf(out, outsz, "\"%lx-%lx-%lx\"", (unsigned long)st->st_ino, (unsigned long)st->st_size, (unsigned long)st->st_mtime); } static int etag_matches(const char *hdr, const char *etag) { const char *p; size_t elen; if (hdr == NULL || etag == NULL) return 0; p = hdr; elen = strlen(etag); while (*p != '\0') { const char *tok; const char *end; size_t len; while (*p == ' ' || *p == '\t' || *p == ',') p++; if (*p == '\0') break; tok = p; while (*p != '\0' && *p != ',') p++; end = p; while (end > tok && (end[-1] == ' ' || end[-1] == '\t')) end--; len = (size_t)(end - tok); if (len == 1 && tok[0] == '*') return 1; if (len == elen && strncmp(tok, etag, elen) == 0) return 1; if (*p == ',') p++; } return 0; } static const char * mime_type(const char *path) { const char *ext; ext = strrchr(path, '.'); if (ext == NULL || ext[1] == '\0') return "application/octet-stream"; ext++; if (strcasecmp(ext, "html") == 0 || strcasecmp(ext, "htm") == 0) return "text/html"; if (strcasecmp(ext, "css") == 0) return "text/css"; if (strcasecmp(ext, "js") == 0) return "application/javascript"; if (strcasecmp(ext, "json") == 0) return "application/json"; if (strcasecmp(ext, "txt") == 0) return "text/plain"; if (strcasecmp(ext, "png") == 0) return "image/png"; if (strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0) return "image/jpeg"; if (strcasecmp(ext, "gif") == 0) return "image/gif"; if (strcasecmp(ext, "svg") == 0) return "image/svg+xml"; if (strcasecmp(ext, "ico") == 0) return "image/x-icon"; return g_plain ? "text/plain" : "application/octet-stream"; } static int write_all(int fd, const void *buf, size_t len) { const char *p; ssize_t n; p = buf; while (len > 0) { n = write(fd, p, len); if (n < 0) { if (errno == EINTR) continue; if (errno == EAGAIN || errno == EWOULDBLOCK) return -1; return -1; } if (n == 0) return -1; p += (size_t)n; len -= (size_t)n; } return 0; } static void close_client(struct client *clients, int idx, int *cur_conn) { pthread_mutex_lock(&g_conn_mtx); close(clients[idx].fd); clients[idx].used = 0; (*cur_conn)--; pthread_mutex_unlock(&g_conn_mtx); } static void release_client(struct client *clients, int idx, int *cur_conn) { (void)cur_conn; pthread_mutex_lock(&g_conn_mtx); clients[idx].used = 2; pthread_mutex_unlock(&g_conn_mtx); } static ssize_t read_request(int fd, struct client *c) { size_t maxread; ssize_t n; maxread = sizeof(c->buf) - c->len - 1; if (g_max_hdr < sizeof(c->buf) - 1) { if (c->len >= g_max_hdr) return -2; if (maxread > g_max_hdr - c->len) maxread = g_max_hdr - c->len; } if (maxread == 0) return -2; n = read(fd, c->buf + c->len, maxread); if (n < 0) { if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) return -3; } if (n > 0) { c->len += (size_t)n; c->buf[c->len] = '\0'; c->last = time(NULL); } return n; } static int enqueue_job(int fd, struct client *clients, int idx, int *cur_conn, const char *req) { struct job *j; pthread_mutex_lock(&g_job_mtx); if ((int)g_job_len >= g_max_jobs) { pthread_mutex_unlock(&g_job_mtx); return -1; } pthread_mutex_unlock(&g_job_mtx); j = calloc(1, sizeof(*j)); if (j == NULL) return -1; j->fd = fd; j->idx = idx; j->clients = clients; j->cur_conn = cur_conn; (void)xstrlcpy(j->req, req, sizeof(j->req)); j->next = NULL; pthread_mutex_lock(&g_job_mtx); if (g_job_tail == NULL) { g_job_head = j; g_job_tail = j; } else { g_job_tail->next = j; g_job_tail = j; } g_job_len++; pthread_cond_signal(&g_job_cv); pthread_mutex_unlock(&g_job_mtx); return 0; } static struct job * dequeue_job(void) { struct job *j; pthread_mutex_lock(&g_job_mtx); while (g_job_head == NULL) pthread_cond_wait(&g_job_cv, &g_job_mtx); j = g_job_head; g_job_head = j->next; if (g_job_head == NULL) g_job_tail = NULL; if (g_job_len > 0) g_job_len--; pthread_mutex_unlock(&g_job_mtx); return j; } static void * worker_main(void *arg) { struct job *j; (void)arg; for (;;) { j = dequeue_job(); handle_request(j->fd, j->req); close(j->fd); pthread_mutex_lock(&g_conn_mtx); j->clients[j->idx].used = 0; (*j->cur_conn)--; pthread_mutex_unlock(&g_conn_mtx); free(j); } return NULL; } static void write_html_escaped(int fd, const char *s) { const char *rep; char ch; while (*s != '\0') { ch = *s++; switch (ch) { case '&': rep = "&"; break; case '<': rep = "<"; break; case '>': rep = ">"; break; case '"': rep = """; break; case '\'': rep = "'"; break; default: if (write_all(fd, &ch, 1) < 0) return; continue; } if (write_all(fd, rep, strlen(rep)) < 0) return; } } static void write_url_escaped(int fd, const char *s) { static const char hex[] = "0123456789ABCDEF"; unsigned char c; char buf[3]; while (*s != '\0') { c = (unsigned char)*s++; if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || c == '/') { if (write_all(fd, &c, 1) < 0) return; } else { buf[0] = '%'; buf[1] = hex[c >> 4]; buf[2] = hex[c & 0x0f]; if (write_all(fd, buf, sizeof(buf)) < 0) return; } } } static void parent_url(const char *req_path, char *out, size_t outsz) { size_t len; len = strlen(req_path); if (len > 1 && req_path[len - 1] == '/') len--; while (len > 1 && req_path[len - 1] != '/') len--; if (len == 0) len = 1; if (len >= outsz) len = outsz - 1; memcpy(out, req_path, len); out[len] = '\0'; if (len + 1 < outsz && out[len - 1] != '/') { out[len] = '/'; out[len + 1] = '\0'; } } static void send_simple(int fd, int code, const char *extra) { char hdr[RESP_BUF]; const char *msg; int n; size_t len; if (g_css != NULL) { static const char head1[] = "\n" ""; static const char head2[] = ""; static const char css1[] = ""; static const char body1[] = "

"; static const char body2[] = "

"; static const char pre1[] = "
";
		static const char pre2[] = "
"; static const char tail[] = "
\n"; msg = status_msg(code); n = snprintf(hdr, sizeof(hdr), KHTTPD_HTTP_VERSION " %d %s\r\n" "Content-Type: text/html\r\n" "%s" "Connection: close\r\n\r\n", code, msg, g_server_hdr ? KHTTPD_SERVER_HEADER : ""); if (n > 0) write_hdr(fd, hdr, sizeof(hdr), n); if (write_all(fd, head1, sizeof(head1) - 1) < 0) return; if (write_all(fd, msg, strlen(msg)) < 0) return; if (write_all(fd, head2, sizeof(head2) - 1) < 0) return; if (write_all(fd, css1, sizeof(css1) - 1) < 0) return; write_html_escaped(fd, g_css); if (write_all(fd, css2, sizeof(css2) - 1) < 0) return; if (write_all(fd, body1, sizeof(body1) - 1) < 0) return; if (write_all(fd, msg, strlen(msg)) < 0) return; if (write_all(fd, body2, sizeof(body2) - 1) < 0) return; if (extra != NULL && *extra != '\0') { if (write_all(fd, pre1, sizeof(pre1) - 1) < 0) return; write_html_escaped(fd, extra); if (write_all(fd, pre2, sizeof(pre2) - 1) < 0) return; } (void)write_all(fd, tail, sizeof(tail) - 1); return; } len = extra != NULL ? strlen(extra) : 0; n = snprintf(hdr, sizeof(hdr), KHTTPD_HTTP_VERSION " %d %s\r\n" "Content-Length: %zu\r\n" "Content-Type: text/plain\r\n" "%s" "Connection: close\r\n\r\n" "%s", code, status_msg(code), len, g_server_hdr ? KHTTPD_SERVER_HEADER : "", extra != NULL ? extra : ""); if (n > 0) write_hdr(fd, hdr, sizeof(hdr), n); } static void send_redirect(int fd, const char *location) { char hdr[RESP_BUF]; int n; n = snprintf(hdr, sizeof(hdr), KHTTPD_HTTP_VERSION " 301 " KHTTPD_STATUS_MOVED_PERMANENTLY "\r\n" "Location: %s\r\n" "%s" "Content-Length: 0\r\n" "Connection: close\r\n\r\n", location, g_server_hdr ? KHTTPD_SERVER_HEADER : ""); if (n > 0) write_hdr(fd, hdr, sizeof(hdr), n); } static int serve_file(int fd, const char *path, const char *mime_path, int head_only, const char *accept_encoding, const char *if_none_match, int has_if_modified_since, time_t if_modified_since, const char *range_hdr) { char enc_path[PATH_MAX]; char hdr[RESP_BUF]; char resolved_path[PATH_MAX]; struct stat lst; struct stat st; const char *content_encoding; const char *served_path; off_t range_start; off_t range_end; off_t body_len; int have_range; int ffd; int n; int status; served_path = path; content_encoding = NULL; range_start = 0; range_end = 0; have_range = 0; status = 200; if (accept_encoding != NULL && *accept_encoding != '\0') { struct stat cst; int want_br; int want_gz; want_br = encoding_allowed(accept_encoding, "br"); want_gz = encoding_allowed(accept_encoding, "gzip"); if (want_br && xstrlcpy(enc_path, path, sizeof(enc_path)) < sizeof(enc_path) && xstrlcat(enc_path, ".br", sizeof(enc_path)) < sizeof(enc_path) && stat(enc_path, &cst) == 0 && S_ISREG(cst.st_mode)) { served_path = enc_path; content_encoding = "br"; } else if (want_gz && xstrlcpy(enc_path, path, sizeof(enc_path)) < sizeof(enc_path) && xstrlcat(enc_path, ".gz", sizeof(enc_path)) < sizeof(enc_path) && stat(enc_path, &cst) == 0 && S_ISREG(cst.st_mode)) { served_path = enc_path; content_encoding = "gzip"; } } { int rrc; rrc = resolve_path_under_root(served_path, resolved_path); if (rrc != 0) return rrc; } if (lstat(resolved_path, &lst) < 0) { if (errno == ENAMETOOLONG) return 414; if (errno == EACCES) return 403; if (errno == ENOENT || errno == ENOTDIR) return 404; return 500; } if (!S_ISREG(lst.st_mode)) return 403; ffd = open(resolved_path, O_RDONLY); if (ffd < 0) { if (errno == ENAMETOOLONG) return 414; if (errno == EACCES) return 403; return 404; } if (fstat(ffd, &st) < 0) { close(ffd); return 500; } if (st.st_dev != lst.st_dev || st.st_ino != lst.st_ino) { close(ffd); return 500; } if (!S_ISREG(st.st_mode)) { close(ffd); return 403; } { char etag[96]; char lm[64]; int not_modified; not_modified = 0; make_etag(&st, etag, sizeof(etag)); format_http_date(st.st_mtime, lm, sizeof(lm)); if (if_none_match != NULL && *if_none_match != '\0') { if (etag_matches(if_none_match, etag)) not_modified = 1; } else if (has_if_modified_since && st.st_mtime <= if_modified_since) { not_modified = 1; } if (not_modified) { n = snprintf(hdr, sizeof(hdr), KHTTPD_HTTP_VERSION " 304 " KHTTPD_STATUS_NOT_MODIFIED "\r\n" "ETag: %s\r\n" "Last-Modified: %s\r\n" "%s%s%s" "%s" "Connection: close\r\n\r\n", etag, lm, content_encoding != NULL ? "Content-Encoding: " : "", content_encoding != NULL ? content_encoding : "", content_encoding != NULL ? "\r\nVary: Accept-Encoding\r\n" : "", g_server_hdr ? KHTTPD_SERVER_HEADER : ""); if (n > 0) write_hdr(fd, hdr, sizeof(hdr), n); close(ffd); return 304; } if (range_hdr != NULL && *range_hdr != '\0') { int pr; pr = parse_range_header(range_hdr, st.st_size, &range_start, &range_end); if (pr < 0) { n = snprintf(hdr, sizeof(hdr), KHTTPD_HTTP_VERSION " 416 " KHTTPD_STATUS_RANGE_NOT_SATISFIABLE "\r\n" "Content-Range: bytes */%ld\r\n" "ETag: %s\r\n" "Last-Modified: %s\r\n" "%s" "Connection: close\r\n\r\n", (long)st.st_size, etag, lm, g_server_hdr ? KHTTPD_SERVER_HEADER : ""); if (n > 0) write_hdr(fd, hdr, sizeof(hdr), n); close(ffd); return 416; } if (pr > 0) { have_range = 1; status = 206; } } body_len = st.st_size; if (have_range) body_len = range_end - range_start + 1; if (have_range) { n = snprintf(hdr, sizeof(hdr), KHTTPD_HTTP_VERSION " 206 " KHTTPD_STATUS_PARTIAL_CONTENT "\r\n" "Content-Length: %ld\r\n" "Content-Range: bytes %ld-%ld/%ld\r\n" "Content-Type: %s\r\n" "ETag: %s\r\n" "Last-Modified: %s\r\n" "Accept-Ranges: bytes\r\n" "%s%s%s" "%s" "Connection: close\r\n\r\n", (long)body_len, (long)range_start, (long)range_end, (long)st.st_size, mime_type(mime_path), etag, lm, content_encoding != NULL ? "Content-Encoding: " : "", content_encoding != NULL ? content_encoding : "", content_encoding != NULL ? "\r\nVary: Accept-Encoding\r\n" : "", g_server_hdr ? KHTTPD_SERVER_HEADER : ""); } else { n = snprintf(hdr, sizeof(hdr), KHTTPD_HTTP_VERSION " 200 " KHTTPD_STATUS_OK "\r\n" "Content-Length: %ld\r\n" "Content-Type: %s\r\n" "ETag: %s\r\n" "Last-Modified: %s\r\n" "Accept-Ranges: bytes\r\n" "%s%s%s" "%s" "Connection: close\r\n\r\n", (long)body_len, mime_type(mime_path), etag, lm, content_encoding != NULL ? "Content-Encoding: " : "", content_encoding != NULL ? content_encoding : "", content_encoding != NULL ? "\r\nVary: Accept-Encoding\r\n" : "", g_server_hdr ? KHTTPD_SERVER_HEADER : ""); } if (n > 0) write_hdr(fd, hdr, sizeof(hdr), n); } if (!head_only) { char buf[4096]; ssize_t nr; off_t left; if (have_range && lseek(ffd, range_start, SEEK_SET) < 0) { close(ffd); return 500; } left = body_len; while (left > 0 && (nr = read(ffd, buf, left < (off_t)sizeof(buf) ? (size_t)left : sizeof(buf))) > 0) { if (write_all(fd, buf, (size_t)nr) < 0) break; left -= nr; } } close(ffd); return status; } static int serve_dir(int fd, const char *fs_path, const char *req_path, int head_only) { char hdr[RESP_BUF]; char pathbuf[PATH_MAX]; char parent[PATH_MAX]; char urlbuf[PATH_MAX]; struct dirent *de; struct stat dst; struct stat lst; struct stat st; DIR *d; int n; static const char head1[] = "\n" "Index of "; static const char head2[] = ""; static const char css1[] = ""; static const char head3[] = "

Index of "; static const char head4[] = "

    \n"; static const char tail[] = "
\n"; static const char link1[] = "
  • "; static const char link3[] = "
  • \n"; static const char parentlink[] = "\">..\n"; d = opendir(fs_path); if (d == NULL) { if (errno == EACCES) return 403; return 404; } if (lstat(fs_path, &lst) < 0 || fstat(dirfd(d), &dst) < 0) { closedir(d); return 500; } if (!S_ISDIR(lst.st_mode) || !S_ISDIR(dst.st_mode) || lst.st_dev != dst.st_dev || lst.st_ino != dst.st_ino) { closedir(d); return 403; } n = snprintf(hdr, sizeof(hdr), KHTTPD_HTTP_VERSION " 200 " KHTTPD_STATUS_OK "\r\n" "Content-Type: text/html\r\n" "%s" "Connection: close\r\n\r\n", g_server_hdr ? KHTTPD_SERVER_HEADER : ""); if (n > 0) write_hdr(fd, hdr, sizeof(hdr), n); if (head_only) { closedir(d); return 200; } if (write_all(fd, head1, sizeof(head1) - 1) < 0) { closedir(d); return 200; } write_html_escaped(fd, req_path); if (write_all(fd, head2, sizeof(head2) - 1) < 0) { closedir(d); return 200; } if (g_css != NULL) { if (write_all(fd, css1, sizeof(css1) - 1) < 0) { closedir(d); return 200; } write_html_escaped(fd, g_css); if (write_all(fd, css2, sizeof(css2) - 1) < 0) { closedir(d); return 200; } } if (write_all(fd, head3, sizeof(head3) - 1) < 0) { closedir(d); return 200; } write_html_escaped(fd, req_path); if (write_all(fd, head4, sizeof(head4) - 1) < 0) { closedir(d); return 200; } if (strcmp(req_path, "/") != 0) { parent_url(req_path, parent, sizeof(parent)); if (write_all(fd, link1, sizeof(link1) - 1) < 0) { closedir(d); return 200; } write_url_escaped(fd, parent); if (write_all(fd, parentlink, sizeof(parentlink) - 1) < 0) { closedir(d); return 200; } } while ((de = readdir(d)) != NULL) { if (strcmp(de->d_name, ".") == 0) continue; if (strcmp(de->d_name, "..") == 0) continue; if (xstrlcpy(pathbuf, fs_path, sizeof(pathbuf)) >= sizeof(pathbuf)) continue; if (xstrlcat(pathbuf, "/", sizeof(pathbuf)) >= sizeof(pathbuf)) continue; if (xstrlcat(pathbuf, de->d_name, sizeof(pathbuf)) >= sizeof(pathbuf)) continue; if (xstrlcpy(urlbuf, req_path, sizeof(urlbuf)) >= sizeof(urlbuf)) continue; if (xstrlcat(urlbuf, de->d_name, sizeof(urlbuf)) >= sizeof(urlbuf)) continue; if (stat(pathbuf, &st) < 0) continue; if (write_all(fd, link1, sizeof(link1) - 1) < 0) break; write_url_escaped(fd, urlbuf); if (S_ISDIR(st.st_mode)) if (write_all(fd, "/", 1) < 0) break; if (write_all(fd, link2, sizeof(link2) - 1) < 0) break; write_html_escaped(fd, de->d_name); if (S_ISDIR(st.st_mode)) if (write_all(fd, "/", 1) < 0) break; if (write_all(fd, link3, sizeof(link3) - 1) < 0) break; } (void)write_all(fd, tail, sizeof(tail) - 1); closedir(d); return 200; } static void handle_request(int cfd, const char *req) { char accept_encoding[256]; char file[PATH_MAX]; char if_none_match[256]; char line[REQ_BUF]; char *query; char range_hdr[128]; char method[8]; char path[PATH_MAX]; const char *hdrp; const char *line_end; const char *sp1; const char *sp2; int has_if_modified_since; int head_only; int host_count; int is_http11; time_t if_modified_since; int rc; accept_encoding[0] = '\0'; if_none_match[0] = '\0'; range_hdr[0] = '\0'; has_if_modified_since = 0; if_modified_since = 0; host_count = 0; is_http11 = 0; line_end = strstr(req, "\r\n"); if (line_end == NULL) line_end = strchr(req, '\n'); if (line_end == NULL) { send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST); return; } if ((size_t)(line_end - req) >= sizeof(line)) { send_simple(cfd, 414, KHTTPD_MSG_URI_TOO_LONG); return; } memcpy(line, req, (size_t)(line_end - req)); line[line_end - req] = '\0'; sp1 = strchr(line, ' '); if (sp1 == NULL || sp1 == line || (size_t)(sp1 - line) >= sizeof(method)) { send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST); return; } memcpy(method, line, (size_t)(sp1 - line)); method[sp1 - line] = '\0'; while (*sp1 == ' ') sp1++; sp2 = strchr(sp1, ' '); if (sp2 == NULL || sp2 == sp1) { send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST); return; } if ((size_t)(sp2 - sp1) >= sizeof(path)) { send_simple(cfd, 414, KHTTPD_MSG_URI_TOO_LONG); return; } memcpy(path, sp1, (size_t)(sp2 - sp1)); path[sp2 - sp1] = '\0'; while (*sp2 == ' ') sp2++; if (strcmp(sp2, "HTTP/1.1") == 0) is_http11 = 1; else if (strcmp(sp2, "HTTP/1.0") != 0) { send_simple(cfd, 505, KHTTPD_MSG_HTTP_UNSUPPORTED); return; } if (strcmp(method, "GET") == 0) head_only = 0; else if (strcmp(method, "HEAD") == 0) head_only = 1; else { send_simple(cfd, 405, KHTTPD_MSG_METHOD_NOT_ALLOWED); return; } query = strchr(path, '?'); if (query != NULL) { *query = '\0'; } if (bad_path(path)) { send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST); return; } if (xstrlcpy(file, g_root, sizeof(file)) >= sizeof(file) || xstrlcat(file, path, sizeof(file)) >= sizeof(file)) { send_simple(cfd, 414, KHTTPD_MSG_URI_TOO_LONG); return; } hdrp = line_end; if (hdrp[0] == '\r' && hdrp[1] == '\n') hdrp += 2; else if (hdrp[0] == '\n') hdrp++; while (hdrp != NULL && *hdrp != '\0') { char *colon; const char *eol; const char *val; size_t llen; eol = strchr(hdrp, '\n'); if (eol == NULL) break; llen = (size_t)(eol - hdrp); if (llen > 0 && hdrp[llen - 1] == '\r') llen--; if (llen == 0) break; if (llen >= sizeof(line)) llen = sizeof(line) - 1; memcpy(line, hdrp, llen); line[llen] = '\0'; colon = strchr(line, ':'); if (colon == NULL) { send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST); return; } colon[0] = '\0'; val = colon + 1; while (*val == ' ' || *val == '\t') val++; if (strcasecmp(line, "Accept-Encoding") == 0) { xstrlcpy(accept_encoding, val, sizeof(accept_encoding)); } else if (strcasecmp(line, "If-None-Match") == 0) { xstrlcpy(if_none_match, val, sizeof(if_none_match)); } else if (strcasecmp(line, "If-Modified-Since") == 0) { has_if_modified_since = parse_http_date(val, &if_modified_since); } else if (strcasecmp(line, "Range") == 0) { xstrlcpy(range_hdr, val, sizeof(range_hdr)); } else if (strcasecmp(line, "Host") == 0) { if (*val == '\0') { send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST); return; } host_count++; } hdrp = eol + 1; } if (is_http11 && host_count != 1) { send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST); return; } { char index[PATH_MAX]; char resolved_file[PATH_MAX]; char reqdir[PATH_MAX]; const char *slash; struct stat ist; struct stat st; size_t len; int rrc; rrc = resolve_path_under_root(file, resolved_file); if (rrc == 0 && stat(resolved_file, &st) == 0 && S_ISDIR(st.st_mode)) { len = strlen(path); slash = (len > 0 && path[len - 1] == '/') ? "" : "/"; if (xstrlcpy(reqdir, path, sizeof(reqdir)) >= sizeof(reqdir) || xstrlcat(reqdir, slash, sizeof(reqdir)) >= sizeof(reqdir)) { send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST); return; } if (path[len - 1] != '/') { send_redirect(cfd, reqdir); return; } len = strlen(resolved_file); slash = (len > 0 && resolved_file[len - 1] == '/') ? "" : "/"; if (xstrlcpy(index, resolved_file, sizeof(index)) >= sizeof(index) || xstrlcat(index, slash, sizeof(index)) >= sizeof(index) || xstrlcat(index, g_index, sizeof(index)) >= sizeof(index)) { send_simple(cfd, 400, KHTTPD_MSG_BAD_REQUEST); return; } if (stat(index, &ist) == 0 && S_ISREG(ist.st_mode)) { rc = serve_file(cfd, index, index, head_only, accept_encoding, if_none_match, has_if_modified_since, if_modified_since, range_hdr); } else if (g_dirlist) rc = serve_dir(cfd, resolved_file, reqdir, head_only); else rc = 403; if (rc != 200 && rc != 206 && rc != 304 && rc != 416) { if (rc == 404) send_simple(cfd, 404, KHTTPD_MSG_NOT_FOUND); else if (rc == 403) send_simple(cfd, 403, KHTTPD_MSG_FORBIDDEN); else if (rc == 414) send_simple(cfd, 414, KHTTPD_MSG_URI_TOO_LONG); else send_simple(cfd, 500, KHTTPD_MSG_SERVER_ERROR); } return; } } { rc = serve_file(cfd, file, file, head_only, accept_encoding, if_none_match, has_if_modified_since, if_modified_since, range_hdr); } if (rc != 200 && rc != 206 && rc != 304 && rc != 416) { if (rc == 404) send_simple(cfd, 404, KHTTPD_MSG_NOT_FOUND); else if (rc == 403) send_simple(cfd, 403, KHTTPD_MSG_FORBIDDEN); else if (rc == 414) send_simple(cfd, 414, KHTTPD_MSG_URI_TOO_LONG); else send_simple(cfd, 500, KHTTPD_MSG_SERVER_ERROR); } } static int setup_listener(const char *addr, int port) { struct sockaddr_in sa4; int fd; int yes; fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) err(1, "socket"); yes = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); memset(&sa4, 0, sizeof(sa4)); sa4.sin_family = AF_INET; sa4.sin_port = htons((unsigned short)port); if (addr != NULL) { if (inet_pton(AF_INET, addr, &sa4.sin_addr) != 1) errx(1, "bad addr: %s", addr); } else sa4.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(fd, (struct sockaddr *)&sa4, sizeof(sa4)) < 0) err(1, "bind"); if (listen(fd, g_max_conn) < 0) err(1, "listen"); return fd; } int main(int argc, char **argv) { struct client *clients; struct pollfd *pfds; pthread_t *workers; const char *addr; const char *group; const char *root; const char *user; int cur_conn; int do_chroot; int have_gid; int have_uid; int i; int idx; int lfd; int opt; int port; int j; int ti; gid_t drop_gid; uid_t drop_uid; if (argv[0] != NULL) g_progname = argv[0]; addr = NULL; root = "."; do_chroot = 0; have_uid = 0; have_gid = 0; user = NULL; group = NULL; port = DEFAULT_PORT; cur_conn = 0; for (i = 1; i < argc; i++) { if (strcmp(argv[i], "--css") != 0) continue; if (i + 1 >= argc) usage(); g_css = argv[i + 1]; for (j = i; j + 2 <= argc; j++) argv[j] = argv[j + 2]; argc -= 2; i--; } while ((opt = getopt(argc, argv, "DHMchla:b:g:i:m:p:r:t:u:T:")) != -1) { switch (opt) { case 'D': daemonize(); break; case 'H': g_server_hdr = 1; break; case 'M': g_plain = 1; break; case 'c': do_chroot = 1; break; case 'h': usage(); break; case 'l': g_dirlist = 1; break; case 'a': addr = optarg; break; case 'b': g_max_hdr = (size_t)parse_int(optarg, 1, REQ_BUF - 1); break; case 'g': group = optarg; break; case 'i': g_index = optarg; break; case 'm': g_max_conn = parse_int(optarg, 1, 4096); break; case 'p': port = parse_int(optarg, 1, 65535); break; case 'r': root = optarg; break; case 't': g_timeout = parse_int(optarg, 1, 3600); break; case 'u': user = optarg; break; case 'T': g_threads = parse_int(optarg, 1, 256); break; default: usage(); } } argc -= optind; if (argc != 0) usage(); g_max_jobs = g_max_conn; signal(SIGPIPE, SIG_IGN); if (group != NULL) { drop_gid = parse_gid(group); have_gid = 1; } if (user != NULL) { drop_uid = parse_uid(user); have_uid = 1; } if (geteuid() == 0 && (!have_uid || drop_uid == 0)) errx(1, "refusing to run as root; use non-root -u"); if (do_chroot) { if (chroot(root) < 0) err(1, "chroot"); if (chdir("/") < 0) err(1, "chdir"); root = "."; } if (realpath(root, g_root_buf) == NULL) err(1, "realpath: %s", root); g_root = g_root_buf; #ifdef __OpenBSD__ if (unveil(g_root, "r") < 0) err(1, "unveil"); if (unveil(NULL, NULL) < 0) err(1, "unveil"); #endif if (have_gid) { if (have_uid) { struct passwd *pw; pw = getpwuid(drop_uid); if (pw == NULL) errx(1, "unknown uid: %lu", (unsigned long)drop_uid); if (initgroups(pw->pw_name, drop_gid) < 0) err(1, "initgroups"); } else { if (setgroups(0, NULL) < 0) err(1, "setgroups"); } if (setgid(drop_gid) < 0) err(1, "setgid"); } if (have_uid) { if (!have_gid) { struct passwd *pw; pw = getpwuid(drop_uid); if (pw == NULL) errx(1, "unknown uid: %lu", (unsigned long)drop_uid); if (initgroups(pw->pw_name, pw->pw_gid) < 0) err(1, "initgroups"); if (setgid(pw->pw_gid) < 0) err(1, "setgid"); } if (setuid(drop_uid) < 0) err(1, "setuid"); } #ifdef __OpenBSD__ if (pledge("stdio rpath inet", NULL) < 0) err(1, "pledge"); #endif lfd = setup_listener(addr, port); if (set_nonblock(lfd) < 0) err(1, "nonblock"); clients = calloc((size_t)g_max_conn, sizeof(struct client)); if (clients == NULL) err(1, "calloc"); pfds = calloc((size_t)g_max_conn + 1, sizeof(struct pollfd)); if (pfds == NULL) err(1, "calloc"); workers = calloc((size_t)g_threads, sizeof(*workers)); if (workers == NULL) err(1, "calloc"); for (ti = 0; ti < g_threads; ti++) { if (pthread_create(&workers[ti], NULL, worker_main, NULL) != 0) err(1, "pthread_create"); (void)pthread_detach(workers[ti]); } for (;;) { time_t now; int nready; pfds[0].fd = lfd; pfds[0].events = POLLIN; pfds[0].revents = 0; idx = 1; for (i = 0; i < g_max_conn; i++) { int used; int fd; pthread_mutex_lock(&g_conn_mtx); used = clients[i].used; fd = clients[i].fd; pthread_mutex_unlock(&g_conn_mtx); if (used != 1) continue; pfds[idx].fd = fd; pfds[idx].events = POLLIN; pfds[idx].revents = 0; idx++; } nready = poll(pfds, (nfds_t)idx, g_timeout > 0 ? 1000 : -1); if (nready < 0) { if (errno == EINTR) continue; err(1, "poll"); } if (pfds[0].revents & POLLIN) { int cfd; int slot; for (;;) { cfd = accept(lfd, NULL, NULL); if (cfd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) break; break; } if (set_nonblock(cfd) < 0) { close(cfd); continue; } pthread_mutex_lock(&g_conn_mtx); if (cur_conn >= g_max_conn) slot = -1; else { slot = -2; for (i = 0; i < g_max_conn; i++) { if (clients[i].used == 0) { slot = i; break; } } if (slot >= 0) { clients[slot].fd = cfd; clients[slot].used = 1; clients[slot].len = 0; clients[slot].last = time(NULL); cur_conn++; } } pthread_mutex_unlock(&g_conn_mtx); if (slot == -1) { send_simple(cfd, 500, KHTTPD_MSG_SERVER_BUSY); close(cfd); } else if (slot == -2) close(cfd); } } idx = 1; for (i = 0; i < g_max_conn; i++) { ssize_t n; int used; pthread_mutex_lock(&g_conn_mtx); used = clients[i].used; pthread_mutex_unlock(&g_conn_mtx); if (used != 1) continue; if ((pfds[idx].revents & (POLLIN | POLLHUP | POLLERR)) == 0) { idx++; continue; } n = read_request(clients[i].fd, &clients[i]); if (n == -3) { idx++; continue; } if (n == -2) { send_simple(clients[i].fd, 400, KHTTPD_MSG_BAD_REQUEST); close_client(clients, i, &cur_conn); idx++; continue; } if (n <= 0) { close_client(clients, i, &cur_conn); idx++; continue; } if (request_complete(clients[i].buf)) { if (enqueue_job(clients[i].fd, clients, i, &cur_conn, clients[i].buf) < 0) { send_simple(clients[i].fd, 500, KHTTPD_MSG_SERVER_ERROR); close_client(clients, i, &cur_conn); } else release_client(clients, i, &cur_conn); } else if (clients[i].len >= sizeof(clients[i].buf) - 2) { send_simple(clients[i].fd, 400, KHTTPD_MSG_BAD_REQUEST); close_client(clients, i, &cur_conn); } idx++; } if (g_timeout > 0) { now = time(NULL); for (i = 0; i < g_max_conn; i++) { int used; time_t last; pthread_mutex_lock(&g_conn_mtx); used = clients[i].used; last = clients[i].last; pthread_mutex_unlock(&g_conn_mtx); if (used != 1) continue; if ((int)(now - last) <= g_timeout) continue; close_client(clients, i, &cur_conn); } } } return 0; }