#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define PORT 8080 #define DEFAULT_MAX_REQ (16 * 1024 * 1024) #define ENABLE_INDEX 0 #define DEFAULT_TTL_SEC (3 * 24 * 60 * 60) #define MAX_TTL_SEC (3 * 24 * 60 * 60) #define CLEANUP_INTERVAL_SEC 60 #define CLIENT_IDLE_TIMEOUT_SEC 30 #define INITIAL_REQ_CAP 8192 #define DEFAULT_MAX_CLIENTS 128 #define DEFAULT_STORAGE_CAP_GB 100 #if defined(__OpenBSD__) || defined(__linux__) || defined(__FreeBSD__) || \ defined(__NetBSD__) || defined(__DragonFly__) #define HAVE_CHROOT 1 #endif #ifdef HAVE_CHROOT #define OPTSTR "dc:m:n:p:s:t:u:" #else #define OPTSTR "dm:n:p:s:t:u:" #endif #ifdef HAVE_CHROOT int chroot(const char *); #endif struct client { char *req; size_t off; size_t cap; time_t last_active; int fd; }; static const char *html_index = "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "Connection: close\r\n\r\n" "filebin" "

filebin

" "
" " " " " "
"; static size_t g_max_req = DEFAULT_MAX_REQ; static int g_max_clients = DEFAULT_MAX_CLIENTS; static off_t g_storage_cap_bytes = (off_t)DEFAULT_STORAGE_CAP_GB * 1024 * 1024 * 1024; static long g_default_ttl_sec = DEFAULT_TTL_SEC; static int has_suffix(const char *, const char *); static int grow_reqbuf(struct client *); static int hdr_get(const char *, const char *, char *, size_t); static int hdr_has_chunked(const char *); static int eqi_n(const char *, const char *, size_t); static int isimg_ext(const char *); static int req_done(const char *, size_t); static int mk_fdir(void); static int parse_uint(const char *, unsigned int *); static int parse_size_mb(const char *, size_t *); static int parse_size_gb(const char *, off_t *); static int parse_ttl(const char *, long *); static int parse_ug(const char *, uid_t *, gid_t *); static int meta_read(const char *, time_t *); static int pass_read(const char *, char *, size_t); static int set_nb(int, int); static off_t cap_bytes(void); static off_t used_bytes(void); static char *find_bytes(const char *, size_t, const char *, size_t); static long parse_clen(const char *, const char *); static int parse_hex_size(const char *, size_t, size_t *); static int decode_chunked_body(const char *, size_t, char **, size_t *); static void cleanup_expired(void); static void client_close(struct client *); static void serve_req(int, char *, size_t); static void die(const char *); static void diex(const char *, const char *); static void serve_file(int, const char *, const char *); static void serve_list(int); static void serve_upload(int, char *, size_t); static void warnp(const char *); static void do_chroot(const char *); static void daemonize_self(void); static void mk_metapath(const char *, char *, size_t); static void mk_passpath(const char *, char *, size_t); static void obsd_sandbox(void); static void clean_ext(const char *, char *, size_t); static void sendall(int, const void *, size_t); static void sendtxt(int, const char *, const char *); static void usage(void); static void drop_privs(int, uid_t, gid_t); static void meta_write(const char *, time_t); static const char *ext_mime(const char *); static void sendall(int fd, const void *buf, size_t len) { const char *p; struct pollfd pfd; ssize_t n; int pr; p = buf; while (len > 0) { n = send(fd, p, len, 0); if (n > 0) { p += n; len -= (size_t)n; continue; } if (n == -1 && errno == EINTR) continue; if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { pfd.fd = fd; pfd.events = POLLOUT; pfd.revents = 0; pr = poll(&pfd, 1, 1000); if (pr > 0) continue; } return; } } static void sendtxt(int fd, const char *code, const char *body) { char hdr[256]; int n; n = snprintf(hdr, sizeof(hdr), "HTTP/1.1 %s\r\n" "Content-Type: text/plain; charset=utf-8\r\n" "Content-Length: %zu\r\n" "Connection: close\r\n\r\n", code, strlen(body)); sendall(fd, hdr, (size_t)n); sendall(fd, body, strlen(body)); } static const char * ext_mime(const char *path) { const char *dot; dot = strrchr(path, '.'); if (dot == NULL) return "application/octet-stream"; dot++; if (strcmp(dot, "txt") == 0) return "text/plain"; if (strcmp(dot, "html") == 0 || strcmp(dot, "htm") == 0) return "text/html"; if (strcmp(dot, "jpg") == 0 || strcmp(dot, "jpeg") == 0) return "image/jpeg"; if (strcmp(dot, "png") == 0) return "image/png"; if (strcmp(dot, "gif") == 0) return "image/gif"; if (strcmp(dot, "webp") == 0) return "image/webp"; if (strcmp(dot, "svg") == 0) return "image/svg+xml"; if (strcmp(dot, "bmp") == 0) return "image/bmp"; if (strcmp(dot, "ico") == 0) return "image/x-icon"; if (strcmp(dot, "avif") == 0) return "image/avif"; if (strcmp(dot, "mp3") == 0) return "audio/mpeg"; if (strcmp(dot, "mp4") == 0) return "video/mp4"; if (strcmp(dot, "flac") == 0) return "audio/flac"; if (strcmp(dot, "pdf") == 0) return "application/pdf"; return "application/octet-stream"; } static int isimg_ext(const char *ext) { if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0) return 1; if (strcmp(ext, "png") == 0 || strcmp(ext, "gif") == 0) return 1; if (strcmp(ext, "webp") == 0 || strcmp(ext, "svg") == 0) return 1; if (strcmp(ext, "bmp") == 0 || strcmp(ext, "ico") == 0) return 1; if (strcmp(ext, "avif") == 0) return 1; return 0; } static void clean_ext(const char *name, char *ext, size_t extsz) { const char *dot; size_t i; i = 0; dot = strrchr(name, '.'); if (dot == NULL || dot[1] == '\0') { ext[0] = '\0'; return; } dot++; while (*dot != '\0' && i + 1 < extsz) { if (isalnum((unsigned char)*dot) != 0) ext[i++] = (char)tolower((unsigned char)*dot); dot++; } ext[i] = '\0'; } static int mk_fdir(void) { struct stat st; if (stat("f", &st) == 0 && S_ISDIR(st.st_mode) != 0) return 0; return mkdir("f", 0755); } static int has_suffix(const char *s, const char *suffix) { size_t ls; size_t lf; ls = strlen(s); lf = strlen(suffix); if (lf > ls) return 0; return strcmp(s + ls - lf, suffix) == 0; } static void mk_metapath(const char *name, char *out, size_t outsz) { snprintf(out, outsz, "f/%s.ttl", name); } static void mk_passpath(const char *name, char *out, size_t outsz) { snprintf(out, outsz, "f/%s.pw", name); } static int parse_ttl(const char *val, long *out) { char *end; long n; long mult; while (*val == ' ' || *val == '\t') val++; if (isdigit((unsigned char)*val) == 0) return 0; n = strtol(val, &end, 10); if (n <= 0) return 0; while (*end == ' ' || *end == '\t') end++; if (*end == 'm' || *end == 'M') mult = 60; else if (*end == 'h' || *end == 'H') mult = 60 * 60; else if (*end == 'd' || *end == 'D') mult = 24 * 60 * 60; else return 0; end++; while (*end == ' ' || *end == '\t') end++; if (*end != '\0' || n > LONG_MAX / mult) return 0; *out = n * mult; if (*out > MAX_TTL_SEC) *out = MAX_TTL_SEC; return 1; } static int parse_uint(const char *s, unsigned int *out) { char *end; unsigned long v; errno = 0; v = strtoul(s, &end, 10); if (s[0] == '\0' || *end != '\0' || errno != 0 || v > UINT_MAX) return 0; *out = (unsigned int)v; return 1; } static int parse_ug(const char *spec, uid_t *uid, gid_t *gid) { char copy[256]; char *group; char *user; struct group *gr; struct passwd *pw; unsigned int id; size_t n; n = strlen(spec); if (n >= sizeof(copy)) return 0; memcpy(copy, spec, n + 1); user = copy; group = strchr(copy, ':'); if (group == NULL || group == user || group[1] == '\0') return 0; *group++ = '\0'; pw = getpwnam(user); if (pw != NULL) *uid = pw->pw_uid; else { if (parse_uint(user, &id) == 0) return 0; *uid = (uid_t)id; } gr = getgrnam(group); if (gr != NULL) *gid = gr->gr_gid; else { if (parse_uint(group, &id) == 0) return 0; *gid = (gid_t)id; } return 1; } static void meta_write(const char *name, time_t exp) { char path[512]; FILE *fp; mk_metapath(name, path, sizeof(path)); fp = fopen(path, "wb"); if (fp == NULL) return; fprintf(fp, "%ld\n", (long)exp); fclose(fp); } static int meta_read(const char *name, time_t *exp) { char path[512]; char buf[64]; FILE *fp; long v; mk_metapath(name, path, sizeof(path)); fp = fopen(path, "rb"); if (fp == NULL) return 0; if (fgets(buf, sizeof(buf), fp) == NULL) { fclose(fp); return 0; } fclose(fp); v = atol(buf); if (v <= 0) return 0; *exp = (time_t)v; return 1; } static int pass_read(const char *name, char *out, size_t outsz) { char path[512]; FILE *fp; size_t n; if (outsz == 0) return 0; out[0] = '\0'; mk_passpath(name, path, sizeof(path)); fp = fopen(path, "rb"); if (fp == NULL) return 0; if (fgets(out, (int)outsz, fp) == NULL) { fclose(fp); out[0] = '\0'; return 0; } fclose(fp); n = strlen(out); while (n > 0 && (out[n - 1] == '\n' || out[n - 1] == '\r')) out[--n] = '\0'; return 1; } static off_t cap_bytes(void) { return g_storage_cap_bytes; } static off_t used_bytes(void) { char path[512]; struct dirent *e; struct stat st; DIR *d; off_t total; total = 0; d = opendir("f"); if (d == NULL) return 0; while ((e = readdir(d)) != NULL) { if (e->d_name[0] == '.' || has_suffix(e->d_name, ".ttl") != 0 || has_suffix(e->d_name, ".pw") != 0) continue; snprintf(path, sizeof(path), "f/%s", e->d_name); if (stat(path, &st) != 0 || S_ISREG(st.st_mode) == 0) continue; if (st.st_size > 0) total += st.st_size; } closedir(d); return total; } static void cleanup_expired(void) { char base[256]; char mpath[512]; char ppath[512]; char path[512]; struct dirent *e; struct stat st; time_t exp; time_t now; size_t n; DIR *d; d = opendir("f"); if (d == NULL) return; now = time(NULL); while ((e = readdir(d)) != NULL) { if (e->d_name[0] == '.') continue; snprintf(path, sizeof(path), "f/%s", e->d_name); if (stat(path, &st) != 0 || S_ISREG(st.st_mode) == 0) continue; if (has_suffix(e->d_name, ".ttl") != 0) { n = strlen(e->d_name); if (n <= 4 || n - 4 >= sizeof(base)) continue; snprintf(mpath, sizeof(mpath), "f/%s", e->d_name); memcpy(base, e->d_name, n - 4); base[n - 4] = '\0'; snprintf(path, sizeof(path), "f/%s", base); if (stat(path, &st) != 0) unlink(mpath); continue; } if (has_suffix(e->d_name, ".pw") != 0) { n = strlen(e->d_name); if (n <= 3 || n - 3 >= sizeof(base)) continue; snprintf(ppath, sizeof(ppath), "f/%s", e->d_name); memcpy(base, e->d_name, n - 3); base[n - 3] = '\0'; snprintf(path, sizeof(path), "f/%s", base); if (stat(path, &st) != 0) unlink(ppath); continue; } if (meta_read(e->d_name, &exp) == 0) exp = st.st_mtime + (time_t)g_default_ttl_sec; if (now < exp) continue; snprintf(path, sizeof(path), "f/%s", e->d_name); unlink(path); mk_metapath(e->d_name, mpath, sizeof(mpath)); unlink(mpath); mk_passpath(e->d_name, ppath, sizeof(ppath)); unlink(ppath); } closedir(d); } static int eqi_n(const char *a, const char *b, size_t n) { size_t i; for (i = 0; i < n; i++) { if (tolower((unsigned char)a[i]) != tolower((unsigned char)b[i])) return 0; } return 1; } static long parse_clen(const char *hdr, const char *hdr_end) { const char *line; const char *line_end; const char *v; line = hdr; while (line < hdr_end && *line != '\0') { line_end = strstr(line, "\r\n"); if (line_end == NULL || line_end > hdr_end || line_end == line) break; if ((size_t)(line_end - line) > 15 && eqi_n(line, "Content-Length", 14) != 0 && line[14] == ':') { v = line + 15; while (v < line_end && (*v == ' ' || *v == '\t')) v++; return atol(v); } line = line_end + 2; } return -1; } static int hdr_has_chunked(const char *hdr) { char te[256]; char low[256]; char *tok; size_t i; size_t n; if (hdr_get(hdr, "Transfer-Encoding", te, sizeof(te)) == 0) return 0; n = strlen(te); for (i = 0; i < n && i + 1 < sizeof(low); i++) low[i] = (char)tolower((unsigned char)te[i]); low[i] = '\0'; tok = strtok(low, ","); while (tok != NULL) { while (*tok == ' ' || *tok == '\t') tok++; if (strcmp(tok, "chunked") == 0) return 1; tok = strtok(NULL, ","); } return 0; } static int parse_size_mb(const char *s, size_t *out) { unsigned long v; char *end; errno = 0; v = strtoul(s, &end, 10); if (s[0] == '\0' || *end != '\0' || errno != 0) return 0; if (v == 0 || v > (unsigned long)((size_t)-1 / (1024UL * 1024UL))) return 0; *out = (size_t)v * 1024UL * 1024UL; return 1; } static int parse_size_gb(const char *s, off_t *out) { unsigned long v; char *end; off_t gb; errno = 0; v = strtoul(s, &end, 10); if (s[0] == '\0' || *end != '\0' || errno != 0) return 0; gb = (off_t)v; if (gb <= 0 || gb > (off_t)-1 / (1024 * 1024 * 1024)) return 0; *out = gb * 1024 * 1024 * 1024; return 1; } static int parse_hex_size(const char *p, size_t len, size_t *out) { size_t i; size_t v; int digit; v = 0; if (len == 0) return 0; for (i = 0; i < len; i++) { if (p[i] == ';') break; if (p[i] >= '0' && p[i] <= '9') digit = p[i] - '0'; else if (p[i] >= 'a' && p[i] <= 'f') digit = 10 + (p[i] - 'a'); else if (p[i] >= 'A' && p[i] <= 'F') digit = 10 + (p[i] - 'A'); else return 0; if (v > (size_t)-1 / 16) return 0; v = v * 16 + (size_t)digit; } *out = v; return 1; } static int decode_chunked_body(const char *in, size_t inlen, char **out, size_t *outlen) { char *buf; const char *line_end; size_t chunk; size_t off; size_t line_len; size_t woff; buf = malloc(inlen + 1); if (buf == NULL) return 0; off = 0; woff = 0; while (off < inlen) { line_end = find_bytes(in + off, inlen - off, "\r\n", 2); if (line_end == NULL) { free(buf); return 0; } line_len = (size_t)(line_end - (in + off)); if (parse_hex_size(in + off, line_len, &chunk) == 0) { free(buf); return 0; } off += line_len + 2; if (chunk == 0) { char *trail_end; trail_end = find_bytes(in + off, inlen - off, "\r\n\r\n", 4); if (trail_end == NULL && inlen - off >= 2 && in[off] == '\r' && in[off + 1] == '\n') off += 2; else if (trail_end != NULL) off = (size_t)(trail_end - in) + 4; else { free(buf); return 0; } break; } if (chunk > inlen - off || woff > g_max_req - chunk) { free(buf); return 0; } memcpy(buf + woff, in + off, chunk); woff += chunk; off += chunk; if (inlen - off < 2 || in[off] != '\r' || in[off + 1] != '\n') { free(buf); return 0; } off += 2; } buf[woff] = '\0'; *out = buf; *outlen = woff; return 1; } static char * find_bytes(const char *hay, size_t hlen, const char *needle, size_t nlen) { size_t i; if (nlen == 0 || hlen < nlen) return NULL; for (i = 0; i + nlen <= hlen; i++) { if (memcmp(hay + i, needle, nlen) == 0) return (char *)(hay + i); } return NULL; } static void serve_file(int fd, const char *path, const char *req) { char phead[128]; char pmeta[128]; char pquery[128]; char reqpw[128]; char full[512]; char hdr[256]; char name[256]; char *q; char *pp; size_t i; size_t nlen; FILE *fp; long sz; int n; phead[0] = '\0'; pmeta[0] = '\0'; pquery[0] = '\0'; reqpw[0] = '\0'; if (hdr_get(req, "X-Filebin-Password", phead, sizeof(phead)) != 0) { nlen = strlen(phead); while (nlen > 0 && (phead[nlen - 1] == ' ' || phead[nlen - 1] == '\t')) phead[--nlen] = '\0'; } if (strstr(path, "..") != NULL) { sendtxt(fd, "400 Bad Request", "bad path\n"); return; } if (has_suffix(path, ".ttl") != 0 || has_suffix(path, ".pw") != 0) { sendtxt(fd, "404 Not Found", "not found\n"); return; } q = strchr(path, '?'); nlen = q == NULL ? strlen(path) : (size_t)(q - path); if (nlen <= 3 || nlen - 3 >= sizeof(name)) { sendtxt(fd, "400 Bad Request", "bad path\n"); return; } memcpy(name, path + 3, nlen - 3); name[nlen - 3] = '\0'; if (q != NULL) { pp = strstr(q + 1, "pw="); if (pp != NULL) { pp += 3; i = 0; while (pp[i] != '\0' && pp[i] != '&' && i + 1 < sizeof(pquery)) { pquery[i] = pp[i]; i++; } pquery[i] = '\0'; } } if (pass_read(name, pmeta, sizeof(pmeta)) != 0) { if (phead[0] != '\0') snprintf(reqpw, sizeof(reqpw), "%s", phead); else if (pquery[0] != '\0') snprintf(reqpw, sizeof(reqpw), "%s", pquery); if (reqpw[0] == '\0' || strcmp(reqpw, pmeta) != 0) { sendtxt(fd, "401 Unauthorized", "password required\n"); return; } } snprintf(full, sizeof(full), ".%s", path); if (q != NULL) full[(size_t)(q - path) + 1] = '\0'; fp = fopen(full, "rb"); if (fp == NULL) { sendtxt(fd, "404 Not Found", "not found\n"); return; } fseek(fp, 0, SEEK_END); sz = ftell(fp); fseek(fp, 0, SEEK_SET); if (sz < 0) { fclose(fp); sendtxt(fd, "500 Internal Server Error", "io error\n"); return; } n = snprintf(hdr, sizeof(hdr), "HTTP/1.1 200 OK\r\n" "Content-Type: %s\r\n" "Content-Length: %ld\r\n" "Connection: close\r\n\r\n", ext_mime(full), sz); sendall(fd, hdr, (size_t)n); while (1) { char buf[8192]; size_t r; r = fread(buf, 1, sizeof(buf), fp); if (r == 0) break; sendall(fd, buf, r); } fclose(fp); } static void serve_list(int fd) { #if ENABLE_INDEX char out[65536]; struct dirent *e; size_t off; DIR *d; off = 0; off += (size_t)snprintf(out + off, sizeof(out) - off, "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "Connection: close\r\n\r\n" "

