diff --git a/README.md b/README.md index 0c36bfb..36d842b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Yeah, decided to drop support of unsecured HTTPS. Two-years ago, when I started * Light container * More secure than official images (see below) * Log enabled + * Specify custom response headers per path and filetype [(info)](./docs/header-config.md) ### Why? Because the official Golang image is wayyyy too big (around 1/2Gb as you can see below) and could be insecure. @@ -39,7 +40,7 @@ docker run -d -p 80:8043 -v path/to/website:/srv/http --name goStatic pierrezemb ``` ./goStatic --help -Usage of /goStatic: +Usage of ./goStatic: -append-header HeaderName:Value HTTP response header, specified as HeaderName:Value that should be added to all responses. -context string @@ -50,8 +51,14 @@ Usage of /goStatic: Enable basic auth. By default, password are randomly generated. Use --set-basic-auth to set it. -enable-health Enable health check endpoint. You can call /health to get a 200 response. Useful for Kubernetes, OpenFaas, etc. + -enable-logging + Enable log request -fallback string - Default fallback file. Either absolute for a specific asset (/index.html), or relative to recursively resolve (index.html). + Default fallback file. Either absolute for a specific asset (/index.html), or relative to recursively resolve (index.html) + -header-config-path string + Path to the config file for custom response headers (default "/config/headerConfig.json") + -https-promote + All HTTP requests should be redirected to HTTPS -password-length int Size of the randomized password (default 16) -path string @@ -60,10 +67,6 @@ Usage of /goStatic: The listening port (default 8043) -set-basic-auth string Define the basic auth. Form must be user:password - -https-promote - Connections to http: are redirected to https: - -enable-logging - Writes a simple log entry for requests to the server ``` #### Fallback diff --git a/customHeaders.go b/customHeaders.go new file mode 100644 index 0000000..1e819bf --- /dev/null +++ b/customHeaders.go @@ -0,0 +1,106 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" +) + +// HeaderConfigArray is the array which contains all the custom header rules +type HeaderConfigArray struct { + Configs []HeaderConfig `json:"configs"` +} + +// HeaderConfig is a single header rule specification +type HeaderConfig struct { + Path string `json:"path"` + FileExtension string `json:"fileExtension"` + Headers []HeaderDefiniton `json:"headers"` +} + +// HeaderDefiniton is a key value pair of a specified header rule +type HeaderDefiniton struct { + Key string `json:"key"` + Value string `json:"value"` +} + +var headerConfigs HeaderConfigArray + +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +func logHeaderConfig(config HeaderConfig) { + fmt.Println("Path: " + config.Path) + fmt.Println("FileExtension: " + config.FileExtension) + + for j := 0; j < len(config.Headers); j++ { + headerRule := config.Headers[j] + fmt.Println(headerRule.Key, ":", headerRule.Value) + } + + fmt.Println("------------------------------") +} + +func initHeaderConfig(headerConfigPath string) bool { + headerConfigValid := false + + if fileExists(headerConfigPath) { + jsonFile, err := os.Open(headerConfigPath) + if err != nil { + fmt.Println("Cant't read header config file. Error:") + fmt.Println(err) + } else { + byteValue, _ := ioutil.ReadAll(jsonFile) + + json.Unmarshal(byteValue, &headerConfigs) + + if len(headerConfigs.Configs) > 0 { + headerConfigValid = true + fmt.Println("Found header config file. Rules:") + fmt.Println("------------------------------") + + for i := 0; i < len(headerConfigs.Configs); i++ { + configEntry := headerConfigs.Configs[i] + logHeaderConfig(configEntry) + } + } else { + fmt.Println("No rules found in header config file.") + } + + } + jsonFile.Close() + } + + return headerConfigValid +} + +func customHeadersMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reqFileExtension := filepath.Ext(r.URL.Path) + + for i := 0; i < len(headerConfigs.Configs); i++ { + configEntry := headerConfigs.Configs[i] + + fileMatch := configEntry.FileExtension == "*" || reqFileExtension == "."+configEntry.FileExtension + pathMatch := configEntry.Path == "*" || strings.HasPrefix(r.URL.Path, configEntry.Path) + + if fileMatch && pathMatch { + for j := 0; j < len(configEntry.Headers); j++ { + headerEntry := configEntry.Headers[j] + w.Header().Set(headerEntry.Key, headerEntry.Value) + } + } + } + + next.ServeHTTP(w, r) + }) +} diff --git a/docs/header-config.md b/docs/header-config.md new file mode 100644 index 0000000..b97255d --- /dev/null +++ b/docs/header-config.md @@ -0,0 +1,80 @@ +# Header Config + +With the header config, you can specify custom [HTTP Header](https://developer.mozilla.org/de/docs/Web/HTTP/Headers) for the responses of certain file types and paths. + +## Config + +You have to create a JSON file that serves as a config. The JSON must contain a `configs` array. For every entry, you can specify a certain path that must be matched as well as a file extension. You can use the `*` symbol to use the config entry for any path or filename. Note that the path option only matches the requested path from the start. Thatswhy you have to start with a `/` and can use paths like `/files/static/css`. The `headers` array includes a key-value pair of the actual header rule. The headers are not parsed so double check your spelling and test your site. + +The created JSON config has to be mounted into the container via a volume into `/config/headerConfig.json` per default. When this file does not exist inside the container, the header middleware will not be active. + +Example command to add to the docker run command: + +``` +docker run ... -v /your/path/to/the/config/myConfig.json:/config/headerConfig.json +``` + +You can also specify where you want to mount your config into with the `header-config-path` flag: + +``` +docker run ... -v /your/path/to/the/config/myConfig.json:/other/path/myConfig.json -header-config-path=/other/path/myConfig.json +``` + +On startup, the container will log the found header rules. + +## Example headerConfig.json + +```json +{ + "configs": [ + { + "path": "*", + "fileExtension": "html", + "headers": [ + { + "key": "cache-control", + "value": "public, max-age=0, must-revalidate" + }, + { + "key": "Strict-Transport-Security", + "value": "max-age=31536000; includeSubDomains;" + } + ] + }, + { + "path": "*", + "fileExtension": "css", + "headers": [ + { + "key": "cache-control", + "value": "public, max-age=31536000, immutable" + } + ] + }, + { + "path": "/page-data", + "fileExtension": "json", + "headers": [ + { + "key": "cache-control", + "value": "public, max-age=0, must-revalidate" + }, + { + "key": "content-language", + "value": "en" + } + ] + }, + { + "path": "/static/", + "fileExtension": "*", + "headers": [ + { + "key": "cache-control", + "value": "public, max-age=31536000, immutable" + } + ] + } + ] +} +``` diff --git a/main.go b/main.go index cb171a6..2590263 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,7 @@ var ( sizeRandom = flag.Int("password-length", 16, "Size of the randomized password") logRequest = flag.Bool("enable-logging", false, "Enable log request") httpsPromote = flag.Bool("https-promote", false, "All HTTP requests should be redirected to HTTPS") + headerConfigPath = flag.String("header-config-path", "/config/headerConfig.json", "Path to the config file for custom response headers") username string password string @@ -80,6 +81,7 @@ func handleReq(h http.Handler) http.Handler { if *logRequest { log.Println(r.Method, r.URL.Path) } + h.ServeHTTP(w, r) }) } @@ -122,6 +124,11 @@ func main() { handler = authMiddleware(handler) } + headerConfigValid := initHeaderConfig(*headerConfigPath) + if headerConfigValid { + handler = customHeadersMiddleware(handler) + } + // Extra headers. if len(*headerFlag) > 0 { header, headerValue := parseHeaderFlag(*headerFlag)