From ea0bcf28bb59f12969fa81ed669abcae55bec090 Mon Sep 17 00:00:00 2001 From: Jeromy Streets Date: Mon, 11 Oct 2021 13:31:25 -0700 Subject: [PATCH 1/4] Added ability to set minimum TLS version. Updated compiler version. Minor code clean-up. Added funding info. --- .github/FUNDING.yml | 1 + Dockerfile | 6 ++-- Dockerfile.all | 6 ++-- README.md | 14 ++++++++ cli/help/help.go | 6 ++++ cli/server/server.go | 1 + config/config.go | 84 +++++++++++++++++++++++++++++++++---------- config/config_test.go | 72 ++++++++++++++++++++++++++++++------- handle/handle.go | 41 ++++++++++++++++++--- handle/handle_test.go | 23 ++++++++++++ 10 files changed, 212 insertions(+), 42 deletions(-) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..63d3c52 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: halverneus diff --git a/Dockerfile b/Dockerfile index 2deb1ac..e799f5c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ ################################################################################ ## GO BUILDER ################################################################################ -FROM golang:1.16.5 as builder +FROM golang:1.17.2 as builder -ENV VERSION 1.8.4 +ENV VERSION 1.8.5 ENV BUILD_DIR /build RUN mkdir -p ${BUILD_DIR} @@ -36,5 +36,5 @@ LABEL life.apets.vendor="Halverneus" \ life.apets.url="https://github.com/halverneus/static-file-server" \ life.apets.name="Static File Server" \ life.apets.description="A tiny static file server" \ - life.apets.version="v1.8.4" \ + life.apets.version="v1.8.5" \ life.apets.schema-version="1.0" diff --git a/Dockerfile.all b/Dockerfile.all index 0bc4d5a..f7a9bd8 100644 --- a/Dockerfile.all +++ b/Dockerfile.all @@ -1,6 +1,6 @@ -FROM golang:1.16.5 as builder +FROM golang:1.17.2 as builder -ENV VERSION 1.8.4 +ENV VERSION 1.8.5 ENV BUILD_DIR /build RUN mkdir -p ${BUILD_DIR} @@ -21,5 +21,5 @@ LABEL life.apets.vendor="Halverneus" \ life.apets.url="https://github.com/halverneus/static-file-server" \ life.apets.name="Static File Server" \ life.apets.description="A tiny static file server" \ - life.apets.version="v1.8.4" \ + life.apets.version="v1.8.5" \ life.apets.schema-version="1.0" diff --git a/README.md b/README.md index d77b07a..d33df3c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # static-file-server + + +Buy me a Smoothie + + ## Introduction Tiny, simple static file server using environment variables for configuration. @@ -47,6 +54,12 @@ URL_PREFIX= TLS_CERT= TLS_KEY= +# If TLS certificates are set then the minimum TLS version may also be set. If +# the value isn't set then the default minimum TLS version is 1.0. Allowed +# values include "TLS10", "TLS11", "TLS12" and "TLS13" for TLS1.0, TLS1.1, +# TLS1.2 and TLS1.3, respectively. The value is not case-sensitive. +TLS_MIN_VERS= + # List of accepted HTTP referrers. Return 403 if HTTP header `Referer` does not # match prefixes provided in the list. # Examples: @@ -73,6 +86,7 @@ referrers: [] show-listing: true tls-cert: "" tls-key: "" +tls-min-vers: "" url-prefix: "" ``` diff --git a/cli/help/help.go b/cli/help/help.go index e5bbb65..c71cb34 100644 --- a/cli/help/help.go +++ b/cli/help/help.go @@ -75,6 +75,10 @@ ENVIRONMENT VARIABLES 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 + TLS_MIN_VERS + The minimum TLS version to use. If not supplied, defaults to TLS1.0. + Acceptable values are 'TLS10', 'TLS11', 'TLS12' and 'TLS13' for TLS1.0, + TLS1.1, TLS1.2 and TLS1.3, respectively. Values are not case-sensitive. 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 @@ -98,6 +102,7 @@ CONFIGURATION FILE show-listing: true tls-cert: "" tls-key: "" + tls-min-vers: "" url-prefix: "" ---------------------------------------------------------------------------- @@ -150,6 +155,7 @@ USAGE export PORT=443 export TLS_CERT=/etc/server/my.machine.crt export TLS_KEY=/etc/server/my.machine.key + export TLS_MIN_VERS=TLS12 static-file-server Retrieve with: wget https://my.machine/my.file diff --git a/cli/server/server.go b/cli/server/server.go index 447c90d..19d55cb 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -76,6 +76,7 @@ func listenerSelector() (listener handle.ListenerFunc) { // Serve files over HTTP or HTTPS based on paths to TLS files being // provided. if 0 < len(config.Get.TLSCert) { + handle.SetMinimumTLSVersion(config.Get.TLSMinVers) listener = handle.TLSListening( config.Get.TLSCert, config.Get.TLSKey, diff --git a/config/config.go b/config/config.go index 58aee00..9c760d9 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,8 @@ package config import ( + "crypto/tls" + "errors" "fmt" "io/ioutil" "log" @@ -14,16 +16,18 @@ import ( var ( // Get the desired configuration value. Get struct { - Cors bool `yaml:"cors"` - Debug bool `yaml:"debug"` - Folder string `yaml:"folder"` - Host string `yaml:"host"` - Port uint16 `yaml:"port"` - ShowListing bool `yaml:"show-listing"` - TLSCert string `yaml:"tls-cert"` - TLSKey string `yaml:"tls-key"` - URLPrefix string `yaml:"url-prefix"` - Referrers []string `yaml:"referrers"` + Cors bool `yaml:"cors"` + Debug bool `yaml:"debug"` + Folder string `yaml:"folder"` + Host string `yaml:"host"` + Port uint16 `yaml:"port"` + ShowListing bool `yaml:"show-listing"` + TLSCert string `yaml:"tls-cert"` + TLSKey string `yaml:"tls-key"` + TLSMinVers uint16 `yaml:"-"` + TLSMinVersStr string `yaml:"tls-min-vers"` + URLPrefix string `yaml:"url-prefix"` + Referrers []string `yaml:"referrers"` } ) @@ -37,6 +41,7 @@ const ( showListingKey = "SHOW_LISTING" tlsCertKey = "TLS_CERT" tlsKeyKey = "TLS_KEY" + tlsMinVersKey = "TLS_MIN_VERS" urlPrefixKey = "URL_PREFIX" ) @@ -49,6 +54,7 @@ var ( defaultShowListing = true defaultTLSCert = "" defaultTLSKey = "" + defaultTLSMinVers = "" defaultURLPrefix = "" defaultCors = false ) @@ -67,6 +73,7 @@ func setDefaults() { Get.ShowListing = defaultShowListing Get.TLSCert = defaultTLSCert Get.TLSKey = defaultTLSKey + Get.TLSMinVersStr = defaultTLSMinVers Get.URLPrefix = defaultURLPrefix Get.Cors = defaultCors } @@ -74,7 +81,7 @@ func setDefaults() { // Load the configuration file. func Load(filename string) (err error) { // If no filename provided, assign envvars. - if "" == filename { + if filename == "" { overrideWithEnvVars() return } @@ -116,6 +123,7 @@ func overrideWithEnvVars() { Get.ShowListing = envAsBool(showListingKey, Get.ShowListing) Get.TLSCert = envAsStr(tlsCertKey, Get.TLSCert) Get.TLSKey = envAsStr(tlsKeyKey, Get.TLSKey) + Get.TLSMinVersStr = envAsStr(tlsMinVersKey, Get.TLSMinVersStr) Get.URLPrefix = envAsStr(urlPrefixKey, Get.URLPrefix) Get.Referrers = envAsStrSlice(referrersKey, Get.Referrers) } @@ -123,8 +131,9 @@ func overrideWithEnvVars() { // validate the configuration. func validate() error { // If HTTPS is to be used, verify both TLS_* environment variables are set. + useTLS := false if 0 < len(Get.TLSCert) || 0 < len(Get.TLSKey) { - if 0 == len(Get.TLSCert) || 0 == len(Get.TLSKey) { + if len(Get.TLSCert) == 0 || len(Get.TLSKey) == 0 { msg := "if value for either 'TLS_CERT' or 'TLS_KEY' is set then " + "then value for the other must also be set (values are " + "currently '%s' and '%s', respectively)" @@ -132,11 +141,30 @@ func validate() error { } if _, err := os.Stat(Get.TLSCert); nil != err { msg := "value of TLS_CERT is set with filename '%s' that returns %v" - return fmt.Errorf(msg, err) + return fmt.Errorf(msg, Get.TLSCert, err) } if _, err := os.Stat(Get.TLSKey); nil != err { msg := "value of TLS_KEY is set with filename '%s' that returns %v" - return fmt.Errorf(msg, err) + return fmt.Errorf(msg, Get.TLSKey, err) + } + useTLS = true + } + + // Verify TLS_MIN_VERS is only (optionally) set if TLS is to be used. + Get.TLSMinVers = tls.VersionTLS10 + if useTLS { + if 0 < len(Get.TLSMinVersStr) { + var err error + if Get.TLSMinVers, err = tlsMinVersAsUint16( + Get.TLSMinVersStr, + ); nil != err { + return err + } + } + } else { + if 0 < len(Get.TLSMinVersStr) { + msg := "value for 'TLS_MIN_VERS' is set but 'TLS_CERT' and 'TLS_KEY' are not" + return errors.New(msg) } } @@ -154,7 +182,7 @@ func validate() error { // envAsStr returns the value of the environment variable as a string if set. func envAsStr(key, fallback string) string { - if value := os.Getenv(key); "" != value { + if value := os.Getenv(key); value != "" { return value } return fallback @@ -163,7 +191,7 @@ func envAsStr(key, fallback string) string { // envAsStrSlice returns the value of the environment variable as a slice of // strings if set. func envAsStrSlice(key string, fallback []string) []string { - if value := os.Getenv(key); "" != value { + if value := os.Getenv(key); value != "" { return strings.Split(value, ",") } return fallback @@ -174,7 +202,7 @@ func envAsUint16(key string, fallback uint16) uint16 { // Retrieve the string value of the environment variable. If not set, // fallback is used. valueStr := os.Getenv(key) - if "" == valueStr { + if valueStr == "" { return fallback } @@ -198,7 +226,7 @@ func envAsBool(key string, fallback bool) bool { // Retrieve the string value of the environment variable. If not set, // fallback is used. valueStr := os.Getenv(key) - if "" == valueStr { + if valueStr == "" { return fallback } @@ -225,8 +253,26 @@ func strAsBool(value string) (result bool, err error) { result = true default: result = false - msg := "Unknown conversion from string to bool for value '%s'" + msg := "unknown conversion from string to bool for value '%s'" err = fmt.Errorf(msg, value) } return } + +// tlsMinVersAsUint16 converts the intent of the passed value into an +// enumeration for the crypto/tls package. +func tlsMinVersAsUint16(value string) (result uint16, err error) { + switch strings.ToLower(value) { + case "tls10": + result = tls.VersionTLS10 + case "tls11": + result = tls.VersionTLS11 + case "tls12": + result = tls.VersionTLS12 + case "tls13": + result = tls.VersionTLS13 + default: + err = fmt.Errorf("unknown value for TLS_MIN_VERS: %s", value) + } + return +} diff --git a/config/config_test.go b/config/config_test.go index 1a9d154..3199fa6 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "crypto/tls" "fmt" "io/ioutil" "os" @@ -160,27 +161,33 @@ func TestValidate(t *testing.T) { cert string key string prefix string + minTLS string isError bool }{ - {"Valid paths w/prefix", validPath, validPath, prefix, false}, - {"Valid paths wo/prefix", validPath, validPath, empty, false}, - {"Empty paths w/prefix", empty, empty, prefix, false}, - {"Empty paths wo/prefix", empty, empty, empty, false}, - {"Mixed paths w/prefix", empty, validPath, prefix, true}, - {"Alt mixed paths w/prefix", validPath, empty, prefix, true}, - {"Mixed paths wo/prefix", empty, validPath, empty, true}, - {"Alt mixed paths wo/prefix", validPath, empty, empty, true}, - {"Invalid cert w/prefix", invalidPath, validPath, prefix, true}, - {"Invalid key w/prefix", validPath, invalidPath, prefix, true}, - {"Invalid cert & key w/prefix", invalidPath, invalidPath, prefix, true}, - {"Prefix missing leading /", empty, empty, "my/prefix", true}, - {"Prefix with trailing /", empty, empty, "/my/prefix/", true}, + {"Valid paths w/prefix", validPath, validPath, prefix, "", false}, + {"Valid paths wo/prefix", validPath, validPath, empty, "", false}, + {"Empty paths w/prefix", empty, empty, prefix, "", false}, + {"Empty paths wo/prefix", empty, empty, empty, "", false}, + {"Mixed paths w/prefix", empty, validPath, prefix, "", true}, + {"Alt mixed paths w/prefix", validPath, empty, prefix, "", true}, + {"Mixed paths wo/prefix", empty, validPath, empty, "", true}, + {"Alt mixed paths wo/prefix", validPath, empty, empty, "", true}, + {"Invalid cert w/prefix", invalidPath, validPath, prefix, "", true}, + {"Invalid key w/prefix", validPath, invalidPath, prefix, "", true}, + {"Invalid cert & key w/prefix", invalidPath, invalidPath, prefix, "", true}, + {"Prefix missing leading /", empty, empty, "my/prefix", "", true}, + {"Prefix with trailing /", empty, empty, "/my/prefix/", "", true}, + {"Valid paths w/min ok TLS", validPath, validPath, prefix, "tls11", false}, + {"Valid paths w/min bad TLS", validPath, validPath, prefix, "bad", true}, + {"Empty paths w/min ok TLS", empty, empty, prefix, "tls11", true}, + {"Empty paths w/min bad TLS", empty, empty, prefix, "bad", true}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { Get.TLSCert = tc.cert Get.TLSKey = tc.key + Get.TLSMinVersStr = tc.minTLS Get.URLPrefix = tc.prefix err := validate() hasError := nil != err @@ -461,3 +468,42 @@ func TestStrAsBool(t *testing.T) { } } + +func TestTlsMinVersAsUint16(t *testing.T) { + testCases := []struct { + name string + value string + result uint16 + isError bool + }{ + {"Empty value", "", 0, true}, + {"Valid TLS1.0", "TLS10", tls.VersionTLS10, false}, + {"Valid TLS1.1", "tls11", tls.VersionTLS11, false}, + {"Valid TLS1.2", "tls12", tls.VersionTLS12, false}, + {"Valid TLS1.3", "tLS13", tls.VersionTLS13, false}, + {"Invalid TLS1.4", "tls14", 0, true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := tlsMinVersAsUint16(tc.value) + if result != tc.result { + t.Errorf( + "Expected %d for %s but got %d", + tc.result, tc.value, result, + ) + } + if tc.isError && nil == err { + t.Errorf( + "Expected error for %s but got no error", + tc.value, + ) + } else if !tc.isError && nil != err { + t.Errorf( + "Expected no error for %s but got %v", + tc.value, err, + ) + } + }) + } +} diff --git a/handle/handle.go b/handle/handle.go index e869e3b..e347b55 100644 --- a/handle/handle.go +++ b/handle/handle.go @@ -1,6 +1,7 @@ package handle import ( + "crypto/tls" "fmt" "log" "net/http" @@ -10,14 +11,46 @@ import ( var ( // These assignments are for unit testing. listenAndServe = http.ListenAndServe - listenAndServeTLS = http.ListenAndServeTLS + listenAndServeTLS = defaultListenAndServeTLS setHandler = http.HandleFunc ) var ( - server http.Server + // Server options to be set prior to calling the listening function. + // minTLSVersion is the minimum allowed TLS version to be used by the + // server. + minTLSVersion uint16 = tls.VersionTLS10 ) +// defaultListenAndServeTLS is the default implementation of the listening +// function for serving with TLS enabled. This is, effectively, a copy from +// the standard library but with the ability to set the minimum TLS version. +func defaultListenAndServeTLS( + binding, certFile, keyFile string, handler http.Handler, +) error { + if handler == nil { + handler = http.DefaultServeMux + } + server := &http.Server{ + Addr: binding, + Handler: handler, + TLSConfig: &tls.Config{ + MinVersion: minTLSVersion, + }, + } + return server.ListenAndServeTLS(certFile, keyFile) +} + +// SetMinimumTLSVersion to be used by the server. +func SetMinimumTLSVersion(version uint16) { + if version < tls.VersionTLS10 { + version = tls.VersionTLS10 + } else if version > tls.VersionTLS13 { + version = tls.VersionTLS13 + } + minTLSVersion = version +} + // ListenerFunc accepts the {hostname:port} binding string required by HTTP // listeners and the handler (router) function and returns any errors that // occur. @@ -50,7 +83,7 @@ func WithReferrers(serveFile FileServerFunc, referrers []string) FileServerFunc func WithLogging(serveFile FileServerFunc) FileServerFunc { return func(w http.ResponseWriter, r *http.Request, name string) { referer := r.Referer() - if 0 == len(referer) { + if len(referer) == 0 { log.Printf( "REQ from '%s': %s %s %s%s -> %s\n", r.RemoteAddr, @@ -139,7 +172,7 @@ func TLSListening(tlsCert, tlsKey string) ListenerFunc { // passed list of referrers. func validReferrer(s []string, e string) bool { // Whitelisted referer list is empty. All requests are allowed. - if 0 == len(s) { + if len(s) == 0 { return true } diff --git a/handle/handle_test.go b/handle/handle_test.go index c165300..a557c6f 100644 --- a/handle/handle_test.go +++ b/handle/handle_test.go @@ -1,6 +1,7 @@ package handle import ( + "crypto/tls" "errors" "io/ioutil" "log" @@ -84,6 +85,28 @@ func teardown() (err error) { return os.RemoveAll("tmp") } +func TestSetMinimumTLSVersion(t *testing.T) { + testCases := []struct { + name string + value uint16 + expected uint16 + }{ + {"Too low", tls.VersionTLS10 - 1, tls.VersionTLS10}, + {"Lower bounds", tls.VersionTLS10, tls.VersionTLS10}, + {"Upper bounds", tls.VersionTLS13, tls.VersionTLS13}, + {"Too high", tls.VersionTLS13 + 1, tls.VersionTLS13}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + SetMinimumTLSVersion(tc.value) + if tc.expected != minTLSVersion { + t.Errorf("Expected %d but got %d", tc.expected, minTLSVersion) + } + }) + } +} + func TestWithReferrers(t *testing.T) { forbidden := http.StatusForbidden From 81e8ebadd3fb62d96a8c020790b845d69a5c33bf Mon Sep 17 00:00:00 2001 From: Jeromy Streets Date: Mon, 11 Oct 2021 14:04:53 -0700 Subject: [PATCH 2/4] Working around HTML limits in GitHub markdown. --- README.md | 7 +-- img/sponsor.svg | 147 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 img/sponsor.svg diff --git a/README.md b/README.md index d33df3c..f373092 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ # static-file-server - - -Buy me a Smoothie + + ## Introduction diff --git a/img/sponsor.svg b/img/sponsor.svg new file mode 100644 index 0000000..a10f598 --- /dev/null +++ b/img/sponsor.svg @@ -0,0 +1,147 @@ + + From 562f95e8ea40a1590d4d9f479774abecaf688898 Mon Sep 17 00:00:00 2001 From: Jeromy Streets Date: Fri, 3 Dec 2021 10:20:31 -0800 Subject: [PATCH 3/4] Fixed issue where some values may not be getting assigned or validated if a config file was not being used. --- config/config.go | 15 ++++++++++++++- config/config_test.go | 2 ++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index 9c760d9..9b8ef00 100644 --- a/config/config.go +++ b/config/config.go @@ -83,7 +83,7 @@ func Load(filename string) (err error) { // If no filename provided, assign envvars. if filename == "" { overrideWithEnvVars() - return + return validate() } // Read contents from configuration file. @@ -161,6 +161,19 @@ func validate() error { return err } } + + // For logging minimum TLS version being used while debugging, backfill + // the TLSMinVersStr field. + switch Get.TLSMinVers { + case tls.VersionTLS10: + Get.TLSMinVersStr = "TLS1.0" + case tls.VersionTLS11: + Get.TLSMinVersStr = "TLS1.1" + case tls.VersionTLS12: + Get.TLSMinVersStr = "TLS1.2" + case tls.VersionTLS13: + Get.TLSMinVersStr = "TLS1.3" + } } else { if 0 < len(Get.TLSMinVersStr) { msg := "value for 'TLS_MIN_VERS' is set but 'TLS_CERT' and 'TLS_KEY' are not" diff --git a/config/config_test.go b/config/config_test.go index 3199fa6..4420411 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -178,6 +178,8 @@ func TestValidate(t *testing.T) { {"Prefix missing leading /", empty, empty, "my/prefix", "", true}, {"Prefix with trailing /", empty, empty, "/my/prefix/", "", true}, {"Valid paths w/min ok TLS", validPath, validPath, prefix, "tls11", false}, + {"Valid paths w/min ok TLS", validPath, validPath, prefix, "tls12", false}, + {"Valid paths w/min ok TLS", validPath, validPath, prefix, "tls13", false}, {"Valid paths w/min bad TLS", validPath, validPath, prefix, "bad", true}, {"Empty paths w/min ok TLS", empty, empty, prefix, "tls11", true}, {"Empty paths w/min bad TLS", empty, empty, prefix, "bad", true}, From 7586936f78be7bea5f0bf3964d7daa9a008b2e24 Mon Sep 17 00:00:00 2001 From: Jeromy Streets Date: Wed, 15 Dec 2021 15:15:20 -0800 Subject: [PATCH 4/4] Updated Go compiler version. --- Dockerfile | 2 +- Dockerfile.all | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index e799f5c..de7de74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ################################################################################ ## GO BUILDER ################################################################################ -FROM golang:1.17.2 as builder +FROM golang:1.17.5 as builder ENV VERSION 1.8.5 ENV BUILD_DIR /build diff --git a/Dockerfile.all b/Dockerfile.all index f7a9bd8..2d80b3e 100644 --- a/Dockerfile.all +++ b/Dockerfile.all @@ -1,4 +1,4 @@ -FROM golang:1.17.2 as builder +FROM golang:1.17.5 as builder ENV VERSION 1.8.5 ENV BUILD_DIR /build