f/

    "); d = opendir("f"); if (d != NULL) { while ((e = readdir(d)) != NULL) { if (e->d_name[0] == '.' || off + 256 >= sizeof(out)) continue; off += (size_t)snprintf(out + off, sizeof(out) - off, "
  • %s
  • ", e->d_name, e->d_name); } closedir(d); } off += (size_t)snprintf(out + off, sizeof(out) - off, "
"); sendall(fd, out, off); #else sendtxt(fd, "403 Forbidden", "index listing is off\n"); #endif } static int hdr_get(const char *hdr, const char *name, char *out, size_t outsz) { const char *line; const char *line_end; const char *v; size_t len; size_t nl; nl = strlen(name); if (outsz == 0) return 0; out[0] = '\0'; line = hdr; while (*line != '\0') { line_end = strstr(line, "\r\n"); if (line_end == NULL || line_end == line) break; if ((size_t)(line_end - line) > nl && eqi_n(line, name, nl) != 0 && line[nl] == ':') { v = line + nl + 1; while (v < line_end && (*v == ' ' || *v == '\t')) v++; len = (size_t)(line_end - v); if (len >= outsz) len = outsz - 1; memcpy(out, v, len); out[len] = '\0'; return 1; } line = line_end + 2; } return 0; } static void serve_upload(int fd, char *req, size_t req_len) { char boundary[256]; char bmark[300]; char bterm[304]; char cd[512]; char cl[64]; char ct[512]; char field[64]; char filename[256]; char final[64]; char path[128]; char rnd[16]; char ttl_hdr[64]; char pass_hdr[128]; char pass_val[128]; char *dec_body; char *body; char *bpos; char *curr; char *fn; char *hdr_end; char *part; char *part_hdr_end; char *pend; char *file_body; char *pbody; const char *alpha; off_t cap_sz; off_t used_sz; size_t bmark_len; size_t bterm_len; size_t file_bytes; size_t field_len; size_t part_hdr_len; size_t part_rem; size_t pbody_len; size_t pass_len; time_t now; long body_len; long ttl_sec; int have_file; int have_pass; int is_chunked; int i; int urfd; unsigned char r[12]; FILE *fp; ttl_sec = g_default_ttl_sec; dec_body = NULL; pass_val[0] = '\0'; filename[0] = '\0'; file_body = NULL; file_bytes = 0; have_file = 0; have_pass = 0; hdr_end = strstr(req, "\r\n\r\n"); if (hdr_end == NULL) { sendtxt(fd, "400 Bad Request", "bad request\n"); return; } *hdr_end = '\0'; if (hdr_get(req, "Content-Type", ct, sizeof(ct)) == 0) { sendtxt(fd, "400 Bad Request", "missing headers\n"); return; } is_chunked = hdr_has_chunked(req); if (is_chunked == 0 && hdr_get(req, "Content-Length", cl, sizeof(cl)) == 0) { sendtxt(fd, "400 Bad Request", "missing headers\n"); return; } if (hdr_get(req, "X-Filebin-TTL", ttl_hdr, sizeof(ttl_hdr)) != 0) { if (parse_ttl(ttl_hdr, &ttl_sec) == 0) { sendtxt(fd, "400 Bad Request", "bad ttl (use 1m/1h/1d)\n"); return; } } if (hdr_get(req, "X-Filebin-Password", pass_hdr, sizeof(pass_hdr)) != 0) { pass_len = strlen(pass_hdr); while (pass_len > 0 && (pass_hdr[pass_len - 1] == ' ' || pass_hdr[pass_len - 1] == '\t')) pass_hdr[--pass_len] = '\0'; if (pass_hdr[0] != '\0') { snprintf(pass_val, sizeof(pass_val), "%s", pass_hdr); have_pass = 1; } } body = hdr_end + 4; if (is_chunked != 0) { size_t raw_len; size_t dec_len; raw_len = req_len - (size_t)(body - req); if (decode_chunked_body(body, raw_len, &dec_body, &dec_len) == 0 || dec_len == 0 || dec_len > g_max_req) { free(dec_body); sendtxt(fd, "400 Bad Request", "bad chunked body\n"); return; } body = dec_body; body_len = (long)dec_len; } else { body_len = atol(cl); if (body_len <= 0 || (size_t)body_len > g_max_req) { sendtxt(fd, "413 Payload Too Large", "bad size\n"); return; } if ((size_t)(body - req) + (size_t)body_len > req_len) { sendtxt(fd, "400 Bad Request", "truncated body\n"); return; } } bpos = strstr(ct, "boundary="); if (bpos == NULL) { sendtxt(fd, "400 Bad Request", "no boundary\n"); return; } bpos += 9; i = 0; if (*bpos == '"') { bpos++; while (bpos[i] != '\0' && bpos[i] != '"' && i + 1 < (int)sizeof(boundary)) { boundary[i] = bpos[i]; i++; } } else { while (bpos[i] != '\0' && bpos[i] != ';' && bpos[i] != ' ' && bpos[i] != '\t' && i + 1 < (int)sizeof(boundary)) { boundary[i] = bpos[i]; i++; } } boundary[i] = '\0'; if (boundary[0] == '\0') { sendtxt(fd, "400 Bad Request", "bad boundary\n"); return; } snprintf(bmark, sizeof(bmark), "--%s", boundary); snprintf(bterm, sizeof(bterm), "\r\n--%s", boundary); bmark_len = strlen(bmark); bterm_len = strlen(bterm); if ((size_t)body_len < bmark_len || memcmp(body, bmark, bmark_len) != 0) { free(dec_body); sendtxt(fd, "400 Bad Request", "bad multipart\n"); return; } curr = body; while (curr < body + body_len) { part = curr + bmark_len; part_rem = (size_t)(body + body_len - part); if (part_rem >= 2 && part[0] == '-' && part[1] == '-') break; if (part_rem < 2 || part[0] != '\r' || part[1] != '\n') { free(dec_body); sendtxt(fd, "400 Bad Request", "bad part\n"); return; } part += 2; part_rem = (size_t)(body + body_len - part); part_hdr_end = find_bytes(part, part_rem, "\r\n\r\n", 4); if (part_hdr_end == NULL) { free(dec_body); sendtxt(fd, "400 Bad Request", "bad part\n"); return; } part_hdr_len = (size_t)(part_hdr_end - part); if (part_hdr_len >= sizeof(ct)) { free(dec_body); sendtxt(fd, "400 Bad Request", "part headers too large\n"); return; } memcpy(ct, part, part_hdr_len); ct[part_hdr_len] = '\0'; if (hdr_get(ct, "Content-Disposition", cd, sizeof(cd)) == 0) { free(dec_body); sendtxt(fd, "400 Bad Request", "no disposition\n"); return; } field[0] = '\0'; fn = strstr(cd, "name="); if (fn != NULL) { fn += 5; if (*fn == '"') { fn++; i = 0; while (*fn != '\0' && *fn != '"' && i + 1 < (int)sizeof(field)) { field[i++] = *fn++; } field[i] = '\0'; } } fn = strstr(cd, "filename="); filename[0] = '\0'; if (fn != NULL) { fn += 9; if (*fn == '"') { fn++; i = 0; while (*fn != '\0' && *fn != '"' && i + 1 < (int)sizeof(filename)) { filename[i++] = *fn++; } filename[i] = '\0'; } } pbody = part_hdr_end + 4; pbody_len = (size_t)(body + body_len - pbody); pend = find_bytes(pbody, pbody_len, bterm, bterm_len); if (pend == NULL) { free(dec_body); sendtxt(fd, "400 Bad Request", "part end missing\n"); return; } if (strcmp(field, "ttl") == 0) { field_len = (size_t)(pend - pbody); if (field_len >= sizeof(ttl_hdr)) field_len = sizeof(ttl_hdr) - 1; memcpy(ttl_hdr, pbody, field_len); ttl_hdr[field_len] = '\0'; if (parse_ttl(ttl_hdr, &ttl_sec) == 0) { free(dec_body); sendtxt(fd, "400 Bad Request", "bad ttl (use 1m/1h/1d)\n"); return; } } else if (strcmp(field, "password") == 0) { field_len = (size_t)(pend - pbody); if (field_len >= sizeof(pass_val)) field_len = sizeof(pass_val) - 1; memcpy(pass_val, pbody, field_len); pass_val[field_len] = '\0'; if (pass_val[0] != '\0') have_pass = 1; } else if (filename[0] != '\0' || strcmp(field, "file") == 0) { file_body = pbody; file_bytes = (size_t)(pend - pbody); have_file = 1; } curr = pend + 2; if ((size_t)(body + body_len - curr) < bmark_len || memcmp(curr, bmark, bmark_len) != 0) { free(dec_body); sendtxt(fd, "400 Bad Request", "bad multipart\n"); return; } } if (have_file == 0) { free(dec_body); sendtxt(fd, "400 Bad Request", "missing file\n"); return; } used_sz = used_bytes(); cap_sz = cap_bytes(); if ((off_t)file_bytes > cap_sz - used_sz) { free(dec_body); sendtxt(fd, "507 Insufficient Storage", "storage cap reached\n"); return; } alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; urfd = open("/dev/urandom", O_RDONLY); if (urfd < 0 || read(urfd, r, sizeof(r)) != (ssize_t)sizeof(r)) { if (urfd >= 0) close(urfd); srand((unsigned int)(time(NULL) ^ getpid())); for (i = 0; i < 12; i++) r[i] = (unsigned char)(rand() & 0xff); } else close(urfd); for (i = 0; i < 12; i++) rnd[i] = alpha[r[i] % 62]; rnd[12] = '\0'; clean_ext(filename, ct, sizeof(ct)); if (ct[0] == '\0') snprintf(final, sizeof(final), "%s.bin", rnd); else snprintf(final, sizeof(final), "%s.%s", rnd, ct); snprintf(path, sizeof(path), "f/%s", final); fp = fopen(path, "wb"); if (fp == NULL) { free(dec_body); sendtxt(fd, "500 Internal Server Error", "cannot write\n"); return; } fwrite(file_body, 1, file_bytes, fp); fclose(fp); if (have_pass != 0) { char pwpath[128]; mk_passpath(final, pwpath, sizeof(pwpath)); fp = fopen(pwpath, "wb"); if (fp != NULL) { fprintf(fp, "%s\n", pass_val); fclose(fp); } } now = time(NULL); meta_write(final, now + (time_t)ttl_sec); if (isimg_ext(ct) != 0) { char resp[2048]; int n; n = snprintf(resp, sizeof(resp), "HTTP/1.1 201 Created\r\n" "Content-Type: text/html; charset=utf-8\r\n" "Connection: close\r\n\r\n" "uploaded" "

