Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 50 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* **Markdown support:** if a `README.md` or `readme.md` file is present in the directory during directory listing, it will be rendered as HTML. Additional support for GitHub-flavored markdown is also available.
* **Fully air-gapped:** the directory listing feature is fully air-gapped, meaning that it does not require any external resources to be loaded. This is useful for environments where internet access is not available.
* **Redirections support:** if a `_redirections` file exists in the target directory, it will be used to redirect requests to other locations. Learn about the syntax [in the docs](docs/redirections.md).
* **TLS / HTTPS support:** serve content over HTTPS with automatic Let's Encrypt certificates or your own cert+key. Includes HTTP-to-HTTPS redirects, certificate reload via SIGHUP or API, and a `/_/tls` metadata endpoint. Learn more [in the docs](docs/tls.md).

The app is available both as a standalone binary and as a Docker container image.

Expand Down Expand Up @@ -80,34 +81,55 @@ A simple HTTP server and a directory listing tool.
Usage:
http-server [flags]

Flags:
--banner string markdown text to be rendered at the top of the directory listing page
--cors enable CORS support by setting the "Access-Control-Allow-Origin" header to "*"
--custom-404 string custom "page not found" to serve
--custom-404-code int custom status code for pages not found
--custom-css-file string path within the served files to a custom CSS file
--disable-cache-buster disable the cache buster for assets from the directory listing feature
--disable-directory-listing disable the directory listing feature and return 404s for directories without index
--disable-etag disable etag header generation
--disable-markdown disable the markdown rendering feature
--disable-redirects disable redirection file handling
--ensure-unexpired-jwt enable time validation for JWT claims "exp" and "nbf"
--etag-max-size string maximum size for etag header generation, where bigger size = more memory usage (default "5M")
--force-download-extensions strings file extensions that should be downloaded instead of displayed in browser
--gzip enable gzip compression for supported content-types
-h, --help help for http-server
--hide-files-in-markdown hide file and directory listing in markdown rendering
--hide-links hide the links to this project's source code visible in the header and footer
--jwt-key string signing key for JWT authentication
--markdown-before-dir render markdown content before the directory listing
--password string password for basic authentication
-d, --path string path to the directory you want to serve (default "./")
--pathprefix string path prefix for the URL where the server will listen on (default "/")
-p, --port int port to configure the server to listen on (default 5000)
--render-all-markdown if enabled, all Markdown files will be rendered using the same rendering as the directory listing READMEs
--title string title of the directory listing page
--username string username for basic authentication
-v, --version version for http-server
Server:
--banner string markdown text to be rendered at the top of the directory listing page
--cors enable CORS support by setting the "Access-Control-Allow-Origin" header to "*"
--gzip enable gzip compression for supported content-types
--hide-links hide the links to this project's source code visible in the header and footer
-d, --path string path to the directory you want to serve (default "./")
--pathprefix string path prefix for the URL where the server will listen on (default "/")
-p, --port int port to configure the server to listen on (default 5000)
--title string title of the directory listing page

TLS:
--hostname string hostname for HTTP-to-HTTPS redirects and automatic certificate provisioning
--http-port int HTTP listener port when TLS is active, use 0 to disable HTTP redirect (default 80)
--https-port int HTTPS listener port when TLS is active (default 443)
--tls-cache-dir string directory for storing automatic TLS certificates (default: .certmagic/ in served directory)
--tls-cert string path to TLS certificate file in PEM format
--tls-email string email address for Let's Encrypt account notifications
--tls-key string path to TLS private key file in PEM format

Authentication:
--ensure-unexpired-jwt enable time validation for JWT claims "exp" and "nbf"
--jwt-key string signing key for JWT authentication
--password string password for basic authentication
--username string username for basic authentication

Directory Listing:
--custom-css-file string path within the served files to a custom CSS file
--disable-cache-buster disable the cache buster for assets from the directory listing feature
--disable-directory-listing disable the directory listing feature and return 404s for directories without index
--disable-markdown disable the markdown rendering feature
--hide-files-in-markdown hide file and directory listing in markdown rendering
--markdown-before-dir render markdown content before the directory listing
--render-all-markdown if enabled, all Markdown files will be rendered using the same rendering as the directory listing READMEs

Error Pages:
--custom-404 string custom "page not found" to serve
--custom-404-code int custom status code for pages not found

Performance:
--disable-etag disable etag header generation
--etag-max-size string maximum size for etag header generation, where bigger size = more memory usage (default "5M")
--force-download-extensions strings file extensions that should be downloaded instead of displayed in browser

Other:
--disable-redirects disable redirection file handling

Global:
-h, --help help for http-server
-v, --version version for http-server
```

### Detailed configuration
Expand Down
95 changes: 95 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,105 @@ func run() error {
flags.BoolVar(&srv.FullMarkdownRender, "render-all-markdown", false, "if enabled, all Markdown files will be rendered using the same rendering as the directory listing READMEs")
flags.StringSliceVar(&srv.ForceDownloadExtensions, "force-download-extensions", nil, "file extensions that should be downloaded instead of displayed in browser")

// TLS flags
flags.StringVar(&srv.TLSCert, "tls-cert", "", "path to TLS certificate file in PEM format")
flags.StringVar(&srv.TLSKey, "tls-key", "", "path to TLS private key file in PEM format")
flags.IntVar(&srv.HTTPPort, "http-port", 80, "HTTP listener port when TLS is active, use 0 to disable HTTP redirect")
flags.IntVar(&srv.HTTPSPort, "https-port", 443, "HTTPS listener port when TLS is active")
flags.StringVar(&srv.Hostname, "hostname", "", "hostname for HTTP-to-HTTPS redirects and automatic certificate provisioning")
flags.StringVar(&srv.TLSEmail, "tls-email", "", "email address for Let's Encrypt account notifications")
flags.StringVar(&srv.TLSCacheDir, "tls-cache-dir", "", "directory for storing automatic TLS certificates (default: .certmagic/ in served directory)")

// Annotate flags with groups for the custom help template
flagGroups := map[string][]string{
"Server": {"port", "path", "pathprefix", "title", "banner", "gzip", "cors", "hide-links"},
"TLS": {"tls-cert", "tls-key", "http-port", "https-port", "hostname", "tls-email", "tls-cache-dir"},
"Authentication": {"username", "password", "jwt-key", "ensure-unexpired-jwt"},
"Directory Listing": {
"disable-directory-listing", "disable-markdown", "markdown-before-dir", "render-all-markdown",
"hide-files-in-markdown", "custom-css-file", "disable-cache-buster",
},
"Error Pages": {"custom-404", "custom-404-code"},
"Performance": {"disable-etag", "etag-max-size", "force-download-extensions"},
"Other": {"disable-redirects"},
}

for group, names := range flagGroups {
for _, name := range names {
rootCmd.Flags().SetAnnotation(name, "group", []string{group}) //nolint:errcheck // best-effort grouping
}
}

rootCmd.SetUsageFunc(groupedUsageFunc(flagGroups))

//nolint:wrapcheck // no need to wrap this error
return rootCmd.Execute()
}

// groupedUsageFunc returns a cobra UsageFunc that renders flags organized
// by group annotations instead of a flat alphabetical list.
func groupedUsageFunc(groups map[string][]string) func(*cobra.Command) error {
// Build a reverse map: flag name -> group name
flagToGroup := make(map[string]string)
for group, names := range groups {
for _, name := range names {
flagToGroup[name] = group
}
}

// Ordered group names for consistent output
groupOrder := []string{"Server", "TLS", "Authentication", "Directory Listing", "Error Pages", "Performance", "Other"}

return func(cmd *cobra.Command) error {
out := cmd.OutOrStdout()
fmt.Fprintf(out, "Usage:\n %s\n\n", cmd.UseLine())

// Collect flags by group
grouped := make(map[string][]*pflag.Flag)
var ungrouped []*pflag.Flag

cmd.Flags().VisitAll(func(f *pflag.Flag) {
if f.Hidden {
return
}
if group, ok := flagToGroup[f.Name]; ok {
grouped[group] = append(grouped[group], f)
} else {
ungrouped = append(ungrouped, f)
}
})

// Print each group
for _, group := range groupOrder {
flags, ok := grouped[group]
if !ok || len(flags) == 0 {
continue
}

fmt.Fprintf(out, "%s:\n", group)
fs := pflag.NewFlagSet(group, pflag.ContinueOnError)
for _, f := range flags {
fs.AddFlag(f)
}
fmt.Fprint(out, fs.FlagUsages())
fmt.Fprintln(out)
}

// Print ungrouped flags (help, version)
if len(ungrouped) > 0 {
fmt.Fprintf(out, "Global:\n")
fs := pflag.NewFlagSet("global", pflag.ContinueOnError)
for _, f := range ungrouped {
fs.AddFlag(f)
}
fmt.Fprint(out, fs.FlagUsages())
fmt.Fprintln(out)
}

return nil
}
}

// sendPipeToLogger reads from the pipe and sends the output to the logger
func sendPipeToLogger(logger *log.Logger, pipe io.Reader) {
// Scan the log messages per line
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ This is the documentation for `http-server`, a simple, no-dependencies command-l
* [Authentication](authentication.md)
* [Redirections](redirections.md)
* [Force Download Extensions](force-download.md)
* [TLS / HTTPS support](tls.md)
2 changes: 1 addition & 1 deletion docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

You can enable basic authentication by using the `--username` and `--password` flags. If both flags are provided, the server will require the provided username and password to access its contents.

This is the simplest form of authentication. The username and password are sent in plain text over the network if you are not serving `http-server` via HTTPS. As such, it's not recommended for production use. If you still decide to use it, use a strong password.
This is the simplest form of authentication. The username and password are sent in plain text over the network if you are not using HTTPS. For production use, enable [TLS support](tls.md) with `--tls-cert` and `--tls-key` to encrypt the connection. If you still decide to use it without HTTPS, use a strong password.

### JWT authentication

Expand Down
6 changes: 5 additions & 1 deletion docs/static-file-server.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Static file server

The core nature of `http-server` is to be a static file server. You can serve any folder in the node where `http-server` is running. **None of the files are hidden**, which means if the user that's executing `http-server` can see them, then they will be listed. The only exception is the `.http-server.yaml` configuration file, which is removed from view and direct access, since it may contain sensitive information.
The core nature of `http-server` is to be a static file server. You can serve any folder in the node where `http-server` is running. **None of the files are hidden**, which means if the user that's executing `http-server` can see them, then they will be listed. The exceptions are:

* The `.http-server.yaml` configuration file, which is removed from view and direct access since it may contain sensitive information.
* The `_redirects` file used for redirection rules.
* TLS certificate and key files, if they reside inside the served directory and TLS is active. These are hidden from listings and blocked from direct download. See the [TLS documentation](tls.md) for details.

The files served are type-hinted and their `Content-Type` header set through this method. The server also supports `Accept-Ranges` header, meaning you can perform partial requests for bigger files and ensure it's possible to download them in chunks if needed.
176 changes: 176 additions & 0 deletions docs/tls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# TLS / HTTPS support

`http-server` supports serving content over HTTPS in two modes:

- **Automatic certificates:** pass `--hostname` and `http-server` provisions a free TLS certificate from Let's Encrypt automatically. No cert files needed.
- **Bring your own certificate:** pass `--tls-cert` and `--tls-key` with your own certificate and key files.

## Automatic certificates (Let's Encrypt)

The simplest way to serve over HTTPS. Just pass `--hostname` with your domain:

```bash
http-server --hostname example.com -d ./site
```

This starts two listeners:

- **HTTPS** on port 443 serving your content with a certificate from Let's Encrypt
- **HTTP** on port 80 handling ACME HTTP-01 challenges and redirecting all other requests to HTTPS

Certificate provisioning, renewal, and storage are fully automatic. Certificates are stored locally in `~/.local/share/certmagic/`.

### How it works

When `http-server` starts with `--hostname`, it contacts Let's Encrypt's ACME servers to prove you control the domain. It does this by responding to an HTTP-01 challenge: Let's Encrypt makes an HTTP request to `http://yourdomain.com/.well-known/acme-challenge/<token>` on port 80, and `http-server` responds with the expected token. Once verified, a certificate is issued.

