Skip to content

diatribes/srv

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

srv

A single-file static HTTP server in C99.

  • Serves static files from a document root over HTTP/1.0 / 1.1 with keep-alive.
  • Answers GET and HEAD only - anything else is 405. IPv4.
  • Multithreaded - one detached POSIX thread per connection, capped to the file-descriptor limit.
  • Drops privileges - binds as root, then sheds supplementary groups, gid, and uid to an unprivileged user.
  • Path-traversal hardened - paths are URL-decoded, canonicalized with realpath(), and confined to the docroot; only world-readable files are served.
  • Overload-resistant - per-request receive deadline (slowloris bound), idle and stalled-send timeouts, and a connection cap.
  • Logs access lines to stdout, lifecycle and errors to syslog.
  • Tiny and portable - one file, depends only on libpthread; builds on Linux (glibc/musl), macOS, and the BSDs.

For TLS, run it behind a terminator such as stunnel.

Build

make            # release: -O3 -DNDEBUG, produces ./srv
make debug      # -g -DDEBUG, perror() diagnostics on the error path
make prof       # -pg, for gprof
make tcc        # build with tcc instead of cc
make clean      # remove ./srv

Compiler flags are strict: -std=c99 -pedantic -Wall -Wextra -Wshadow -Wpointer-arith -Wcast-qual -Wstrict-prototypes -Wmissing-prototypes -Wdeclaration-after-statement.

The release build also adds glibc/ELF hardening (_FORTIFY_SOURCE, stack protector, PIE, full RELRO). It uses the system cc (override with make CC=gcc). On macOS or the BSDs those Linux-specific flags don't apply; build with make HARDEN= to drop them.

Run

srv username port docroot [ip address]
Argument Meaning
username user to setuid() to after the socket is bound
port TCP port to listen on
docroot directory to serve; resolved with realpath() at startup
ip optional IPv4 bind address; default is all interfaces

Example:

sudo ./srv nobody 8080 ~/docroot

Startup order: open syslog, install signal handlers, canonicalize docroot, chdir() into it, look up the user, bind the listening socket, then setuid() to drop privileges. Binding before the drop is why it is normally started as root. SIGINT and SIGTERM trigger a fast shutdown - the accept loop stops and the process exits, letting the OS reclaim threads, sockets, and memory; in-flight connections are not drained. The peak concurrent-thread count is printed on exit.

A request for a directory is served index.html from that directory - there is no directory listing, so a directory without index.html returns 404. A path with no matching MIME extension is sent as application/octet-stream.

Request handling

  • Methods: GET and HEAD. HEAD sends headers only. Any other method on a well-formed request line gets 405.
  • Versions: HTTP/1.0 and HTTP/1.1. The status line echoes the request minor version.
  • Status codes emitted: 200, 400 (malformed request), 403 (resolves outside docroot, or not world-readable), 404 (not found), 405 (unsupported method), 500 (allocation failure).
  • Response headers: Content-Type, Content-Length, Allow: GET, HEAD, and Connection.
  • Keep-alive: HTTP/1.1 keeps the connection open by default and honors Connection: close; HTTP/1.0 closes after each response. Bytes received past the end of one request are retained for the next.
  • Logging: the request line (sanitized of control characters) is written to stdout on every request - an access log for the supervisor to capture and rotate. Lifecycle markers (started, normal exit, error exit) go to syslog (LOG_DAEMON, LOG_PID).

Security model

  • Privilege drop: when started as root, the socket is bound first, then supplementary groups, gid, and uid are dropped to username (refused if it resolves to uid 0). Started unprivileged, there is nothing to drop and it runs as the invoking user.
  • Path traversal: each request path is URL-decoded, rejected if it contains control characters, joined to the docroot, and canonicalized with realpath() (which resolves .., symlinks, and duplicate slashes). The result must still begin with the docroot followed by /, or the request is refused with 403.
  • File constraints: only regular files are served, and only if world-readable (the other read bit) - publication is opt-in; directories other than via index.html are refused.
  • Input limits: request size and header count are capped; oversized requests are refused (see Tunables).
  • Timeouts: idle receives and stalled sends both time out, and a full request must arrive within HEADER_TIMEO_SEC - bounding slow-client (slowloris) and stalled-reader attacks that would otherwise pin a worker thread indefinitely.
  • Overload: thread_count_max caps concurrent connections and is reduced at startup to fit the process descriptor limit ((RLIMIT_NOFILE - FD_RESERVE) / 2); under descriptor exhaustion the accept loop backs off instead of spinning.
  • Signals: workers block SIGINT/SIGTERM; only main() handles them, and SIGPIPE is ignored.

Tunables

Compile-time constants at the top of srv.c:

Constant Default Meaning
DEFAULT_FILE index.html served for directory requests
DEFAULT_MIME_TYPE application/octet-stream fallback content type
MIN_REQUEST_LEN 12 shortest acceptable request
MAX_REQUEST_LEN 4096 request buffer size
MAX_HEADERS 32 header slots per request
RCVTIMEO_SEC 5 per-socket recv/send timeout
HEADER_TIMEO_SEC 10 deadline to receive a full request
SEND_FILE_NBYTES 65536 file send chunk size
THREAD_STACK_SIZE 16384 per-worker stack
FD_RESERVE 8 descriptors kept free when capping the connection limit
thread_count_max 4096 max connections; capped at startup to fit RLIMIT_NOFILE

MIME types

Extension lookup; anything unlisted falls back to application/octet-stream.

Extension(s) Type
.html .htm text/html; charset=utf-8
.txt .c .h .asm .fs .vs text/plain; charset=utf-8
.js text/javascript
.wasm application/wasm
.css text/css; charset=utf-8
.xml application/xml; charset=utf-8
.json application/json
.png image/png
.jpg .jpeg image/jpeg
.gif image/gif
.svg image/svg+xml
.ico image/x-icon

TLS

srv speaks plain HTTP only. For HTTPS, terminate TLS in front of it. tls.conf is an stunnel example that accepts on 443 and forwards to a local srv on 80:

stunnel tls.conf

Test

The suite in test/ builds a fresh server and drives it with curl (and raw /dev/tcp for malformed-request cases). 64 tests cover seven groups: proto, routing, mime, headers, security, keepalive, concurrency.

make test            # functional suite, plain mode
make test-valgrind   # same tests under valgrind + a leak verdict
make test-asan       # same tests built with AddressSanitizer
make menu            # interactive dialog(1) front-end

The runner takes a mode and an optional group list:

test/run.sh [plain|valgrind|asan] [group ...]

HOST, PORT, and SRV_USER override the defaults (127.0.0.1, 18080, the current user).

License

See LICENSE.


Matt Vianueva - diatribes@gmail.com

About

A static http server.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors