diff --git a/README.md b/README.md index c844049..5aa334f 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,21 @@ Available on Docker Hub at https://hub.docker.com/r/halverneus/static-file-serve Environment variables with defaults: ```bash -# Optional Hostname for binding. Leave black to accept any incoming HTTP request on the prescribed port. +# Optional Hostname for binding. Leave black to accept any incoming HTTP request +# on the prescribed port. HOST= # If assigned, must be a valid port number. PORT=8080 # Folder with the content to serve. FOLDER=/web +# URL path prefix. If 'my.file' is in the root of $FOLDER and $URL_PREFIX is +# '/my/place' then file is retrieved with 'http://$HOST:$PORT/my/place/my.file'. +URL_PREFIX= +# Paths to the TLS certificate and key. If one is set then both must be set. If +# both set then files are served using HTTPS. If neither are set then files are +# served using HTTP. +TLS_CERT= +TLS_KEY= ``` ### Without Docker @@ -30,4 +39,10 @@ Any of the variables can also be modified: docker run -d -v /home/me/dev/source:/content/html -v /home/me/dev/files:/content/more/files -e FOLDER=/content -p 8080:8080 halverneus/static-file-server:latest ``` +### Also try... +``` +./serve help +# OR +docker run -it halverneus/static-file-server:latest help +``` This maybe a cheesy program, but it is convenient and less than 6MB in size. diff --git a/serve.go b/serve.go index 4b44119..5a7cca3 100644 --- a/serve.go +++ b/serve.go @@ -1,21 +1,177 @@ package main import ( + "fmt" + "log" "net/http" "os" + "strings" +) + +var ( + version = "Version 1.1" + + help = ` +NAME + static-file-server + +SYNOPSIS + static-file-server + static-file-server [ help | -help | --help ] + static-file-server [ version | -version | --version ] + +DESCRIPTION + The Static File Server is intended to be a tiny, fast and simple solution + for serving files over HTTP. The features included are limited to make to + binding to a host name and port, selecting a folder to serve, choosing a + URL path prefix and selecting TLS certificates. If you want really awesome + reverse proxy features, I recommend Nginx. + +DEPENDENCIES + None... not even libc! + +ENVIRONMENT VARIABLES + FOLDER + The path to the folder containing the contents to be served over + HTTP(s). If not supplied, defaults to '/web' (for Docker reasons). + HOST + The hostname used for binding. If not supplied, contents will be served + to a client without regard for the hostname. + PORT + The port used for binding. If not supplied, defaults to port '8080'. + TLS_CERT + Path to the TLS certificate file to serve files using HTTPS. If supplied + then TLS_KEY must also be supplied. If not supplied, contents will be + served via HTTP. + TLS_KEY + Path to the TLS key file to serve files using HTTPS. If supplied then + TLS_CERT must also be supplied. If not supplied, contents will be served + via HTTPS + URL_PREFIX + The prefix to use in the URL path. If supplied, then the prefix must + start with a forward-slash and NOT end with a forward-slash. If not + supplied then no prefix is used. + +USAGE + FILE LAYOUT + /var/www/sub/my.file + + COMMAND + export FOLDER=/var/www/sub + static-file-server + Retrieve with: wget http://localhost:8080/my.file + wget http://my.machine:8080/my.file + + export FOLDER=/var/www + export HOST=my.machine + export PORT=80 + static-file-server + Retrieve with: wget http://my.machine/sub/my.file + + export FOLDER=/var/www/sub + export HOST=my.machine + export PORT=80 + export URL_PREFIX=/my/stuff + static-file-server + Retrieve with: wget http://my.machine/my/stuff/my.file + + export FOLDER=/var/www/sub + export TLS_CERT=/etc/server/my.machine.crt + export TLS_KEY=/etc/server/my.machine.key + static-file-server + Retrieve with: wget https://my.machine:8080/my.file + + export FOLDER=/var/www/sub + export PORT=443 + export TLS_CERT=/etc/server/my.machine.crt + export TLS_KEY=/etc/server/my.machine.key + static-file-server + Retrieve with: wget https://my.machine/my.file +` ) func main() { + // Evaluate and execute subcommand if supplied. + if 1 < len(os.Args) { + arg := os.Args[1] + switch { + case strings.Contains(arg, "help"): + fmt.Println(help) + case strings.Contains(arg, "version"): + fmt.Println(version) + default: + name := os.Args[0] + log.Fatalf("Unknown argument: %s. Try '%s help'.", arg, name) + } + return + } + + // Collect environment variables. + folder := env("FOLDER", "/web") + "/" host := env("HOST", "") port := env("PORT", "8080") - folder := env("FOLDER", "/web") + "/" + tlsCert := env("TLS_CERT", "") + tlsKey := env("TLS_KEY", "") + urlPrefix := env("URL_PREFIX", "") + + // If HTTPS is to be used, verify both TLS_* environment variables are set. + if 0 < len(tlsCert) || 0 < len(tlsKey) { + if 0 == len(tlsCert) || 0 == len(tlsKey) { + log.Fatalln( + "If value for environment variable 'TLS_CERT' or 'TLS_KEY' is set " + + "then value for environment variable 'TLS_KEY' or 'TLS_CERT' must " + + "also be set.", + ) + } + } + + // If the URL path prefix is to be used, verify it is properly formatted. + if 0 < len(urlPrefix) && + (!strings.HasPrefix(urlPrefix, "/") || strings.HasSuffix(urlPrefix, "/")) { + log.Fatalln( + "Value for environment variable 'URL_PREFIX' must start " + + "with '/' and not end with '/'. Example: '/my/prefix'", + ) + } + + // Choose and set the appropriate, optimized static file serving function. + var handler http.HandlerFunc + if 0 == len(urlPrefix) { + handler = basicHandler(folder) + } else { + handler = prefixHandler(folder, urlPrefix) + } + http.HandleFunc("/", handler) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Serve files over HTTP or HTTPS based on paths to TLS files being provided. + if 0 == len(tlsCert) { + log.Fatalln(http.ListenAndServe(host+":"+port, nil)) + } else { + log.Fatalln(http.ListenAndServeTLS(host+":"+port, tlsCert, tlsKey, nil)) + } +} + +// basicHandler serves files from the folder passed. +func basicHandler(folder string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, folder+r.URL.Path) - }) - http.ListenAndServe(host+":"+port, nil) + } +} + +// prefixHandler removes the URL path prefix before serving files from the +// folder passed. +func prefixHandler(folder, urlPrefix string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, urlPrefix) { + http.NotFound(w, r) + return + } + http.ServeFile(w, r, folder+strings.TrimPrefix(r.URL.Path, urlPrefix)) + } } +// env returns the value for an environment variable or, if not set, a fallback +// value. func env(key, fallback string) string { if value := os.Getenv(key); 0 < len(value) { return value