uploaded: /f/%s

" "

", final, final, final); sendall(fd, resp, (size_t)n); free(dec_body); return; } { char resp[512]; int n; n = snprintf(resp, sizeof(resp), "HTTP/1.1 201 Created\r\n" "Content-Type: text/plain; charset=utf-8\r\n" "Connection: close\r\n\r\n" "/f/%s\n", final); sendall(fd, resp, (size_t)n); } free(dec_body); } static int set_nb(int fd, int on) { int fl; fl = fcntl(fd, F_GETFL, 0); if (fl < 0) return -1; if (on != 0) fl |= O_NONBLOCK; else fl &= ~O_NONBLOCK; return fcntl(fd, F_SETFL, fl); } static void client_close(struct client *cl) { if (cl->fd >= 0) close(cl->fd); free(cl->req); cl->req = NULL; cl->off = 0; cl->cap = 0; cl->last_active = 0; cl->fd = -1; } static int grow_reqbuf(struct client *cl) { char *p; size_t newcap; if (cl->off < cl->cap) return 1; if (cl->cap >= g_max_req) return 0; newcap = cl->cap == 0 ? INITIAL_REQ_CAP : cl->cap * 2; if (newcap > g_max_req) newcap = g_max_req; p = realloc(cl->req, newcap + 1); if (p == NULL) return 0; cl->req = p; cl->cap = newcap; return 1; } static int req_done(const char *req, size_t off) { char *hdr_end; char *trail_end; long need; size_t have; hdr_end = strstr((char *)req, "\r\n\r\n"); if (hdr_end == NULL) return 0; if (hdr_has_chunked(req) != 0) { trail_end = find_bytes(hdr_end + 4, off - (size_t)(hdr_end + 4 - req), "\r\n0\r\n\r\n", 7); if (trail_end != NULL) return 1; trail_end = find_bytes(hdr_end + 4, off - (size_t)(hdr_end + 4 - req), "\r\n0\r\n", 5); if (trail_end != NULL) { trail_end = find_bytes(trail_end + 5, off - (size_t)(trail_end + 5 - req), "\r\n\r\n", 4); if (trail_end != NULL) return 1; } if (off >= (size_t)(hdr_end + 6 - req) && memcmp(hdr_end + 4, "0\r\n\r\n", 5) == 0) return 1; return 0; } need = parse_clen(req, hdr_end); have = off - (size_t)(hdr_end + 4 - req); if (need < 0) return 1; if (have >= (size_t)need) return 1; return 0; } static void serve_req(int fd, char *req, size_t req_len) { char method[16]; char path[1024]; (void)req_len; method[0] = '\0'; path[0] = '\0'; sscanf(req, "%15s %1023s", method, path); if (strcmp(method, "GET") == 0 && strcmp(path, "/") == 0) sendall(fd, html_index, strlen(html_index)); else if (strcmp(method, "GET") == 0 && strcmp(path, "/f") == 0) serve_list(fd); else if (strcmp(method, "GET") == 0 && strncmp(path, "/f/", 3) == 0) serve_file(fd, path, req); else if (strcmp(method, "POST") == 0 && strcmp(path, "/upload") == 0) serve_upload(fd, req, req_len); else sendtxt(fd, "404 Not Found", "not found\n"); } static void usage(void) { #ifdef HAVE_CHROOT fprintf(stderr, "usage: filebin [-d] [-c chrootdir] [-m maxmb] [-n maxclients] " "[-p port] [-s storagegb] [-t ttl] [-u user:group]\n"); #else fprintf(stderr, "usage: filebin [-d] [-m maxmb] [-n maxclients] [-p port] " "[-s storagegb] [-t ttl] [-u user:group]\n"); #endif exit(1); } static void die(const char *msg) { perror(msg); exit(1); } static void diex(const char *msg, const char *arg) { fprintf(stderr, "%s: %s\n", msg, arg); exit(1); } static void warnp(const char *msg) { perror(msg); } static void do_chroot(const char *dir) { #ifdef HAVE_CHROOT if (dir == NULL) return; if (chroot(dir) != 0) die("chroot"); if (chdir("/") != 0) die("chdir"); #else if (dir != NULL) diex("chroot unsupported on this platform", dir); #endif } static void drop_privs(int enabled, uid_t uid, gid_t gid) { if (enabled == 0) return; if (setgid(gid) != 0) die("setgid"); if (setuid(uid) != 0) die("setuid"); } static void daemonize_self(void) { int devnull; pid_t pid; pid = fork(); if (pid < 0) die("fork"); if (pid > 0) _exit(0); if (setsid() < 0) die("setsid"); signal(SIGHUP, SIG_IGN); pid = fork(); if (pid < 0) die("fork"); if (pid > 0) _exit(0); devnull = open("/dev/null", O_RDWR); if (devnull < 0) die("open /dev/null"); if (dup2(devnull, STDIN_FILENO) < 0) die("dup2 stdin"); if (dup2(devnull, STDOUT_FILENO) < 0) die("dup2 stdout"); if (dup2(devnull, STDERR_FILENO) < 0) die("dup2 stderr"); if (devnull > STDERR_FILENO) close(devnull); } static void obsd_sandbox(void) { #ifdef __OpenBSD__ if (unveil("f", "rwc") != 0) die("unveil f"); if (unveil("/dev/urandom", "r") != 0) die("unveil urandom"); if (unveil(NULL, NULL) != 0) die("unveil lock"); if (pledge("stdio inet rpath wpath cpath", NULL) != 0) die("pledge"); #endif } int main(int argc, char *argv[]) { #ifdef HAVE_CHROOT const char *chroot_dir; #endif char *end; struct client *clients; struct pollfd *pfds; struct sockaddr_in sin; time_t last_cleanup; uid_t run_uid; gid_t run_gid; int *map; int c; int ch; int do_daemon; int do_drop; int i; int nfds; int one; int port; int pr; int s; size_t max_req_mb; off_t storage_cap_bytes; unsigned int max_clients; long ttl_cfg; port = PORT; do_daemon = 0; do_drop = 0; max_req_mb = DEFAULT_MAX_REQ / (1024 * 1024); max_clients = DEFAULT_MAX_CLIENTS; storage_cap_bytes = (off_t)DEFAULT_STORAGE_CAP_GB * 1024 * 1024 * 1024; ttl_cfg = DEFAULT_TTL_SEC; run_uid = 0; run_gid = 0; #ifdef HAVE_CHROOT chroot_dir = NULL; #endif while ((ch = getopt(argc, argv, OPTSTR)) != -1) { switch (ch) { case 'd': do_daemon = 1; break; #ifdef HAVE_CHROOT case 'c': chroot_dir = optarg; break; #endif case 'p': errno = 0; port = (int)strtol(optarg, &end, 10); if (optarg[0] == '\0' || *end != '\0' || errno != 0 || port <= 0 || port > 65535) usage(); break; case 'm': if (parse_size_mb(optarg, &g_max_req) == 0) usage(); break; case 'n': if (parse_uint(optarg, &max_clients) == 0 || max_clients == 0) usage(); if (max_clients > INT_MAX - 1) usage(); g_max_clients = (int)max_clients; break; case 's': if (parse_size_gb(optarg, &storage_cap_bytes) == 0) usage(); g_storage_cap_bytes = storage_cap_bytes; break; case 't': if (parse_ttl(optarg, &ttl_cfg) == 0) usage(); g_default_ttl_sec = ttl_cfg; break; case 'u': if (parse_ug(optarg, &run_uid, &run_gid) == 0) diex("invalid user:group", optarg); do_drop = 1; break; default: usage(); } } argc -= optind; argv += optind; if (argc != 0) usage(); (void)max_req_mb; clients = calloc((size_t)g_max_clients, sizeof(*clients)); pfds = calloc((size_t)g_max_clients + 1, sizeof(*pfds)); map = calloc((size_t)g_max_clients + 1, sizeof(*map)); if (clients == NULL || pfds == NULL || map == NULL) die("calloc"); s = socket(AF_INET, SOCK_STREAM, 0); if (s < 0) die("socket"); one = 1; if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) != 0) die("setsockopt"); memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = htonl(INADDR_ANY); sin.sin_port = htons((unsigned short)port); if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) != 0) die("bind"); if (listen(s, 16) != 0) die("listen"); #ifdef HAVE_CHROOT do_chroot(chroot_dir); #else do_chroot(NULL); #endif if (mk_fdir() != 0) die("mkdir f"); drop_privs(do_drop, run_uid, run_gid); obsd_sandbox(); if (set_nb(s, 1) != 0) die("set_nb"); if (do_daemon != 0) daemonize_self(); for (i = 0; i < g_max_clients; i++) { clients[i].req = NULL; clients[i].off = 0; clients[i].cap = 0; clients[i].last_active = 0; clients[i].fd = -1; } last_cleanup = 0; printf("filebin listening on :%d\n", port); fflush(stdout); while (1) { time_t now; now = time(NULL); if (last_cleanup == 0 || now - last_cleanup >= CLEANUP_INTERVAL_SEC) { cleanup_expired(); last_cleanup = now; } for (i = 0; i < g_max_clients; i++) { if (clients[i].fd >= 0 && now - clients[i].last_active >= CLIENT_IDLE_TIMEOUT_SEC) client_close(&clients[i]); } pfds[0].fd = s; pfds[0].events = POLLIN; pfds[0].revents = 0; map[0] = -1; nfds = 1; for (i = 0; i < g_max_clients; i++) { if (clients[i].fd < 0) continue; pfds[nfds].fd = clients[i].fd; pfds[nfds].events = POLLIN; pfds[nfds].revents = 0; map[nfds] = i; nfds++; } pr = poll(pfds, (nfds_t)nfds, 1000); if (pr < 0) { if (errno == EINTR) continue; warnp("poll"); continue; } if ((pfds[0].revents & POLLIN) != 0) { while (1) { c = accept(s, NULL, NULL); if (c < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) break; warnp("accept"); break; } if (set_nb(c, 1) != 0) { close(c); continue; } for (i = 0; i < g_max_clients; i++) { if (clients[i].fd < 0) break; } if (i == g_max_clients) { close(c); continue; } clients[i].req = malloc(INITIAL_REQ_CAP + 1); if (clients[i].req == NULL) { close(c); continue; } clients[i].req[0] = '\0'; clients[i].off = 0; clients[i].cap = INITIAL_REQ_CAP; clients[i].last_active = now; clients[i].fd = c; } } for (i = 1; i < nfds; i++) { struct client *cl; ssize_t nr; if ((pfds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { client_close(&clients[map[i]]); continue; } if ((pfds[i].revents & POLLIN) == 0) continue; cl = &clients[map[i]]; while (1) { if (grow_reqbuf(cl) == 0) { sendtxt(cl->fd, "413 Payload Too Large", "bad size\n"); client_close(cl); break; } nr = recv(cl->fd, cl->req + cl->off, cl->cap - cl->off, 0); if (nr > 0) { cl->off += (size_t)nr; cl->req[cl->off] = '\0'; cl->last_active = now; if (req_done(cl->req, cl->off) != 0) { serve_req(cl->fd, cl->req, cl->off); client_close(cl); break; } if (cl->off >= g_max_req) { sendtxt(cl->fd, "413 Payload Too Large", "bad size\n"); client_close(cl); break; } continue; } if (nr == 0) { client_close(cl); break; } if (errno == EAGAIN || errno == EWOULDBLOCK) break; client_close(cl); break; } } } }