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..de7de74 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,9 @@
################################################################################
## GO BUILDER
################################################################################
-FROM golang:1.16.5 as builder
+FROM golang:1.17.5 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..2d80b3e 100644
--- a/Dockerfile.all
+++ b/Dockerfile.all
@@ -1,6 +1,6 @@
-FROM golang:1.16.5 as builder
+FROM golang:1.17.5 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..f373092 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# static-file-server
+
+
+
+
## Introduction
Tiny, simple static file server using environment variables for configuration.
@@ -47,6 +51,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 +83,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..9b8ef00 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,9 +81,9 @@ func setDefaults() {
// Load the configuration file.
func Load(filename string) (err error) {
// If no filename provided, assign envvars.
- if "" == filename {
+ if filename == "" {
overrideWithEnvVars()
- return
+ return validate()
}
// Read contents from configuration file.
@@ -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,43 @@ 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
+ }
+ }
+
+ // 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"
+ return errors.New(msg)
}
}
@@ -154,7 +195,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 +204,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 +215,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 +239,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 +266,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..4420411 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,35 @@ 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 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},
}
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 +470,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
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 @@
+
+