This means:
- **Port 80 must be reachable** from the public internet
- **DNS must point to your server** (A or AAAA record for the hostname)
- The certificate is provisioned on first startup and renewed automatically before expiry

### ACME email (optional)

Let's Encrypt can send expiry notifications to an email address:

```bash
http-server --hostname example.com --tls-email you@example.com -d ./site
```

## Bring your own certificate

If you already have a certificate (from any CA, self-signed, or internal PKI), pass the cert and key files:

```bash
http-server --tls-cert cert.pem --tls-key key.pem --hostname example.com -d ./site
```

Both `--tls-cert` and `--tls-key` must be provided together. `--hostname` is required for HTTP-to-HTTPS redirect URL construction.

## Flags

| Flag | Default | Description |
|------|---------|-------------|
| `--hostname` | *(none)* | Domain name for TLS. Alone = auto-cert. With `--tls-cert`/`--tls-key` = BYO cert. |
| `--tls-cert` | *(none)* | Path to TLS certificate file (PEM format, BYO mode) |
| `--tls-key` | *(none)* | Path to TLS private key file (PEM format, BYO mode) |
| `--tls-email` | *(none)* | Email for Let's Encrypt notifications (auto mode) |
| `--tls-cache-dir` | `.certmagic/` in served dir | Directory for storing automatic TLS certificates |
| `--https-port` | `443` | Port for the HTTPS listener |
| `--http-port` | `80` | Port for the HTTP listener (use `0` to disable) |

