a file server?
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-24 08:03:46 +03:30
doc init 2026-10-18 02:27:47 +03:30
src init 2026-10-18 02:27:47 +03:30
.gitignore init 2026-10-18 02:27:47 +03:30
LICENSE init 2026-10-18 02:27:47 +03:30
README.org readme change 2026-10-24 08:03:46 +03:30

filed

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 (206 and 416)
  • Last-Modified with If-Modified-Since support (304)
  • index.html lookup, 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 SIGINT and SIGTERM
  • 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

  1. the target is cut at ?; the query plays no part in the lookup and is kept only in redirects.
  2. the path is precent-coded; a bad escape or %00 returns 400
  3. any path segment that starts with . returns 404; this hides dotfiles and rejects .. in plain and %2e%2e form.
  4. the decoded path is joined 2 the root and resolved with realpath()
  5. 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 301 to the slash form.
  • with a trailing slash, index.html is served if present.
  • otherwise, a listing is generated instead, or 403 if -l is 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.