git.strcat.st

/strcat/filebin.git/ - summarytreelogarchive

subject
init
commit
e2b2905aae40b3e3076ad3db7bfcd784b914c9b2
date
2026-04-21T14:18:07Z
message
diff
 Makefile  |   10 +
 filebin.c | 1329 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 1339 insertions(+)

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0648d1b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,10 @@
+CC     = cc
+CFLAGS = -std=c89 -Wall -Wextra -O2
+
+all: filebin
+
+filebin: filebin.c
+	$(CC) $(CFLAGS) filebin.c -o filebin
+
+clean:
+	rm -f filebin
diff --git a/filebin.c b/filebin.c
new file mode 100644
index 0000000..f0a45d3
--- /dev/null
+++ b/filebin.c
@@ -0,0 +1,1329 @@
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <limits.h>
+#include <poll.h>
+#include <pwd.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#define	PORT			8080
+#define	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	MAX_CLIENTS		128
+#define	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:p:u:"
+#else
+#define	OPTSTR			"dp: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"
+    "<!doctype html><meta charset=utf-8><title>filebin</title>"
+    "<h1>filebin</h1><div>"
+    "<form method=post action=/upload enctype=multipart/form-data>"
+    "<input type=file name=file required> "
+    "<select name=ttl>"
+    "<option value='1m'>1m</option>"
+    "<option value='10m'>10m</option>"
+    "<option value='30m'>30m</option>"
+    "<option value='1h' selected>1h</option>"
+    "<option value='3h'>3h</option>"
+    "<option value='6h'>6h</option>"
+    "<option value='1d'>1d</option>"
+    "<option value='3d'>3d</option>"
+    "</select> "
+    "<button>Upload</button></form></div>";
+
+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	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_ttl(const char *, long *);
+static int	parse_ug(const char *, uid_t *, gid_t *);
+static int	meta_read(const char *, time_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 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 *);
+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	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 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 off_t
+cap_bytes(void)
+{
+	off_t cap;
+
+	cap = (off_t)STORAGE_CAP_GB;
+	cap *= 1024;
+	cap *= 1024;
+	cap *= 1024;
+	return cap;
+}
+
+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)
+	        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 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 (meta_read(e->d_name, &exp) == 0)
+	        exp = st.st_mtime + 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);
+	}
+	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 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)
+{
+	char full[512];
+	char hdr[256];
+	FILE *fp;
+	long sz;
+	int n;
+
+	if (strstr(path, "..") != NULL) {
+	    sendtxt(fd, "400 Bad Request", "bad path\n");
+	    return;
+	}
+	if (has_suffix(path, ".ttl") != 0) {
+	    sendtxt(fd, "404 Not Found", "not found\n");
+	    return;
+	}
+	snprintf(full, sizeof(full), ".%s", path);
+	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"
+	    "<h1>f/</h1><ul>");
+	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,
+	            "<li><a href='/f/%s'>%s</a></li>",
+	            e->d_name, e->d_name);
+	    }
+	    closedir(d);
+	}
+	off += (size_t)snprintf(out + off, sizeof(out) - off, "</ul>");
+	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 *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;
+	time_t now;
+	long body_len;
+	long ttl_sec;
+	int have_file;
+	int i;
+	int urfd;
+	unsigned char r[12];
+	FILE *fp;
+
+	ttl_sec = DEFAULT_TTL_SEC;
+	filename[0] = '\0';
+	file_body = NULL;
+	file_bytes = 0;
+	have_file = 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 ||
+	    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;
+	    }
+	}
+	body_len = atol(cl);
+	if (body_len <= 0 || body_len > MAX_REQ) {
+	    sendtxt(fd, "413 Payload Too Large", "bad size\n");
+	    return;
+	}
+	body = hdr_end + 4;
+	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) {
+	    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') {
+	        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) {
+	        sendtxt(fd, "400 Bad Request", "bad part\n");
+	        return;
+	    }
+	    part_hdr_len = (size_t)(part_hdr_end - part);
+	    if (part_hdr_len >= sizeof(ct)) {
+	        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) {
+	        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) {
+	        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) {
+	            sendtxt(fd, "400 Bad Request", "bad ttl (use 1m/1h/1d)\n");
+	            return;
+	        }
+	    } 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) {
+	        sendtxt(fd, "400 Bad Request", "bad multipart\n");
+	        return;
+	    }
+	}
+	if (have_file == 0) {
+	    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) {
+	    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) {
+	    sendtxt(fd, "500 Internal Server Error", "cannot write\n");
+	    return;
+	}
+	fwrite(file_body, 1, file_bytes, fp);
+	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"
+	        "<!doctype html><meta charset=utf-8><title>uploaded</title>"
+	        "<p>uploaded: <a href='/f/%s'>/f/%s</a></p>"
+	        "<p><img src='/f/%s' style='max-width:100%%;height:auto' "
+	        "alt='upload'></p>",
+	        final, final, final);
+	    sendall(fd, resp, (size_t)n);
+	    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);
+	}
+}
+
+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 >= MAX_REQ)
+	    return 0;
+	newcap = cl->cap == 0 ? INITIAL_REQ_CAP : cl->cap * 2;
+	if (newcap > MAX_REQ)
+	    newcap = 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;
+	long need;
+	size_t have;
+
+	hdr_end = strstr((char *)req, "\r\n\r\n");
+	if (hdr_end == NULL)
+	    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);
+	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] [-p port] [-u user:group]\n");
+#else
+	fprintf(stderr, "usage: filebin [-d] [-p port] [-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[MAX_CLIENTS];
+	struct pollfd pfds[MAX_CLIENTS + 1];
+	struct sockaddr_in sin;
+	time_t last_cleanup;
+	uid_t run_uid;
+	gid_t run_gid;
+	int map[MAX_CLIENTS + 1];
+	int c;
+	int ch;
+	int do_daemon;
+	int do_drop;
+	int i;
+	int nfds;
+	int one;
+	int port;
+	int pr;
+	int s;
+
+	port = PORT;
+	do_daemon = 0;
+	do_drop = 0;
+	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 '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();
+
+	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 < 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 < 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 < 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 < MAX_CLIENTS; i++) {
+	                if (clients[i].fd < 0)
+	                    break;
+	            }
+	            if (i == 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 >= 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;
+	        }
+	    }
+	}
+}