Added ability to set minimum TLS version. Updated compiler version. Minor code clean-up. Added funding info.

This commit is contained in:
Jeromy Streets 2021-10-11 13:31:25 -07:00
parent 7d738d7188
commit ea0bcf28bb
10 changed files with 212 additions and 42 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: halverneus

View File

@ -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"

View File

@ -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"

View File

@ -1,5 +1,12 @@
# static-file-server
<a href="https://github.com/sponsors/halverneus" style="background-color:#fff;color:#000;padding:3px 12px;font-size:12px;border-color:#000;border:1px solid;border-radius:6px;box-sizing:border-box;line-height:20px;display:inline-block;">
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" style="vertical-align: middle; margin-right:4px;color:#f00;">
<path fill-rule="evenodd" style="color:#f00;fill: currentColor;" d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"></path>
</svg>
<span style="color:#000">Buy me a Smoothie</span>
</a>
## 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: ""
```

View File

@ -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

View File

@ -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,

View File

@ -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
}

View File

@ -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,
)
}
})
}
}

View File

@ -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
}

View File

@ -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