#!/usr/bin/env bx use b_plain # FIXME: doesn't encode entities in dir listings, redirect message cstr version = "0" cstr listen_addr = "0.0.0.0" int listen_port = 80 cstr listen_addr_stunnel = "127.0.0.1" int listen_port_stunnel = 81 int listen_port_ssl = 443 # 8 cstr www_root = "/www" cstr default_user = "www-data" cstr default_host = "default" cstr server_admin = "sam@nipl.net" #cstr avfs_root = "/avfs" cstr avfs_root = NULL def avfs_char '^' def avfs_char_cstr "^" boolean allow_bots_avfs = 0 boolean allow_bots_query = 0 # TODO allow_bots_binary ? boolean allow_find = 0 boolean show_exe = 0 boolean show_exe = 0 boolean cgibin_exec = 1 boolean cgibin_secret = 1 boolean link_to_homepage = 0 cstr index_files[] = { "index.html", "index.cgi", "index.php", NULL } Def text_types_list "b", "bb", "c", "h", "cc", "cpp", "pl", "pm", "py", "rb", "java", "sh", "diff", "patch", "lyx", "tex", "tsv", "csv" Def bin_types_list "gz", "pnd" Def zip_types_list "zip", "tar", "tgz", "tar.gz", "tbz2", "tar.bz2", "tar.Z", "rar", "ar", "deb", "jar", "7z", "rpm", "lha", "zoo", "arj" Def z_types_list "gz", "bz2", "Z" Def bots "google", "slurp", "check_http", "bot", "hosttracker", "yandex", "spider", "presto", "scanner", "phpcrawl", "bot", "lwp" # use: stunnel -d 443 -r 81 -f # or see etc/ for stunnel4 + sslh config # use: avfsd -o allow_other /avfs # note - to be useful with web, avfs was recompiled to use '!' not '#' int n_servers = 1 #int n_servers = 2 int server_i def verbose void #def verbose warn def wlog sf hashtable *users User *defu hashtable _cache, *cache = &_cache int cache_n_buckets = 1009 off_t cache_max_entry_size = 4096 struct cache_item time_t mtime buffer body Main() # memlog_file() uid_t uid = geteuid() if uid != 0 error("%s must be run as root", program) Nice(-20) Clearenv() Setenv("PATH", "/usr/local/bin:/usr/bin:/bin") Setenv("SERVER_SOFTWARE", Format("%s/0", program)) Setenv("GATEWAY_INTERFACE", "CGI/1.1") Setenv("SERVER_ADMIN", server_admin) if avfs_root && !is_dir(avfs_root) avfs_root = NULL Setlinebuf(stdout) sched_busy = 16 block_size = 16*1024 # block_size = 1024 max_line_length = 2048 # listen_backlog = 1 # Sigdfl_all() ignore_pipe() each(i, SIGINT, SIGQUIT, SIGTERM, SIGHUP) sigact(i, term_handler) sigact(SIGUSR1, usr1_handler) date_rfc1123_init() text_mimetypes() bin_mimetypes() load_mimetypes() users = load_users() defu = Get(users, default_user) init(cache, hashtable, cstr_hash, cstr_eq, cache_n_buckets) new(l, listener_tcp, listen_addr, listen_port) new(w, httpd_launcher) sh(sock_p, l, w) new(l_https, listener_tcp, listen_addr_stunnel, listen_port_stunnel) new(w_https, httpd_launcher) sh(sock_p, l_https, w_https) Setuser(defu) for server_i=0; server_i < n_servers-1 ; ++server_i if Fork() == 0 break sched_init() start(l) ; start(w) start(l_https) ; start(w_https) run() Setuser_root() term_handler(int sig) kill(0, sig) exit(0) usr1_handler(int sig) use(sig) sched_exit() proc httpd_launcher() port sock_p in state sock_p sk repeat rd(in, sk) New(s, httpd, sk) start(s) proc httpd(sock *sk) port buffer in port buffer out port buffer fin port buffer fout state int fd = sk->fd state reader r state writer w state reader fr state writer fw state cat c1 state sockaddr_in sockname state socklen_t namelen = sizeof(sockaddr_in) state cstr scheme = "http" state in_addr server_addr state cstr server_addr_str state int server_port state in_addr remote_addr state cstr remote_addr_str state int remote_port verbose("server %d accepted new connection", server_i) # Rsleep(0.001) # FIXME shouldn't need to do this to get connections evenly distributed between the processes! if getsockname(fd, &sockname, &namelen) == 0 if sockname.sin_family == AF_INET server_addr = sockname.sin_addr server_addr_str = Strdup(inet_ntoa(server_addr)) int port = ntohs(sockname.sin_port) if listen_addr_stunnel && port == listen_port_stunnel scheme = "https" server_port = listen_port_ssl else server_port = port remote_addr = sock_in_addr(sk) remote_addr_str = Strdup(inet_ntoa(remote_addr)) remote_port = sock_in_port(sk) nodelay(fd) init(&r, reader, fd) init(&w, writer, fd) sh(buffer, &r, This) sh(buffer, This, &w) init(&in, buffer, block_size) init(&out, buffer, block_size) start(&r) ; start(&w) req state cstr s state char *c state size_t l state cstr method_str state http__method method state cstr url = NULL state cstr proto = NULL state cstr host = NULL state cstr path = NULL state cstr root = NULL int root_len = 0 state cstr fullpath = NULL state cstr query = NULL state cstr user = default_user state cstr pass = NULL state off_t reqlen = -1 state int file_fd = -1 state User *u state int code state cstr msg state cstr location = NULL state int private = 0 state int expire_already = 0 state cstr user_agent = NULL state cstr referer = NULL state boolean keep_alive = 1 state boolean statable = 0 decl(st, Stats) state boolean http1_1 = 1 state cstr body = NULL state time_t mtime = -1 state cstr mtype = "text/plain" state int status state boolean range_req = 0 state long long int byte0 = 0 state long long int byte1 state boolean content_range_req = 0 state long long int cr_byte0 = 0 state long long int cr_byte1 state off_t fullsize state off_t size state off_t offset state ssize_t count state int line_start = 0 state num response_time state pid_t child Setuser_default() # TODO make this part of process lib # exec_sshd() # stop() new(headers, vec, key_value, 32) repeat breadln(in, 0, c) if !buflen(&in) quit s = buf0(&in) l = c - s + 1 cstr_dos_to_unix(s) if *s break buffer_shift(&in, l) sched_set_time() cstr *words = split(s, ' ') if arylen(words) != 3 buffer_shift(&in, l) Free(words) bad method_str = Strdup(words[0]) method = http_which_method(method_str) url = Strdup(words[1]) ; proto = Strdup(words[2]) verbose("%s %s %s", method_str, url, scheme) Free(words) line_start = l # check proto if !strncmp(proto, "HTTP/", 5) if !strcmp(proto+5, "1.0") || proto[5] == '0' http1_1 = 0 keep_alive = 0 # read_headers repeat breadln(in, line_start, c) if buflen(&in) <= line_start quit s = b(&in, line_start) l = c - s + 1 cstr_dos_to_unix(s) line_start += l if !*s break verbose(s) cstr *words = split(s, ':', 2) if arylen(words) != 2 Free(words) bad cstr key = words[0], val = words[1] Free(words) while isspace(val[0]) ++val for char *i=key; *i; ++i if tween(*i, 'A', 'Z') *i += 'a' - 'A' key_value *kv = vec_push(headers) kv->k = Strdup(key) ; kv->v = Strdup(val) if !strcmp(key, "host") host = Strdup(val) eif !strcmp(key, "authorization") if !strncasecmp(val, "Basic ", 6) decl(base64, buffer) buffer_from_cstr(base64, val+6) new(o, buffer, 32) base64_decode_buffers(base64, o) user = buffer_to_cstr(o) cstr *user_pass = split(user, ':', 2) if arylen(user_pass) != 2 Free(user_pass) bad pass = user_pass[1] Free(user_pass) eif !strcmp(key, "user-agent") user_agent = Strdup(val) eif !strcmp(key, "referer") referer = Strdup(val) eif !strcmp(key, "content-length") reqlen = atoll(val) eif !strcmp(key, "connection") if !strcasecmp(val, "close") keep_alive = 0 eif !strcasecmp(val, "Keep-Alive") keep_alive = 1 eif !strcmp(key, "range") int n = sscanf(val, "bytes=%lld-%lld", &byte0, &byte1) # FIXME safe? if n range_req = 1 if n == 1 byte1 = -1 eif !strcmp(key, "content-range") int n = sscanf(val, "bytes=%lld-%lld", &cr_byte0, &cr_byte1) # FIXME safe? if n content_range_req = 1 if n == 1 cr_byte1 = -1 if host == NULL host = get_host_from_url(url) if host == NULL host = Strdup(default_host) path = get_path_from_url(url) root = path_cat(www_root, host) delimit(root, ':') root_len = strlen(root) # redirect to real vhost Stats str if !lstat(root, &str) if S_ISLNK(str.st_mode) cstr realhost = readlinks(Strdup(root), OE_CONT) cstr free_me = realhost size_t www_root_len = strlen(www_root) if cstr_begins_with(realhost, www_root) && realhost[www_root_len] == '/' realhost += www_root_len + 1 if !strchr(realhost, '/') location = Format("%s://%s%s", scheme, base_name(realhost), path) Free(free_me) loc Free(free_me) else notf fullpath = path_cat(root, *path == '/' ? path+1 : path) query = strchr(fullpath, '?') if query *query++ = '\0' cstr q2 = strchr(path, '?') if q2 *q2 = '\0' query = Strdup(query) if !allow_bots_query && is_bot(user_agent) forbid url_decode(path) url_decode(fullpath) fullpath = path_tidy(fullpath) unless(cstr_begins_with(fullpath, root) && fullpath[root_len] == '/') bad for char *i=fullpath; *i; ++i if *i == '/' && i[1] == '.' forbid # redirect /index.html -> / so each file has a single url if cstr_ends_with(url, "/index.html") location = Strdup(url) strrchr(location, '/')[1] = '\0' loc # set user and respond u = get(users, user) if user != default_user && !(u && pass && auth(u, pass)) reqauth if pass bzero(pass, strlen(pass)) if user != default_user Setuser_via_root(u) boolean checked_redirect = 0 # avfs virtual filesystem char real_path[PATH_MAX] if avfs_root char *p = strchr(fullpath, avfs_char) if p && !exists(fullpath) if !allow_bots_avfs if is_bot(user_agent) forbid char *subpath = Strdup(p) *p = '\0' if !realpath(fullpath, real_path) cstr target = readlink(fullpath) # FIXME improve this & merge with below if target && (strstr(target, "://") || cstr_begins_with(target, "mailto:")) # warn("realpath failed - trying %s", target) location = Format("%s%s%s%s", target, subpath, query?"?":"", query?query:"") Free(target) ; Free(subpath) loc Free(subpath) accerr if cstr_ends_with(fullpath, "/") && strcmp(real_path, "/") int rplen = strlen(real_path) real_path[rplen] = '/' ; real_path[rplen+1] = '\0' # redirect to real path if necessary+possible TODO improve if !strstr(real_path, "/.") && strcmp(fullpath, real_path) && cstr_begins_with(real_path, root) && real_path[root_len] == '/' location = Format("%s://%s%s%s%s%s", scheme, host, real_path+root_len, subpath, query?"?":"", query?query:"") Free(subpath) loc checked_redirect = 1 boolean can_read = test_read(real_path) # warn("test_read %s -> %d", real_path, can_read) Free(fullpath) fullpath = Format("%s%s%s", avfs_root, real_path, subpath) # warn("changed fullpath to %s for avfs", fullpath) if !can_read # avfs bug means if user can't read the # archive, might still be allowed access based # on the archive's perms. Don't allow that! Free(subpath) accerr Free(subpath) if !checked_redirect if !realpath(fullpath, real_path) cstr target = readlink(fullpath) if target && (strstr(target, "://") || cstr_begins_with(target, "mailto:")) # warn("realpath failed - trying %s", target) location = Format("%s%s%s", target, query?"?":"", query?query:"") Free(target) loc accerr if cstr_ends_with(fullpath, "/") && strcmp(real_path, "/") int rplen = strlen(real_path) real_path[rplen] = '/' ; real_path[rplen+1] = '\0' # warn("checking real_path %s for fullpath %s root %s root_len %d", real_path, fullpath, root, root_len) # redirect to real path if necessary+possible TODO improve if !strstr(real_path, "/.") && strcmp(fullpath, real_path) && cstr_begins_with(real_path, root) && real_path[root_len] == '/' location = Format("%s://%s%s%s%s", scheme, host, real_path+root_len, query?"?":"", query?query:"") loc # index files if fullpath[strlen(fullpath)-1] == '/' cstr *index for index = index_files; *index; ++index cstr fullpath_index = cstr_cat(fullpath, *index) statable = !stat(fullpath_index, st) if statable && S_ISREG(st->st_mode) Free(fullpath) fullpath = fullpath_index if method == HTTP_GET && !query && !strstr(*index, ".htm") Free(query) query = Strdup("") else Free(fullpath_index) if !statable statable = !stat(fullpath, st) verbose(" %s", fullpath) if statable && st->st_mode & S_IRUSR mtime = st->st_mtime fullsize = st->st_size eif among(method, HTTP_GET, HTTP_HEAD, HTTP_DELETE) if statable && !(st->st_mode & S_IRUSR) errno = EPERM accerr else fullsize = 0 if !(st->st_mode & S_IROTH) private = 1 # if !user_agent || strcasestr(user_agent, "MSIE") || !strcasestr(user_agent, "Mozilla/5.0") expire_already = 1 state cstr base = strrchr(fullpath, '/') state cstr ext = strrchr(base, '.') if ext ++ext else ext = "" state boolean is_php = cstr_eq(ext, "php") state boolean in_cgibin = cstr_begins_with(path, "/cgi-bin/") != NULL if S_ISREG(st->st_mode) && (st->st_mode & S_IXUSR || is_php) && (method == HTTP_POST || (method == HTTP_GET && (query || (cgibin_exec && in_cgibin)))) get_cgi() eif among(method, HTTP_PUT, HTTP_POST) put_file() eif method == HTTP_DELETE delete_file() eif !among(method, HTTP_GET, HTTP_HEAD) || reqlen > 0 bad eif cgibin_secret && in_cgibin forbid eif S_ISREG(st->st_mode) get_file() eif allow_find && S_ISDIR(st->st_mode) && query get_find() eif S_ISDIR(st->st_mode) get_dir() else notf done Setuser_default() Free(url) ; Free(proto) ; Free(host) ; Free(method_str) Free(root) ; Free(fullpath) ; Free(body) ; Free(query) if file_fd != -1 close(file_fd) ; file_fd = -1 key_value *i = vec0(headers) key_value *e = vecend(headers) for ; i!=e; ++i Free(i->k) ; Free(i->v) vec_free(headers) if user != default_user Free(user) if keep_alive buffer_shift(&in, line_start) req Free(server_addr_str) Free(remote_addr_str) shut shut bclose(out) quit quit verbose("") rm_fd(fd) sock_free(sk) Free(sk) buffer_free(&in) buffer_free(&out) Free(This) stop() mesg body = Format("%s: %d %s\r\n", program, code, msg) headers(code, msg, strlen(body)) bodymsg bodymsg discard_req() bcrlf(out) bflush(out) if method != HTTP_HEAD bprint(out, body) bflush(out) if buflen(&out) # error keep_alive = 0 done ok code = 200 ; msg = "OK" mesg created code = 201 ; msg = "Created" mesg bad code = 400 ; msg = "Bad Request" mesg forbid code = 403 ; msg = "Forbidden" mesg notf code = 404 ; msg = "Not Found" mesg badrng code = 416 ; msg = "Requested Range Not Satisfiable" mesg srverr code = 500 ; msg = "Internal Server Error" mesg reqauth if !cstr_eq(scheme, "https") https body = Format("%s: 401 Unauthorized\r\n", program) headers(401, "Unauthorized", strlen(body)) bsayf(out, "WWW-Authenticate: Basic realm=\"%s\"\r", host) bodymsg loc mtype = "text/html" body = Format("%s: redirected to %s\r\n", program, location, location) # headers(301, "Moved Permanently", strlen(body)) headers(302, "Found", strlen(body)) bsayf(out, "Location: %s\r", location) verbose(" %s", location) Free(location) bodymsg https if listen_port_ssl == 443 location = Format("https://%s%s%s%s", host, path, query ? "?" : "", query ? query : "") else location = Format("https://%s:%d%s%s", host, listen_port_ssl, path, query ? "?" : "", query ? query : "") loc accerr which errno EACCES reqauth EPERM reqauth ENOENT notf ENOTDIR notf ENAMETOOLONG bad else srverr def headers(code, msg, size) Setuser_default() # XXX this is BIG for a macro! response_time = sched_get_time() wlog("%lld\t%s\t%s\t%s\t%d %s\t%lld\t%s\t%s\t%s%s%s\t%s\t%s\t%s", (long long)response_time, host, user, remote_addr_str, code, msg, (long long)size, scheme, method_str, url, query?"?":"", query?query:"", referer?referer:"", location?location:"", user_agent?user_agent:"") bsayf(out, "HTTP/1.1 %d %s\r", code, msg) bsayf(out, "Date: %s\r", date_rfc1123((time_t)response_time)) if mtime != -1 bsayf(out, "Last-Modified: %s\r", date_rfc1123(mtime)) if range_req bsayf(out, "Content-Range: %lld-%lld/%lld\r", (long long int)byte0, (long long int)byte1, (long long int)fullsize) if size >= 0: # FIXME causes a compile warning bsayf(out, "Content-Length: %lld\r", (long long int)size) if code == 200 && expire_already bsay(out, "Cache-Control: private, no-cache, no-store\r") bsay(out, "Expires: 0\r") if mtype && cstr_begins_with(mtype, "text/") bsayf(out, "Content-Type: %s; charset=utf-8\r", mtype) eif mtype bsayf(out, "Content-Type: %s\r", mtype) bsayf(out, "Server: %s\r", program) if !http1_1 bsayf(out, "Connection: %s\r", keep_alive ? "Keep-Alive" : "close") def get_file() if *ext cstr _mtype = mimetype(ext) if _mtype mtype = _mtype state boolean in_cache = 0 state cache_item *citem = NULL if method == HTTP_GET # not HTTP_HEAD if fullsize < cache_max_entry_size && mtime < (time_t)sched_get_time() && user == default_user key_value *kv = kv(cache, fullpath, NULL) if kv->v == NULL kv->k = strdup(fullpath) citem = kv->v = Talloc(cache_item) init(&citem->body, buffer, st->st_size) else # warn("cache hit") citem = kv->v if mtime == citem->mtime in_cache = 1 if !in_cache citem->mtime = mtime buffer_set_size(&citem->body, fullsize) buffer_squeeze(&citem->body) file_fd = open(fullpath, O_RDONLY) if file_fd == -1 # XXX FIXME this dance to avoid freeing clobbered pointers sucks, # put it in a Del func or something? void *k = kv->k, *v = kv->v del(cache, fullpath) Free(k) ; Free(v) accerr read(file_fd, buf0(&citem->body), fullsize) # TODO don't ignore return value of read? close(file_fd) ; file_fd = -1 # warn("loaded %lld bytes into cache", (long long int)fullsize) in_cache = 1 else file_fd = open(fullpath, O_RDONLY) if file_fd == -1 accerr cloexec(file_fd) do_range_size() if range_req # if byte0 # Lseek(file_fd, byte0) headers(206, "Partial Content", size) else headers(200, "OK", size) bcrlf(out) # bflush(out) if method == HTTP_GET if in_cache struct iovec iov[2] iov[0].iov_base = buf0(&out) iov[0].iov_len = buflen(&out) iov[1].iov_base = b(&citem->body, byte0) iov[1].iov_len = size nonblock(fd, 0) Writev(fd, iov, 2) nonblock(fd, 1) bufclr(&out) # bwrite_direct(out, b(&citem->body, byte0), b(&citem->body, byte0+size)) else cork(fd) bflush(out) offset = byte0 while size # size_t bytes = imin(size, sendfile_block_size) count = sendfile(fd, file_fd, &offset, size) if count == -1 if among(errno, ECONNRESET, EPIPE) keep_alive = 0 break eif errno != EAGAIN swarning("sendfile %d %d %d %d", fd, file_fd, (int)offset, (int)size) break count = 0 # if count >= (ssize_t)block_size # nodelay(fd) if count < size write(fd) # else # yield() size -= count cork(fd, 0) # rm_fd(file_fd) close(file_fd) ; file_fd = -1 else bflush(out) def dir_ends_slash_or_redirect() if fullpath[strlen(fullpath)-1] != '/' location = Format("%s/%s%s", url, query ? "?" : "", query ? query : "") loc def get_dir() dir_ends_slash_or_redirect() vec *v = ls(fullpath) if !v accerr sort_vec(v, cstrp_cmp) new(b1, buffer, 256) new(b2, buffer, 256) new(b3, buffer, 256) # # main dir # Sprintf(b1, "./ \n", path, path) cstr *j = vec0(v) cstr *e = vecend(v) for ; j!=e; ++j if (*j)[0] == '.' continue cstr entpath = path_cat(fullpath, *j) int dir = 0 Stats ste if !stat(entpath, &ste) && S_ISDIR(ste.st_mode) dir = 1 if dir Sprintf(b1, "%s", *j, *j) if allow_find: Sprintf(b1, "/", *j) Sprintf(b1, " \n") else boolean exe = show_exe && ste.st_mode & S_IXUSR Sprintf(b2, "%s%s%s%s \n", *j, *j, exe?"*":"") if avfs_root && !strstr(*j, "/+") int zip = is_zip_file(*j) if zip int find = allow_find && zip > 1 Sprintf(b3, "%s%s%s%s \n", *j, avfs_char, find?"/":"", *j, find?"/":"") # hm, ugly! Free(entpath) ; Free(*j) vec_free(v) buffer_cat_cstr(b1, "\n