When TLS is active, the `--port` flag cannot be used. Use `--http-port` and `--https-port` instead.

## Custom ports

To use non-privileged ports (no root required):

```bash
http-server --tls-cert cert.pem --tls-key key.pem --hostname localhost \
--https-port 8443 --http-port 8080 -d ./site
```

## Disabling the HTTP redirect

If you only want HTTPS with no HTTP listener at all:

```bash
http-server --tls-cert cert.pem --tls-key key.pem --hostname example.com \
--http-port 0 -d ./site
```

## Certificate storage

In auto mode, certificates and private keys are stored on the local filesystem at `.certmagic/` inside the served directory by default. This means certificates persist across restarts and multiple instances sharing the same volume will reuse the same certificates (certmagic uses file locking to prevent ACME races).

To use a different storage location:

```bash
http-server --hostname example.com --tls-cache-dir /etc/http-server/certs -d ./site
```

The `.certmagic/` directory (or custom cache dir) is automatically hidden from directory listings and blocked from direct URL access. Private keys stored inside it are never served to clients.

## Certificate file hiding

If your certificate and key files are inside the directory being served, they are automatically hidden from directory listings and blocked from direct download. A warning is printed at startup recommending you move them outside the served directory.

## Certificate reload

You can reload the TLS certificate without restarting the server:

- **Unix:** send `SIGHUP` to the process: `kill -HUP $(pgrep http-server)`
- **Any platform:** send a POST request to the `/_/tls/reload` endpoint

The reload endpoint returns JSON:

```json
{"reloaded": true}
```

If the new certificate is invalid, the old certificate is preserved and an error is returned.

## Certificate metadata endpoint

When TLS is active, a `GET /_/tls` endpoint returns JSON metadata about the currently loaded certificate:

```json
{
"tls_mode": "byo",
"tls_cert_subject": "example.com",
"tls_cert_sans": ["example.com", "www.example.com"],
"tls_cert_issuer": "Let's Encrypt",
"tls_cert_not_after": "2026-06-15T00:00:00Z",
"tls_cert_not_before": "2026-03-15T00:00:00Z"
}
```

When TLS is not active, this endpoint is not available.

Both `/_/tls` and `/_/tls/reload` are protected by the same authentication (basic auth or JWT) as the rest of the server, if configured.

## Validation

At startup, `http-server` validates:

- Both `--tls-cert` and `--tls-key` are provided together
- The certificate and key files exist and form a valid pair
- The certificate has not expired (hard error)
- The certificate is not future-dated (hard error)
- The certificate expiry is within 30 days (warning, server still starts)
- `--port` is not explicitly set alongside TLS flags
- `--http-port` and `--https-port` differ (unless `--http-port` is `0`)
- `--hostname` is provided

## Docker usage

```yaml
services:
http-server:
image: ghcr.io/patrickdappollonio/docker-http-server:v2
restart: unless-stopped
ports:
- "443:443"
- "80:80"
volumes:
- ./site:/html:ro
- ./certs:/certs:ro
environment:
- TLS_CERT=/certs/cert.pem
- TLS_KEY=/certs/key.pem
- HOSTNAME=example.com
```

Or with custom ports:

```yaml
ports:
- "8443:8443"
- "8080:8080"
environment:
- TLS_CERT=/certs/cert.pem
- TLS_KEY=/certs/key.pem
- HOSTNAME=example.com
- HTTPS_PORT=8443
- HTTP_PORT=8080
```
Loading