- C 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| doc | ||
| src | ||
| .gitignore | ||
| LICENSE | ||
| README.org | ||
filed
- Overview
- Features
- Build
- Usage
- Behavior
- Signals and exit status
- Logging
- Security notes
- Man page
- Contribution
- Author
- Licensing
a small, static HTTP/1.1 file server written in C99 against posix.1-2008. one source file as of now, no dependencies other than libc and pthreads. future updates incoming.
Overview
filed serves the files under a directory over HTTP. It answers GET and HEAD, keeps connections alive, handles range and conditional requests, and generates dir listings.
filed has no TLS, no authentication, and no configuration files in this current version. Patches for anything are always welcome, unless they are not.
Features
- HTTP/1.1 with keepalive (100 reqs per connection) and pipelining
- fixed thread pool fed by a bounded connection queue
- single-range requests (
206and416) Last-ModifiedwithIf-Modified-Sincesupport (304)index.htmllookup, otherwise a generated HTML directory listing is provided- builtin MIME table for common extensions
- IPv6 dualstack by default, IPv4 fallback
- path containment through
realpath(), dotfiles, and..refused - clean shutdown on
SIGINTandSIGTERM - one log line per request on stderr
Build
any C99 compiler really works, with POSIX.1-2008 headers. so basically everything nowadays
gcc -std=c99 -O2 -Wall -Wextra -pedantic -o filed src/main.c -lpthread
clang works the same way!
Install:
install -m 755 filed /usr/local/bin/filed
cp ./doc/filed.1 .
gzip ./filed.1
install -m 644 filed.1.gz /usr/local/share/man/man1/filed.1.gz
tthe man page, filed.1, can be read without installing it:
man ./doc/filed.1
Usage
filed [-l] [-b addr] [-p port] [-t threads] [-T secs] [dir] filed -h
dir is the document root and filed defaults to the current directory.
Options
consult filed(1) after a successful installation. otherwise, consult ./doc/filed.1
without -b, filed binds :: with IPV6_V6ONLY off and falls back to 0.0.0.0 if that fails. with -b, it uses the first address that getaddrinfo returns to the given host.
Examples
#serve the current dir on port 8080:
filed
#local only, w/ custom root:
filed -p 8000 -b 127.0.0.1 ~/dir
#no listings, 32 workers, 5 sec. timeout:
filed -l -t 32 -T 5 /dir/subdir
Behavior
URL to file mapping
- the target is cut at
?; the query plays no part in the lookup and is kept only in redirects. - the path is precent-coded; a bad escape or
%00returns400 - any path segment that starts with
.returns404; this hides dotfiles and rejects..in plain and%2e%2eform. - the decoded path is joined 2 the root and resolved with
realpath() - a result outside the root returns
403; this stops symlinks that point to the outside of the current tree
Directories
- a directory URL without a trailing slash gets a
301to the slash form. - with a trailing slash,
index.htmlis served if present. - otherwise, a listing is generated instead, or
403if-lis set as an argument. - listings sort directories first, then filers, by byte order of the name
- entries that start with
.are left out; each row shows a UTC modification time and the size in buytes.
Range requests
accepted forms are bytes=a-b, bytes=a- and bytes-n=; a valid range returns 206 with Content-Range. a range that starts past the end of the file returns 416 with Content-Range: bytes */size. a malformed header r a multi-range req is ignored, and the whole file is sent with 200.
Conditional requests
If-Modified-Since is compared to Last-Modified as an exact string. A match returns 304 with no body. any other vaue returns the full file. there is no ETag (TD).
Keep-alive
HTTP/1.1 connections stay open by default and HTTP/1.0 connections close.
Connection: close and Connection: keep-alive override the default.
requests with a body (Content-Length above 0, or any Transfer-Encoding) get 400 and the connection closes… header blocks are limited to 8192 bytes (431 beyond that), and a request target to 4095 characters.
Status codes
200, 206, 301, 304, 400, 403, 404, 405, 414, 416, 431,
500, 505. other methods than GET and HEAD get 405 with
Allow: GET, HEAD.
Architechture
the main thread calls poll() on the listing socket with a one second time out, then accept(). accepted sockets go into a ring buffer of 256 slots guarded by a mutex and two condition variabls. worker threads take one socket at a time and run full keep-alive loop for it before returning to the queue.
each worker sets SO_RCVTIMEO and SO_SNDTIMEO on its socket, it reads a header block, builds the response, writes a log line and repeats. shutdown sets a flag, wakes all waiters, lets workers drain the queue and joins them.
because each connection holds a worker, idle keep-alive clients occupy threads until the time ouit. with the defaults, eight idle clients stall everyone else for up to 15 seconds. raise -t or lower -T for public use, or put a reverse proxy in front.
Signals and exit status
SIGINT, SIGTERM : stop accepting, drain the queue, join threads
SIGPIPE : ignored
exit status is 0 after a clean shutdown or -h, 1 when the root does not resolve, the port cannot be bound or memory runs out, and 2 for bad options.
Logging
everything goes to stderr. startup and shutdowns print one line each, each request prints:
peer "METHOD target" status bytes
bytes counts the response body only. control characters in the target are printed as ? and the target is cut at 200 chars.
Security notes
every path is resolved with realpath() and must stay inside the root
dotfiles and hidden directories (/.directory) are never served or listed
there is no authentication or access control implemented yet, anything readable by the process user inside the root is public for the time being.
filed does not drop privileges or call chroot. run it as an unprivileged user.
a symlibk swapped between the realpath() check and open() is a theoretical race. please do not use a root that untrusted users can write to!
there is no TLS yet, terminate TLS in a reverse proxy.
Rverse proxy example (nginx?)
listen 443 ssl;
server_name files.example.org;
# ssl_certificate and ssl_certificate_key are meant to be here
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
Man page
filed.1 documents the synopsis and the options table iun the same form as the Options section above. this readme covers behavior and architecture more.
Contribution
to correct and error or typo, or to send a patch. write to coasteen@proton.me.
Author
coast (coasteen.xyz+contraption.ir) - coasteen@proton.me
Licensing
see the LICENSE file in the source tree.