/* See LICENSE file for license details. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "arg.h" char *argv0; #include "config.h" enum stati { S_OK = 200, S_PARTIAL_CONTENT = 206, S_MOVED_PERMANENTLY = 301, S_BAD_REQUEST = 400, S_FORBIDDEN = 403, S_NOT_FOUND = 404, S_METHOD_NOT_ALLOWED = 405, S_REQUEST_TIMEOUT = 408, S_REQUEST_TOO_LARGE = 431, S_INTERNAL_SERVER_ERROR = 500, S_VERSION_NOT_SUPPORTED = 505, }; static char *statistr[] = { [S_OK] = "OK", [S_PARTIAL_CONTENT] = "Partial Content", [S_MOVED_PERMANENTLY] = "Moved Permanently", [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_REQUEST_TOO_LARGE] = "Request Header Fields Too Large", [S_INTERNAL_SERVER_ERROR] = "Internal Server Error", [S_VERSION_NOT_SUPPORTED] = "HTTP Version not supported", }; #undef MIN #define MIN(x,y) ((x) < (y) ? (x) : (y)) static char * timestamp(time_t t) { static char s[30]; if (!t) t = time(NULL); strftime(s, sizeof(s), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&t)); return s; } static int sendstatus(enum stati code, int fd, ...) { va_list ap; char buf[4096]; size_t written, buflen; ssize_t ret; long lower, upper, size; buflen = snprintf(buf, sizeof(buf), "HTTP/1.1 %d %s\r\n", code, statistr[code]); buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "Date: %s\r\n", timestamp(0)); va_start(ap, fd); switch (code) { case S_OK: /* arg-list: mime, size */ buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "Content-Type: %s\r\n", va_arg(ap, char *)); if ((size = va_arg(ap, long)) >= 0) { buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "Content-Length: %ld\r\n", size); } break; case S_PARTIAL_CONTENT: /* arg-list: mime, lower, upper, size */ buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "Content-Type: %s\r\n", va_arg(ap, char *)); lower = va_arg(ap, long); upper = va_arg(ap, long); size = va_arg(ap, long); buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "Content-Range: bytes %ld-%ld/%ld\r\n", lower, upper, size); buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "Content-Length: %ld\r\n", (upper - lower) + 1); break; case S_MOVED_PERMANENTLY: /* arg-list: host, url */ if (!strcmp(port, "80")) { buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "Location: http://%s%s\r\n", va_arg(ap, char *), va_arg(ap, char *)); } else { buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "Location: http://%s:%s%s\r\n", va_arg(ap, char *), port, va_arg(ap, char *)); } break; case S_METHOD_NOT_ALLOWED: /* arg-list: none */ buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "Allow: GET\r\n"); break; default: break; } va_end(ap); buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "Connection: close\r\n"); if (code != S_OK && code != S_PARTIAL_CONTENT) { buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "Content-Type: text/html\r\n"); buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "\r\n\r\n\r\n" "\t%d %s" "\r\n\t

%d %s