\n\n") buffer_cat_cstr(b2, "\n

\n\n") buffer_cat_cstr(b3, "\n

\n\n") buffer_cat_range(b1, buffer_range(b2)) buffer_cat_range(b1, buffer_range(b3)) buffer_free(b2) buffer_free(b3) if link_to_homepage: Sprintf(b1, "
%s

\n", scheme, host, host) # link to homepage mtype = "text/html" send_buffer(b1) def send_buffer(b0) fullsize = buflen(b0) do_range_size() if range_req headers(206, "Partial Content", size) else headers(200, "OK", size) bcrlf(out) bflush(out) if method == HTTP_GET # not HTTP_HEAD bwrite(out, b(b0, byte0), b(b0, size)) bflush(out) buffer_free(b0) if buflen(&out) # error keep_alive = 0 #def exec_sshd() # pid_t child = Fork() # if child == 0 # nonblock(fd, 0) # Dup2(fd, STDIN_FILENO) # Dup2(fd, STDOUT_FILENO) ## Dup2(fd, STDERR_FILENO) # # Sigdfl(SIGPIPE) # exec__warn_fail = 0 # Execlp("/usr/sbin/sshd", "/usr/sbin/sshd", "-i", NULL) # . # waitchild(child, status) def get_cgi() # FIXME! srverr # XXX remove this! state int cgi_sock[2] Socketpair(cgi_sock) # we copy request content in for GET and POST nonblock(cgi_sock[0]) nonblock(cgi_sock[1]) cloexec(cgi_sock[0]) cloexec(cgi_sock[1]) if add_fd(cgi_sock[0]) || add_fd(cgi_sock[1]) Close(cgi_sock[0]) ; Close(cgi_sock[1]) srverr state ssize_t req_read = buflen(&in)-line_start # warn("req_read: %d reqlen: %d", req_read, reqlen) if reqlen >= 0 && reqlen < req_read # warn("only part is of req") req_read = reqlen if user == default_user && getegid() != st->st_gid: reqauth child = Fork() if child == 0 Setuidgid_via_root(u) Setenv("USER", u->pw_name) Setenv("LOGNAME", u->pw_name) Setenv("HOME", u->pw_dir) # cstr rb1 = b(&in, line_start) # rb1[req_read] = '\0' # Setenv("REQUEST_BODY_1", rb1) # FIXME XXX not compatible, doesn't cope with \0 if query Setenv("QUERY_STRING", query) set_cgi_vars() nonblock(cgi_sock[1], 0) Dup2(cgi_sock[1], STDIN_FILENO) Dup2(cgi_sock[1], STDOUT_FILENO) # Dup2(cgi_sock[1], STDERR_FILENO) Sigdfl(SIGPIPE) exec__warn_fail = 0 dirbase d_b = dirbasename(Strdup(fullpath)) if chdir(d_b.dir) exit_exec_failed() if is_php Execlp("php", "php", fullpath, NULL) Execl(fullpath, fullpath, NULL) have_child(child) Setuser_default() Close(cgi_sock[1]) init(&fw, writer, cgi_sock[0]) sh(buffer, This, fout, &fw, in) init(&fout, buffer, block_size) start(&fw) bwrite(fout, b(&in, line_start), b(&in, line_start+req_read)) bflush(fout) line_start += req_read init(&fr, reader, cgi_sock[0]) init(&c1, cat, reqlen) # TODO a join or redirect function to simplify this plumbing stuff buffer save_in = in sh(buffer, &r, &c1) ; sh(buffer, &c1, &fw) This->in->d = save_in init(&c1.out->d, buffer, block_size) sh(buffer, &fr, out, This, fin) init(&fin, buffer, block_size) start(&fr) ; start(&c1) warn("about to read response from CGI script") keep_alive = 0 # FIXME use chunked encoding and keep_alive # TODO put this in its own proc? state int cgi_response_started = 0 repeat warn("looping reading response from CGI script") bread(fin) warn("1") if !buflen(&fin) break warn(" len: %d", buflen(&fin)) buffer_dump(&fin) if !cgi_response_started cgi_response_started = 1 mtime = -1 ; mtype = NULL headers(200, "OK", -1) warn("2") bwrite(out, buf0(&fin), bufend(&fin)) warn("3") bflush(out) warn("4") bufclr(&fin) if buflen(&out) # error keep_alive = 0 break warn("done reading response from CGI script") buffer_free(&fin) buffer_free(&fout) buffer_free(&c1.out->d) # FIXME this does not work well with keep-alive, because we don't count # the content length first. FIXME I should use the "chunked" encoding # if HTTP/1.1, eek. . warn("waiting for child to finish") drop_child(child) # FIXME this is dodgy waitchild(child, status) # waitchild fails or blocks if child already exited :/ warn("child finished") halt(&c1) ; halt(&fr) ; halt(&fw) # await(&c1) ; await(&fr) ; await(&fw) # FIXME busy waits! save_in = in sh(buffer, &r, This) in = save_in if status_execfailed(status) reqauth else # # This is a bit bogus, when can we use keep_alive for POST? # # keep_alive can work if the whole request was read by tachyon, # # if the CGI script gets to read on the socket, the next req # # would get buffered and lost. # # I would need a way for the script to pass any extra buffered # # input back to tachyon! unread() would be nice... # # keep_alive = keep_alive && reqlen >= 0 && req_read >= reqlen # warn("keep_alive: %d", keep_alive) if status != 0 srverr # maybe too late! def put_file() int open_opt = O_WRONLY|O_CREAT|O_NONBLOCK if content_range_req put_do_range() else if method == HTTP_PUT open_opt |= O_TRUNC else # method == HTTP_POST open_opt |= O_APPEND file_fd = open(fullpath, open_opt, 0666) Setuser_default() if content_range_req Lseek(file_fd, cr_byte0) if file_fd == -1 accerr if add_fd(file_fd) Close(file_fd) ; file_fd = -1 srverr cloexec(file_fd) init(&fw, writer, file_fd) sh(buffer, This, fout, &fw, in) # init(&fout, buffer, block_size) start(&fw) let(out_tmp, out) offset = 0 if buflen(&in) <= line_start push(in) if reqlen != 0 repeat pull(in) if buflen(&in) <= line_start break count = buflen(&in) if reqlen > 0 && (off_t)(offset + count) > reqlen count = reqlen - offset offset += count bwrite_direct(fout, b(&in, line_start), b(&in, line_start+count)) # bflush(fout) if buflen(&fout) # error break buffer_set_size(&in, line_start) if offset == reqlen break push(in) # buffer_free(&fout) XXX isn't this needed? out = out_tmp if content_range_req && method == HTTP_PUT && cr_byte1 >= fullsize-1 Ftruncate(file_fd, cr_byte0 + offset) if file_fd != -1 rm_fd(file_fd) if close(file_fd) swarning("close failed for file: %s", fullpath) file_fd = -1 if reqlen == -1 keep_alive = 0 eif offset < reqlen reqlen = 0 bad reqlen = 0 if statable ok else created def delete_file() int failed = remove(fullpath) if failed accerr ok def discard_req() if reqlen > 0 bread(in, reqlen) buffer_set_size(&in, line_start) def do_range_size() if range_req fix_byte_range(byte0) ; fix_byte_range(byte1) if byte1 >= fullsize byte1 = fullsize - 1 size = byte1 - byte0 + 1 if byte1 <= byte0 badrng else size = fullsize def put_do_range() if range_req fix_byte_range(cr_byte0) ; fix_byte_range(cr_byte1) if cr_byte1 <= cr_byte0 bad def fix_byte_range(b) if b < 0 b += fullsize if b < 0 b = 0 def Setuser_default() # if user != default_user if geteuid() != defu->pw_uid Setuser_via_root(defu) text_mimetypes() # some mimetypes that I want the browser to display as text/plain mimetypes_init() sym_init() cstr type = sym("text/plain") each(e, (char *)text_types_list) kv(mimetypes, sym(e), type) bin_mimetypes() # some mimetypes that I want the browser to display as application/octet-stream mimetypes_init() sym_init() cstr type = sym("application/octet-stream") each(e, (char *)bin_types_list) kv(mimetypes, sym(e), type) def set_cgi_vars() char tmp[1024] Setenv("SERVER_NAME", host) Setenv("SERVER_PROTOCOL", proto) Setenv("REQUEST_METHOD", method_str) # if (path_info) { # Setenv("PATH_INFO", "/foo/bar") # Setenv("PATH_TRANSLATED", "/www/sam.nipl.net/foo/bar") # } Setenv("SCRIPT_NAME", path) # Setenv("REMOTE_HOST", "...") # blank if not found, or omit it Setenv("REMOTE_ADDR", remote_addr_str) # TODO # Setenv("AUTH_TYPE", "basic") # Setenv("REMOTE_USER", "sam") # Setenv("REMOTE_IDENT", "sam") # omit? if (method == HTTP_POST) { # TODO # Setenv("CONTENT_TYPE", "form/url-encoded") # of form only if submitted sprintf(tmp, "%lld", (long long int)reqlen) Setenv("CONTENT_LENGTH", tmp) } # non-standard or CGI/1.1 snprintf(tmp, sizeof(tmp), "
%s/%s at %s Port %d
", program, version, host, server_port) Setenv("SERVER_SIGNATURE", tmp) Setenv("SERVER_ADDR", server_addr_str) sprintf(tmp, "%d", server_port) Setenv("SERVER_PORT", tmp) Setenv("DOCUMENT_ROOT", root) Setenv("SCRIPT_FILENAME", fullpath) sprintf(tmp, "%d", remote_port) Setenv("REMOTE_PORT", tmp) Setenv("REQUEST_URI", path) # HTTP lines key_value *i = vec0(headers) key_value *e = vecend(headers) for ; i!=e; ++i char tmp[64] strcpy(tmp, "HTTP_") strncpy(tmp+5, i->k, sizeof(tmp)-5-1) tmp[63] = '\0' char *j = tmp+5 for ; *j; ++j if *j == '-' *j = '_' eif islower(*j) *j -= 'a' - 'A' Setenv(tmp, i->v) def get_find() dir_ends_slash_or_redirect() new(b, buffer, 256) if find_to_html(b, fullpath, path) buffer_free(b) accerr mtype = "text/html" send_buffer(b) int find_to_html(buffer *b, cstr fullpath, cstr path) # TODO fix find to skip inaccessible parts? assert(cstr_ends_with(fullpath, "/")) # warn("finding under %s", fullpath) new(v, vec, cstr, 1024) try(x) int l = strlen(fullpath) find(fullpath, f, s) char *x = f + l # over the slash if x[-1] == '\0' || *x == '\0' # don't show the main dir continue vec_push(v, f) sort_vec(v, cstrp_cmp) # TODO find_sort function? find that returns a vec? # # main dir # Sprintf(b, "./
\n", path, path) for_vec(fp, v, cstr) char *f = *fp char *x = f + l # over the slash new(s, lstats, f) # XXX a bit inefficient to stat everything twice cstr typechr_href = "" int dir = S_ISDIR(s->st_mode) int exe = 0 if dir typechr_href = "/" else exe = s->st_mode & S_IXUSR # warn("found %s %s", f, typechr_href) char *base = strrchr(x, '/') if !base base = x else ++base int indent = 0 char *p for p=x; p!=base; ++p if *p == '/' ++indent cstr otag = "", ctag = "" if dir otag = "" ; ctag = "" repeat(indent): Sprintf(b, ". ") Sprintf(b, "%s%s%s", path, x, typechr_href, otag, base, ctag) if (exe && show_exe) || (dir && allow_find) # or find in a dir Sprintf(b, "%s%c%s", path, x, typechr_href, otag, dir ? '/' : '*', ctag) if avfs_root && !strstr(f, "/+") int zip = is_zip_file(f) if zip dir = zip > 1 if dir otag = "" ; ctag = "" typechr_href = "/" Sprintf(b, " %s%c%s", path, x, avfs_char, typechr_href, otag, avfs_char, ctag) if dir && allow_find Sprintf(b, "%s%c%s", path, x, otag, '/', ctag) Sprintf(b, "
\n") except(x) warn("find under %s died", fullpath) vec_free(v) return KO vec_free(v) return OK int is_zip_file(cstr filename) each(e, (char *)zip_types_list) if filename_ext_eq(filename, e) return 2 each(e, (char *)z_types_list) if filename_ext_eq(filename, e) return 1 return 0 boolean is_bot(cstr user_agent) if !user_agent return 0 each(b, (char *)bots) if strcasestr(user_agent, b) return 1 return 0 def OK 0 def KO 1