\r\n" "\r\n", code, statistr[code], code, statistr[code]); } else { buflen += snprintf(buf + buflen, sizeof(buf) - buflen, "\r\n"); } for (written = 0; buflen > 0; written += ret, buflen -= ret) { if ((ret = write(fd, buf + written, buflen)) < 0) { code = S_REQUEST_TIMEOUT; break; } } return code; } static size_t 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'; return i; } static size_t encode(char src[PATH_MAX], char dest[PATH_MAX]) { size_t i; char *s; for (s = src, i = 0; *s; s++) { if (isalnum(*s) || *s == '~' || *s == '-' || *s == '.' || *s == '_' || *s > 127) { i += snprintf(dest + i, PATH_MAX - i, "%%%02X", *s); } else { dest[i] = *s; i++; } } return 0; } static int listdir(char *dir, int fd) { struct dirent **e = NULL; static char buf[BUFSIZ]; size_t buflen; ssize_t bread, written; int dirlen, ret, i; if ((dirlen = scandir(dir, &e, NULL, alphasort)) < 0) { return sendstatus(S_FORBIDDEN, fd); } if ((ret = sendstatus(S_OK, fd, "text/html", (long)-1)) != S_OK) { return ret; } if ((buflen = snprintf(buf, sizeof(buf), "\r\n" "\r\nIndex of %s" "\r\n\r\n" "..
\r\n", dir)) >= sizeof(buf)) { return S_INTERNAL_SERVER_ERROR; } written = 0; while (buflen > 0) { if ((bread = write(fd, buf + written, buflen)) < 0) { return S_REQUEST_TIMEOUT; } written += bread; buflen -= bread; } for (i = 0; i < dirlen; i++) { if (e[i]->d_name[0] == '.') { /* hidden files, ., .. */ continue; } if ((buflen = snprintf(buf, sizeof(buf), "%s
\r\n", e[i]->d_name, e[i]->d_name)) >= sizeof(buf)) { return S_INTERNAL_SERVER_ERROR; } written = 0; while (buflen > 0) { if ((bread = write(fd, buf + written, buflen)) < 0) { return S_REQUEST_TIMEOUT; } written += bread; buflen -= bread; } } if ((buflen = snprintf(buf, sizeof(buf), "\r\n\r\n")) >= sizeof(buf)) { return S_INTERNAL_SERVER_ERROR; } written = 0; while (buflen > 0) { if ((bread = write(fd, buf + written, buflen)) < 0) { return S_REQUEST_TIMEOUT; } written += bread; buflen -= bread; } return S_OK; } static int handle(int infd, char **url) { FILE *fp; struct stat st; size_t reqlen, urllen, i; ssize_t off, buflen, written; long lower, upper, fsize, remaining; int needredirect, ret; static char req[MAXREQLEN], buf[BUFSIZ], urlenc[PATH_MAX], urldec[PATH_MAX], urldecnorm[PATH_MAX], urldecnormind[PATH_MAX], reqhost[256], range[128], modsince[30]; char *p, *q, *mime; /* get request header */ for (reqlen = 0; ;) { if ((off = read(infd, req + reqlen, MAXREQLEN - reqlen)) < 0) { return sendstatus(S_REQUEST_TIMEOUT, infd); } else if (off == 0) { break; } reqlen += off; if (reqlen >= 4 && !memcmp(req + reqlen - 4, "\r\n\r\n", 4)) { break; } if (reqlen == MAXREQLEN) { return sendstatus(S_REQUEST_TOO_LARGE, infd); } } if (reqlen < 2) { return sendstatus(S_BAD_REQUEST, infd); } reqlen -= 2; /* remove last \r\n */ req[reqlen] = '\0'; /* make it safe */ /* parse request line */ if (reqlen < 3) { return sendstatus(S_BAD_REQUEST, infd); } else if (strncmp(req, "GET", sizeof("GET") - 1)) { return sendstatus(S_METHOD_NOT_ALLOWED, infd); } else if (req[3] != ' ') { return sendstatus(S_BAD_REQUEST, infd); } for (p = req + sizeof("GET ") - 1; *p && *p != ' '; p++) ; if (!*p) { return sendstatus(S_BAD_REQUEST, infd); } *p = '\0'; if (snprintf(urlenc, sizeof(urlenc), "%s", req + sizeof("GET ") - 1) >= sizeof(urlenc)) { return sendstatus(S_BAD_REQUEST, infd); } *url = urldecnorm; if (!strlen(urlenc)) { return sendstatus(S_BAD_REQUEST, infd); } p += sizeof(" ") - 1; if (strncmp(p, "HTTP/", sizeof("HTTP/") - 1)) { return sendstatus(S_BAD_REQUEST, infd); } p += sizeof("HTTP/") - 1; if (strncmp(p, "1.0", sizeof("1.0") - 1) && strncmp(p, "1.1", sizeof("1.1") - 1)) { return sendstatus(S_VERSION_NOT_SUPPORTED, infd); } p += sizeof("1.*") - 1; if (strncmp(p, "\r\n", sizeof("\r\n") - 1)) { return sendstatus(S_BAD_REQUEST, infd); } p += sizeof("\r\n") - 1; /* parse header fields */ for (; (q = strstr(p, "\r\n")); p = q + sizeof("\r\n") - 1) { *q = '\0'; if (!strncmp(p, "Host:", sizeof("Host:") - 1)) { p += sizeof("Host:") - 1; while (isspace(*p)) { p++; } if (snprintf(reqhost, sizeof(reqhost), "%s", p) >= sizeof(reqhost)) { return sendstatus(S_INTERNAL_SERVER_ERROR, infd); } } else if (!strncmp(p, "Range:", sizeof("Range:") - 1)) { p += sizeof("Range:") - 1; while (isspace(*p)) { p++; } if (snprintf(range, sizeof(range), "%s", p) >= sizeof(range)) { return sendstatus(S_INTERNAL_SERVER_ERROR, infd); } } else if (!strncmp(p, "If-Modified-Since:", sizeof("If-Modified-Since:") - 1)) { p+= sizeof("If-Modified-Since:") - 1; while (isspace(*p)) { p++; } if (snprintf(modsince, sizeof(modsince), "%s", p) >= sizeof(modsince)) { return sendstatus(S_INTERNAL_SERVER_ERROR, infd); } } } /* normalization */ needredirect = 0; decode(urlenc, urldec); if (!realpath(urldec, urldecnorm)) { /* todo: break up the cases */ return sendstatus((errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND, infd); } /* hidden path? */ if (urldecnorm[0] == '.' || strstr(urldecnorm, "/.")) { return sendstatus(S_FORBIDDEN, infd); } /* check if file or directory */ if (stat(urldecnorm, &st) < 0) { /* todo: break up the cases */ return sendstatus(S_NOT_FOUND, infd); } if (S_ISDIR(st.st_mode)) { /* add / at the end, was removed by realpath */ urllen = strlen(urldecnorm); if (urldecnorm[urllen - 1] != '/') { urldecnorm[urllen + 1] = '\0'; urldecnorm[urllen] = '/'; } /* is a / at the end on the raw string? */ urllen = strlen(urldec); if (urldec[urllen - 1] != '/') { needredirect = 1; } else if (!needredirect) { /* check index */ if (snprintf(urldecnormind, sizeof(urldecnormind), "%s/%s", urldecnorm, docindex) >= sizeof(urldecnorm)) { return sendstatus(S_BAD_REQUEST, infd); } if (stat(urldecnormind, &st) < 0) { /* no index, serve dir */ if (!listdirs) { return sendstatus(S_FORBIDDEN, infd); } return listdir(urldecnorm, infd); } } } if (strcmp(urldec, urldecnorm)) { needredirect = 1; } if (needredirect) { encode(urldecnorm, urlenc); return sendstatus(S_MOVED_PERMANENTLY, infd, urlenc, reqhost[0] ? reqhost : host); } /* range */ lower = 0; upper = LONG_MAX; if (range[0]) { if (strncmp(range, "bytes=", sizeof("bytes=") - 1)) { return sendstatus(S_BAD_REQUEST, infd); } p = range + sizeof("bytes=") - 1; if (!(q = strchr(p, '-'))) { return sendstatus(S_BAD_REQUEST, infd); } *(q++) = '\0'; if (p[0]) { lower = atoi(p); } if (q[0]) { upper = atoi(q); } } /* serve file */ if (!(fp = fopen(urldecnorm, "r"))) { return sendstatus(S_FORBIDDEN, infd); } mime = "text/plain"; if ((p = strrchr(urldecnorm, '.'))) { for (i = 0; i < sizeof(mimes)/sizeof(*mimes); i++) { if (!strcmp(mimes[i].ext, p + 1)) { mime = mimes[i].type; break; } } } if (fseek(fp, 0, SEEK_END) || (fsize = ftell(fp)) < 0) { return sendstatus(S_INTERNAL_SERVER_ERROR, infd); } rewind(fp); if (fsize && upper > fsize) { upper = fsize - 1; } if (fseek(fp, lower, SEEK_SET)) { return sendstatus(S_INTERNAL_SERVER_ERROR, infd); } if (!range[0]) { if ((ret = sendstatus(S_OK, infd, mime, (long)fsize)) != S_OK) { return ret; } } else { if ((ret = sendstatus(S_PARTIAL_CONTENT, infd, mime, lower, upper, fsize)) != S_PARTIAL_CONTENT) { return ret; } } remaining = (upper - lower) + 1; while ((buflen = fread(buf, 1, MIN(sizeof(buf), remaining), fp))) { remaining -= buflen; if (buflen < 0) { return S_INTERNAL_SERVER_ERROR; } p = buf; while (buflen > 0) { written = write(infd, p, buflen); if (written <= 0) { return S_REQUEST_TIMEOUT; } buflen -= written; p += written; } } return S_OK; } static void serve(int insock) { struct sockaddr_storage in_sa; struct timeval tv; pid_t p; socklen_t in_sa_len; time_t t; enum stati status; int infd; char inip4[INET_ADDRSTRLEN], inip6[INET6_ADDRSTRLEN], *url = "", tstmp[25]; while (1) { /* accept incoming connections */ in_sa_len = sizeof(in_sa); if ((infd = accept(insock, (struct sockaddr *)&in_sa, &in_sa_len)) < 0) { fprintf(stderr, "%s: accept: %s\n", argv0, strerror(errno)); continue; } switch ((p = fork())) { case -1: fprintf(stderr, "%s: fork: %s", argv0, strerror(errno)); break; case 0: close(insock); /* set connection timeout */ tv.tv_sec = 30; tv.tv_usec = 0; if (setsockopt(infd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0 || setsockopt(infd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0) { fprintf(stderr, "%s: setsockopt: %s\n", argv0, strerror(errno)); return; } status = handle(infd, &url); /* write output to log */ t = time(NULL); strftime(tstmp, sizeof(tstmp), "%Y-%m-%dT%H:%M:%S", gmtime(&t)); if (in_sa.ss_family == AF_INET) { inet_ntop(AF_INET, &(((struct sockaddr_in *)&in_sa)->sin_addr), inip4, sizeof(inip4)); printf("%s\t%s\t%d\t%s\n", tstmp, inip4, status, url); } else { inet_ntop(AF_INET6, &(((struct sockaddr_in6*)&in_sa)->sin6_addr), inip6, sizeof(inip6)); printf("%s\t%s\t%d\t%s\n", tstmp, inip6, status, url); } /* clean up and finish */ shutdown(infd, SHUT_RD); shutdown(infd, SHUT_WR); close(infd); _exit(0); default: close(infd); } } } void die(const char *errstr, ...) { va_list ap; va_start(ap, errstr); vfprintf(stderr, errstr, ap); va_end(ap); exit(1); } static int getipsock(void) { struct addrinfo hints, *ai, *p; int ret, insock = 0, yes; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if ((ret = getaddrinfo(host, port, &hints, &ai))) { die("%s: getaddrinfo: %s\n", argv0, gai_strerror(ret)); } for (yes = 1, p = ai; p; p = p->ai_next) { if ((insock = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) { continue; } if (setsockopt(insock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) < 0) { die("%s: setsockopt: %s\n", argv0, strerror(errno)); } if (bind(insock, p->ai_addr, p->ai_addrlen) < 0) { close(insock); continue; } break; } freeaddrinfo(ai); if (!p) { die("%s: failed to bind\n", argv0); } if (listen(insock, SOMAXCONN) < 0) { die("%s: listen: %s\n", argv0, strerror(errno)); } return insock; } static int getusock(char *udsname) { struct sockaddr_un addr; int insock; if ((insock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { die("%s: socket: %s\n", argv0, strerror(errno)); } memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; strncpy(addr.sun_path, udsname, sizeof(addr.sun_path) - 1); unlink(udsname); if (bind(insock, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { die("%s: bind: %s\n", argv0, strerror(errno)); } if (listen(insock, SOMAXCONN) < 0) { die("%s: listen: %s\n", argv0, strerror(errno)); } return insock; } static void usage(void) { die("usage: %s [-v] [[[-h host] [-p port]] | [-U udsocket]] " "[-d dir] [-u user] [-g group]\n", argv0); } int main(int argc, char *argv[]) { struct passwd *pwd = NULL; struct group *grp = NULL; struct rlimit rlim; int insock; char *udsname = NULL; ARGBEGIN { case 'd': servedir = EARGF(usage()); break; case 'g': group = EARGF(usage()); break; case 'h': host = EARGF(usage()); break; case 'p': port = EARGF(usage()); break; case 'u': user = EARGF(usage()); break; case 'U': udsname = EARGF(usage()); break; case 'v': fputs("quark-"VERSION"\n", stderr); return 0; default: usage(); } ARGEND if (argc) usage(); /* reap children automatically */ if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) { fprintf(stderr, "%s: signal: Failed to set SIG_IGN on" "SIGCHLD\n", argv0); return 1; } /* raise the process limit */ rlim.rlim_cur = rlim.rlim_max = maxnprocs; if (setrlimit(RLIMIT_NPROC, &rlim) < 0) { fprintf(stderr, "%s: setrlimit RLIMIT_NPROC: %s\n", argv0, strerror(errno)); return 1; } /* validate user and group */ errno = 0; if (user && !(pwd = getpwnam(user))) { die("%s: invalid user %s\n", argv0, user); } errno = 0; if (group && !(grp = getgrnam(group))) { die("%s: invalid group %s\n", argv0, group); } /* bind socket */ insock = udsname ? getusock(udsname) : getipsock(); /* chroot */ if (chdir(servedir) < 0) { die("%s: chdir %s: %s\n", argv0, servedir, strerror(errno)); } if (chroot(".") < 0) { die("%s: chroot .: %s\n", argv0, strerror(errno)); } /* drop root */ if (grp && setgroups(1, &(grp->gr_gid)) < 0) { die("%s: setgroups: %s\n", argv0, strerror(errno)); } if (grp && setgid(grp->gr_gid) < 0) { die("%s: setgid: %s\n", argv0, strerror(errno)); } if (pwd && setuid(pwd->pw_uid) < 0) { die("%s: setuid: %s\n", argv0, strerror(errno)); } if (getuid() == 0) { die("%s: won't run as root user\n", argv0); } if (getgid() == 0) { die("%s: won't run as root group\n", argv0); } serve(insock); close(insock); return 0; }