├── .github ├── FUNDING.yml └── dependabot.yml ├── .dockerignore ├── .gitignore ├── go.mod ├── cli ├── help │ ├── help_test.go │ └── help.go ├── version │ ├── version_test.go │ └── version.go ├── args.go ├── execute.go ├── server │ ├── server.go │ └── server_test.go ├── args_test.go └── execute_test.go ├── bin └── serve │ └── main.go ├── go.sum ├── LICENSE ├── update.sh ├── Dockerfile ├── Dockerfile.all ├── README.md ├── handle ├── handle.go └── handle_test.go ├── config ├── config.go └── config_test.go └── img └── sponsor.svg /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: halverneus 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | Dockerfile 4 | Dockerfile.all 5 | LICENSE 6 | README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | 3 | # Ignore the binary output directory (result of running update.sh). 4 | /out/ 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/halverneus/static-file-server 2 | 3 | go 1.18 4 | 5 | require gopkg.in/yaml.v3 v3.0.1 6 | -------------------------------------------------------------------------------- /cli/help/help_test.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | import "testing" 4 | 5 | func TestRun(t *testing.T) { 6 | if err := Run(); nil != err { 7 | t.Errorf("While running help got %v", err) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /cli/version/version_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "testing" 4 | 5 | func TestVersion(t *testing.T) { 6 | if err := Run(); nil != err { 7 | t.Errorf("While running version got %v", err) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /bin/serve/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/halverneus/static-file-server/cli" 7 | ) 8 | 9 | func main() { 10 | if err := cli.Execute(); nil != err { 11 | log.Fatalf("Error: %v\n", err) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 2 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 3 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 4 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 5 | -------------------------------------------------------------------------------- /cli/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | // Run print operation. 9 | func Run() error { 10 | fmt.Printf("%s\n%s\n", VersionText, GoVersionText) 11 | return nil 12 | } 13 | 14 | var ( 15 | // version is the application version set during build. 16 | version string 17 | 18 | // VersionText for directly accessing the static-file-server version. 19 | VersionText = fmt.Sprintf("v%s", version) 20 | 21 | // GoVersionText for directly accessing the version of the Go runtime 22 | // compiled with the static-file-server. 23 | GoVersionText = runtime.Version() 24 | ) 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "gomod" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | - package-ecosystem: "docker" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "daily" 21 | 22 | -------------------------------------------------------------------------------- /cli/args.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | // Args parsed from the command-line. 4 | type Args []string 5 | 6 | // Parse command-line arguments into Args. Value is returned to support daisy 7 | // chaining. 8 | func Parse(values []string) Args { 9 | args := Args(values) 10 | return args 11 | } 12 | 13 | // Matches is used to determine if the arguments match the provided pattern. 14 | func (args Args) Matches(pattern ...string) bool { 15 | // If lengths don't match then nothing does. 16 | if len(pattern) != len(args) { 17 | return false 18 | } 19 | 20 | // Compare slices using '*' as a wildcard. 21 | for index, value := range pattern { 22 | if "*" != value && value != args[index] { 23 | return false 24 | } 25 | } 26 | return true 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jeromy Streets 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ $# -eq 0 ]; then 6 | echo "Usage: ./update.sh v#.#.#" 7 | exit 8 | fi 9 | 10 | VERSION=$1 11 | 12 | docker build -t sfs-builder -f ./Dockerfile.all . 13 | 14 | ID=$(docker create sfs-builder) 15 | 16 | rm -rf out 17 | mkdir -p out 18 | docker cp "${ID}:/build/pkg/linux-amd64/serve" "./out/static-file-server-${VERSION}-linux-amd64" 19 | docker cp "${ID}:/build/pkg/linux-i386/serve" "./out/static-file-server-${VERSION}-linux-386" 20 | docker cp "${ID}:/build/pkg/linux-arm6/serve" "./out/static-file-server-${VERSION}-linux-arm6" 21 | docker cp "${ID}:/build/pkg/linux-arm7/serve" "./out/static-file-server-${VERSION}-linux-arm7" 22 | docker cp "${ID}:/build/pkg/linux-arm64/serve" "./out/static-file-server-${VERSION}-linux-arm64" 23 | docker cp "${ID}:/build/pkg/darwin-amd64/serve" "./out/static-file-server-${VERSION}-darwin-amd64" 24 | docker cp "${ID}:/build/pkg/win-amd64/serve.exe" "./out/static-file-server-${VERSION}-windows-amd64.exe" 25 | 26 | docker rm -f "${ID}" 27 | docker rmi sfs-builder 28 | 29 | docker buildx build --push --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag "halverneus/static-file-server:${VERSION}" . 30 | docker buildx build --push --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag halverneus/static-file-server:latest . 31 | 32 | echo "Done" 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ## GO BUILDER 3 | ################################################################################ 4 | FROM golang:1.22.4 as builder 5 | 6 | ENV VERSION 1.8.11 7 | ENV CGO_ENABLED 0 8 | ENV BUILD_DIR /build 9 | 10 | RUN mkdir -p ${BUILD_DIR} 11 | WORKDIR ${BUILD_DIR} 12 | 13 | COPY go.* ./ 14 | RUN go mod download 15 | COPY . . 16 | 17 | RUN go test -cover ./... 18 | RUN go build -a -tags netgo -installsuffix netgo -ldflags "-s -w -X github.com/halverneus/static-file-server/cli/version.version=${VERSION}" -o /serve /build/bin/serve 19 | 20 | RUN adduser --system --no-create-home --uid 1000 --shell /usr/sbin/nologin static 21 | 22 | ################################################################################ 23 | ## DEPLOYMENT CONTAINER 24 | ################################################################################ 25 | FROM scratch 26 | 27 | EXPOSE 8080 28 | COPY --from=builder /serve / 29 | COPY --from=builder /etc/passwd /etc/passwd 30 | 31 | USER static 32 | ENTRYPOINT ["/serve"] 33 | CMD [] 34 | 35 | # Metadata 36 | LABEL life.apets.vendor="Halverneus" \ 37 | life.apets.url="https://github.com/halverneus/static-file-server" \ 38 | life.apets.name="Static File Server" \ 39 | life.apets.description="A tiny static file server" \ 40 | life.apets.version="v1.8.11" \ 41 | life.apets.schema-version="1.0" 42 | -------------------------------------------------------------------------------- /Dockerfile.all: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.4 as builder 2 | 3 | ENV VERSION 1.8.11 4 | ENV BUILD_DIR /build 5 | ENV CGO_ENABLED 0 6 | 7 | RUN mkdir -p ${BUILD_DIR} 8 | WORKDIR ${BUILD_DIR} 9 | 10 | COPY . . 11 | RUN go test -cover ./... 12 | RUN GOOS=linux GOARCH=amd64 go build -a -tags netgo -installsuffix netgo -ldflags "-s -w -X github.com/halverneus/static-file-server/cli/version.version=${VERSION}" -o pkg/linux-amd64/serve /build/bin/serve 13 | RUN GOOS=linux GOARCH=386 go build -a -tags netgo -installsuffix netgo -ldflags "-s -w -X github.com/halverneus/static-file-server/cli/version.version=${VERSION}" -o pkg/linux-i386/serve /build/bin/serve 14 | RUN GOOS=linux GOARCH=arm GOARM=6 go build -a -tags netgo -installsuffix netgo -ldflags "-s -w -X github.com/halverneus/static-file-server/cli/version.version=${VERSION}" -o pkg/linux-arm6/serve /build/bin/serve 15 | RUN GOOS=linux GOARCH=arm GOARM=7 go build -a -tags netgo -installsuffix netgo -ldflags "-s -w -X github.com/halverneus/static-file-server/cli/version.version=${VERSION}" -o pkg/linux-arm7/serve /build/bin/serve 16 | RUN GOOS=linux GOARCH=arm64 go build -a -tags netgo -installsuffix netgo -ldflags "-s -w -X github.com/halverneus/static-file-server/cli/version.version=${VERSION}" -o pkg/linux-arm64/serve /build/bin/serve 17 | RUN GOOS=darwin GOARCH=amd64 go build -a -tags netgo -installsuffix netgo -ldflags "-s -w -X github.com/halverneus/static-file-server/cli/version.version=${VERSION}" -o pkg/darwin-amd64/serve /build/bin/serve 18 | RUN GOOS=windows GOARCH=amd64 go build -a -tags netgo -installsuffix netgo -ldflags "-s -w -X github.com/halverneus/static-file-server/cli/version.version=${VERSION}" -o pkg/win-amd64/serve.exe /build/bin/serve 19 | 20 | # Metadata 21 | LABEL life.apets.vendor="Halverneus" \ 22 | life.apets.url="https://github.com/halverneus/static-file-server" \ 23 | life.apets.name="Static File Server" \ 24 | life.apets.description="A tiny static file server" \ 25 | life.apets.version="v1.8.11" \ 26 | life.apets.schema-version="1.0" 27 | -------------------------------------------------------------------------------- /cli/execute.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/halverneus/static-file-server/cli/help" 8 | "github.com/halverneus/static-file-server/cli/server" 9 | "github.com/halverneus/static-file-server/cli/version" 10 | "github.com/halverneus/static-file-server/config" 11 | ) 12 | 13 | var ( 14 | option struct { 15 | configFile string 16 | helpFlag bool 17 | versionFlag bool 18 | } 19 | ) 20 | 21 | // Assignments used to simplify testing. 22 | var ( 23 | selectRoutine = selectionRoutine 24 | unknownArgsFunc = unknownArgs 25 | runServerFunc = server.Run 26 | runHelpFunc = help.Run 27 | runVersionFunc = version.Run 28 | loadConfig = config.Load 29 | ) 30 | 31 | func init() { 32 | setupFlags() 33 | } 34 | 35 | func setupFlags() { 36 | flag.StringVar(&option.configFile, "config", "", "") 37 | flag.StringVar(&option.configFile, "c", "", "") 38 | flag.BoolVar(&option.helpFlag, "help", false, "") 39 | flag.BoolVar(&option.helpFlag, "h", false, "") 40 | flag.BoolVar(&option.versionFlag, "version", false, "") 41 | flag.BoolVar(&option.versionFlag, "v", false, "") 42 | } 43 | 44 | // Execute CLI arguments. 45 | func Execute() (err error) { 46 | // Parse flag options, then parse commands arguments. 47 | flag.Parse() 48 | args := Parse(flag.Args()) 49 | 50 | job := selectRoutine(args) 51 | return job() 52 | } 53 | 54 | func selectionRoutine(args Args) func() error { 55 | switch { 56 | 57 | // serve help 58 | // serve --help 59 | // serve -h 60 | case args.Matches("help") || option.helpFlag: 61 | return runHelpFunc 62 | 63 | // serve version 64 | // serve --version 65 | // serve -v 66 | case args.Matches("version") || option.versionFlag: 67 | return runVersionFunc 68 | 69 | // serve 70 | case args.Matches(): 71 | return withConfig(runServerFunc) 72 | 73 | // Unknown arguments. 74 | default: 75 | return unknownArgsFunc(args) 76 | } 77 | } 78 | 79 | func unknownArgs(args Args) func() error { 80 | return func() error { 81 | return fmt.Errorf( 82 | "unknown arguments provided [%v], try: 'help'", 83 | args, 84 | ) 85 | } 86 | } 87 | 88 | func withConfig(routine func() error) func() error { 89 | return func() (err error) { 90 | if err = loadConfig(option.configFile); nil != err { 91 | return 92 | } 93 | return routine() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /cli/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/halverneus/static-file-server/config" 8 | "github.com/halverneus/static-file-server/handle" 9 | ) 10 | 11 | var ( 12 | // Values to be overridden to simplify unit testing. 13 | selectHandler = handlerSelector 14 | selectListener = listenerSelector 15 | ) 16 | 17 | // Run server. 18 | func Run() error { 19 | if config.Get.Debug { 20 | config.Log() 21 | } 22 | // Choose and set the appropriate, optimized static file serving function. 23 | handler := selectHandler() 24 | 25 | // Serve files over HTTP or HTTPS based on paths to TLS files being 26 | // provided. 27 | listener := selectListener() 28 | 29 | binding := fmt.Sprintf("%s:%d", config.Get.Host, config.Get.Port) 30 | return listener(binding, handler) 31 | } 32 | 33 | // handlerSelector returns the appropriate request handler based on 34 | // configuration. 35 | func handlerSelector() (handler http.HandlerFunc) { 36 | var serveFileHandler handle.FileServerFunc 37 | 38 | serveFileHandler = http.ServeFile 39 | if config.Get.Debug { 40 | serveFileHandler = handle.WithLogging(serveFileHandler) 41 | } 42 | 43 | if 0 != len(config.Get.Referrers) { 44 | serveFileHandler = handle.WithReferrers( 45 | serveFileHandler, config.Get.Referrers, 46 | ) 47 | } 48 | 49 | // Choose and set the appropriate, optimized static file serving function. 50 | if 0 == len(config.Get.URLPrefix) { 51 | handler = handle.Basic(serveFileHandler, config.Get.Folder) 52 | } else { 53 | handler = handle.Prefix( 54 | serveFileHandler, 55 | config.Get.Folder, 56 | config.Get.URLPrefix, 57 | ) 58 | } 59 | 60 | // Determine whether index files should hidden. 61 | if !config.Get.ShowListing { 62 | if config.Get.AllowIndex { 63 | handler = handle.PreventListings(handler, config.Get.Folder, config.Get.URLPrefix) 64 | } else { 65 | handler = handle.IgnoreIndex(handler) 66 | } 67 | } 68 | // If configured, apply wildcard CORS support. 69 | if config.Get.Cors { 70 | handler = handle.AddCorsWildcardHeaders(handler) 71 | } 72 | 73 | // If configured, apply key code access control. 74 | if "" != config.Get.AccessKey { 75 | handler = handle.AddAccessKey(handler, config.Get.AccessKey) 76 | } 77 | 78 | return 79 | } 80 | 81 | // listenerSelector returns the appropriate listener handler based on 82 | // configuration. 83 | func listenerSelector() (listener handle.ListenerFunc) { 84 | // Serve files over HTTP or HTTPS based on paths to TLS files being 85 | // provided. 86 | if 0 < len(config.Get.TLSCert) { 87 | handle.SetMinimumTLSVersion(config.Get.TLSMinVers) 88 | listener = handle.TLSListening( 89 | config.Get.TLSCert, 90 | config.Get.TLSKey, 91 | ) 92 | } else { 93 | listener = handle.Listening() 94 | } 95 | return 96 | } 97 | -------------------------------------------------------------------------------- /cli/args_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParse(t *testing.T) { 8 | matches := func(args Args, orig []string) bool { 9 | if nil == orig { 10 | return nil == args 11 | } 12 | if len(orig) != len(args) { 13 | return false 14 | } 15 | for index, value := range args { 16 | if orig[index] != value { 17 | return false 18 | } 19 | } 20 | return true 21 | } 22 | 23 | testCases := []struct { 24 | name string 25 | value []string 26 | }{ 27 | {"Nil arguments", nil}, 28 | {"No arguments", []string{}}, 29 | {"Arguments", []string{"first", "second", "*"}}, 30 | } 31 | 32 | for _, tc := range testCases { 33 | t.Run(tc.name, func(t *testing.T) { 34 | if args := Parse(tc.value); !matches(args, tc.value) { 35 | t.Errorf("Expected [%v] but got [%v]", tc.value, args) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestMatches(t *testing.T) { 42 | testCases := []struct { 43 | name string 44 | value []string 45 | pattern []string 46 | result bool 47 | }{ 48 | {"Nil args and nil pattern", nil, nil, true}, 49 | {"No args and nil pattern", []string{}, nil, true}, 50 | {"Nil args and no pattern", nil, []string{}, true}, 51 | {"No args and no pattern", []string{}, []string{}, true}, 52 | {"Nil args and pattern", nil, []string{"test"}, false}, 53 | {"No args and pattern", []string{}, []string{"test"}, false}, 54 | {"Args and nil pattern", []string{"test"}, nil, false}, 55 | {"Args and no pattern", []string{"test"}, []string{}, false}, 56 | {"Simple single compare", []string{"test"}, []string{"test"}, true}, 57 | {"Simple double compare", []string{"one", "two"}, []string{"one", "two"}, true}, 58 | {"Bad single", []string{"one"}, []string{"two"}, false}, 59 | {"Bad double", []string{"one", "two"}, []string{"one", "owt"}, false}, 60 | {"Count mismatch", []string{"one", "two"}, []string{"one"}, false}, 61 | {"Nil args and wild", nil, []string{"*"}, false}, 62 | {"No args and wild", []string{}, []string{"*"}, false}, 63 | {"Single arg and wild", []string{"one"}, []string{"*"}, true}, 64 | {"Double arg and first wild", []string{"one", "two"}, []string{"*", "two"}, true}, 65 | {"Double arg and second wild", []string{"one", "two"}, []string{"one", "*"}, true}, 66 | {"Double arg and first wild mismatched", []string{"one", "two"}, []string{"*", "owt"}, false}, 67 | {"Double arg and second wild mismatched", []string{"one", "two"}, []string{"eno", "*"}, false}, 68 | {"Double arg and double wild", []string{"one", "two"}, []string{"*", "*"}, true}, 69 | } 70 | 71 | for _, tc := range testCases { 72 | t.Run(tc.name, func(t *testing.T) { 73 | args := Parse(tc.value) 74 | if resp := args.Matches(tc.pattern...); tc.result != resp { 75 | msg := "For arguments [%v] matched to pattern [%v] expected " + 76 | "%b but got %b" 77 | t.Errorf(msg, tc.value, tc.pattern, tc.result, resp) 78 | } 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # static-file-server 2 | 3 | 4 | 5 | 6 | 7 | ## Introduction 8 | 9 | Tiny, simple static file server using environment variables for configuration. 10 | Install from any of the following locations: 11 | 12 | - Docker Hub: https://hub.docker.com/r/halverneus/static-file-server/ 13 | - GitHub: https://github.com/halverneus/static-file-server 14 | 15 | ## Configuration 16 | 17 | ### Environment Variables 18 | 19 | Default values are shown with the associated environment variable. 20 | 21 | ```bash 22 | # Enables resource access from any domain. 23 | CORS=false 24 | 25 | # Enable debugging for troubleshooting. If set to 'true' this prints extra 26 | # information during execution. IMPORTANT NOTE: The configuration summary is 27 | # printed to stdout while logs generated during execution are printed to stderr. 28 | DEBUG=false 29 | 30 | # Optional Hostname for binding. Leave unset to accept any incoming HTTP request 31 | # on the prescribed port. 32 | HOST= 33 | 34 | # If assigned, must be a valid port number. 35 | PORT=8080 36 | 37 | # When set to 'true' the index.html file in the folder will be served. And 38 | # the file list will not be served. 39 | ALLOW_INDEX=true 40 | 41 | # Automatically serve the index of file list for a given directory (default). 42 | SHOW_LISTING=true 43 | 44 | # Folder with the content to serve. 45 | FOLDER=/web 46 | 47 | # URL path prefix. If 'my.file' is in the root of $FOLDER and $URL_PREFIX is 48 | # '/my/place' then file is retrieved with 'http://$HOST:$PORT/my/place/my.file'. 49 | URL_PREFIX= 50 | 51 | # Paths to the TLS certificate and key. If one is set then both must be set. If 52 | # both set then files are served using HTTPS. If neither are set then files are 53 | # served using HTTP. 54 | TLS_CERT= 55 | TLS_KEY= 56 | 57 | # If TLS certificates are set then the minimum TLS version may also be set. If 58 | # the value isn't set then the default minimum TLS version is 1.0. Allowed 59 | # values include "TLS10", "TLS11", "TLS12" and "TLS13" for TLS1.0, TLS1.1, 60 | # TLS1.2 and TLS1.3, respectively. The value is not case-sensitive. 61 | TLS_MIN_VERS= 62 | 63 | # List of accepted HTTP referrers. Return 403 if HTTP header `Referer` does not 64 | # match prefixes provided in the list. 65 | # Examples: 66 | # 'REFERRERS=http://localhost,https://...,https://another.name' 67 | # To accept missing referrer header, add a blank entry (start comma): 68 | # 'REFERRERS=,http://localhost,https://another.name' 69 | REFERRERS= 70 | 71 | # Use key / code parameter in the request URL for access control. The code is 72 | # computed by requested PATH and your key. 73 | # Example: 74 | # ACCESS_KEY=username 75 | # To access your file, either access: 76 | # http://$HOST:$PORT/my/place/my.file?key=username 77 | # or access (md5sum of "/my/place/my.fileusername"): 78 | # http://$HOST:$PORT/my/place/my.file?code=44356A355E89D9EE7B2D5687E48024B0 79 | ACCESS_KEY= 80 | ``` 81 | 82 | ### YAML Configuration File 83 | 84 | YAML settings are individually overridden by the corresponding environment 85 | variable. The following is an example configuration file with defaults. Pass in 86 | the path to the configuration file using the command line option 87 | ('-c', '-config', '--config'). 88 | 89 | ```yaml 90 | cors: false 91 | debug: false 92 | folder: /web 93 | host: "" 94 | port: 8080 95 | referrers: [] 96 | show-listing: true 97 | tls-cert: "" 98 | tls-key: "" 99 | tls-min-vers: "" 100 | url-prefix: "" 101 | access-key: "" 102 | ``` 103 | 104 | Example configuration with possible alternative values: 105 | 106 | ```yaml 107 | debug: true 108 | folder: /var/www 109 | port: 80 110 | referrers: 111 | - http://localhost 112 | - https://mydomain.com 113 | ``` 114 | 115 | ## Deployment 116 | 117 | ### Without Docker 118 | 119 | ```bash 120 | PORT=8888 FOLDER=. ./serve 121 | ``` 122 | 123 | Files can then be accessed by going to http://localhost:8888/my/file.txt 124 | 125 | ### With Docker 126 | 127 | ```bash 128 | docker run -d \ 129 | -v /my/folder:/web \ 130 | -p 8080:8080 \ 131 | halverneus/static-file-server:latest 132 | ``` 133 | 134 | This will serve the folder "/my/folder" over http://localhost:8080/my/file.txt 135 | 136 | Any of the variables can also be modified: 137 | 138 | ```bash 139 | docker run -d \ 140 | -v /home/me/dev/source:/content/html \ 141 | -v /home/me/dev/files:/content/more/files \ 142 | -e FOLDER=/content \ 143 | -p 8080:8080 \ 144 | halverneus/static-file-server:latest 145 | ``` 146 | 147 | ### Getting Help 148 | 149 | ```bash 150 | ./serve help 151 | # OR 152 | docker run -it halverneus/static-file-server:latest help 153 | ``` 154 | -------------------------------------------------------------------------------- /cli/execute_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestSetupFlags(t *testing.T) { 11 | app := os.Args[0] 12 | 13 | file := "file.txt" 14 | wConfig := "Config (file.txt)" 15 | 16 | testCases := []struct { 17 | name string 18 | args []string 19 | config string 20 | help bool 21 | version bool 22 | }{ 23 | {"Empty args", []string{app}, "", false, false}, 24 | {"Help (--help)", []string{app, "--help"}, "", true, false}, 25 | {"Help (-help)", []string{app, "-help"}, "", true, false}, 26 | {"Help (-h)", []string{app, "-h"}, "", true, false}, 27 | {"Version (--version)", []string{app, "--version"}, "", false, true}, 28 | {"Version (-version)", []string{app, "-version"}, "", false, true}, 29 | {"Version (-v)", []string{app, "-v"}, "", false, true}, 30 | {"Config ()", []string{app, "--config", ""}, "", false, false}, 31 | {wConfig, []string{app, "--config", file}, file, false, false}, 32 | {wConfig, []string{app, "--config=file.txt"}, file, false, false}, 33 | {wConfig, []string{app, "-config", file}, file, false, false}, 34 | {wConfig, []string{app, "-config=file.txt"}, file, false, false}, 35 | {wConfig, []string{app, "-c", file}, file, false, false}, 36 | {"All set", []string{app, "-h", "-v", "-c", file}, file, true, true}, 37 | } 38 | 39 | reset := func() { 40 | option.configFile = "" 41 | option.helpFlag = false 42 | option.versionFlag = false 43 | } 44 | 45 | for _, tc := range testCases { 46 | t.Run(tc.name, func(t *testing.T) { 47 | reset() 48 | os.Args = tc.args 49 | flag.Parse() 50 | 51 | if option.configFile != tc.config { 52 | t.Errorf( 53 | "For options [%v] expected a config file of %s but got %s", 54 | tc.args, tc.config, option.configFile, 55 | ) 56 | } 57 | if option.helpFlag != tc.help { 58 | t.Errorf( 59 | "For options [%v] expected help flag of %t but got %t", 60 | tc.args, tc.help, option.helpFlag, 61 | ) 62 | } 63 | if option.versionFlag != tc.version { 64 | t.Errorf( 65 | "For options [%v] expected version flag of %t but got %t", 66 | tc.args, tc.version, option.versionFlag, 67 | ) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestExecuteAndSelection(t *testing.T) { 74 | app := os.Args[0] 75 | 76 | runHelpFuncError := errors.New("help") 77 | runHelpFunc = func() error { 78 | return runHelpFuncError 79 | } 80 | runVersionFuncError := errors.New("version") 81 | runVersionFunc = func() error { 82 | return runVersionFuncError 83 | } 84 | runServerFuncError := errors.New("server") 85 | runServerFunc = func() error { 86 | return runServerFuncError 87 | } 88 | unknownArgsFuncError := errors.New("unknown") 89 | unknownArgsFunc = func(Args) func() error { 90 | return func() error { 91 | return unknownArgsFuncError 92 | } 93 | } 94 | 95 | reset := func() { 96 | option.configFile = "" 97 | option.helpFlag = false 98 | option.versionFlag = false 99 | } 100 | 101 | testCases := []struct { 102 | name string 103 | args []string 104 | result error 105 | }{ 106 | {"Help", []string{app, "help"}, runHelpFuncError}, 107 | {"Help", []string{app, "--help"}, runHelpFuncError}, 108 | {"Version", []string{app, "version"}, runVersionFuncError}, 109 | {"Version", []string{app, "--version"}, runVersionFuncError}, 110 | {"Serve", []string{app}, runServerFuncError}, 111 | {"Unknown", []string{app, "unknown"}, unknownArgsFuncError}, 112 | } 113 | 114 | for _, tc := range testCases { 115 | t.Run(tc.name, func(t *testing.T) { 116 | reset() 117 | os.Args = tc.args 118 | 119 | if err := Execute(); tc.result != err { 120 | t.Errorf( 121 | "Expected error for %v but got %v", 122 | tc.result, err, 123 | ) 124 | } 125 | }) 126 | } 127 | } 128 | 129 | func TestUnknownArgs(t *testing.T) { 130 | errFunc := unknownArgs(Args{"unknown"}) 131 | if err := errFunc(); nil == err { 132 | t.Errorf( 133 | "Expected a given unknown argument error but got %v", 134 | err, 135 | ) 136 | } 137 | } 138 | 139 | func TestWithConfig(t *testing.T) { 140 | configError := errors.New("config") 141 | routineError := errors.New("routine") 142 | routine := func() error { return routineError } 143 | 144 | testCases := []struct { 145 | name string 146 | loadConfig func(string) error 147 | result error 148 | }{ 149 | {"Config error", func(string) error { return configError }, configError}, 150 | {"Routine error", func(string) error { return nil }, routineError}, 151 | } 152 | 153 | for _, tc := range testCases { 154 | t.Run(tc.name, func(t *testing.T) { 155 | loadConfig = tc.loadConfig 156 | errFunc := withConfig(routine) 157 | if err := errFunc(); tc.result != err { 158 | t.Errorf("Expected error %v but got %v", tc.result, err) 159 | } 160 | }) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /cli/server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/halverneus/static-file-server/config" 9 | "github.com/halverneus/static-file-server/handle" 10 | ) 11 | 12 | func TestRun(t *testing.T) { 13 | listenerError := errors.New("listener") 14 | selectListener = func() handle.ListenerFunc { 15 | return func(string, http.HandlerFunc) error { 16 | return listenerError 17 | } 18 | } 19 | 20 | config.Get.Debug = false 21 | if err := Run(); listenerError != err { 22 | t.Errorf("Without debug expected %v but got %v", listenerError, err) 23 | } 24 | 25 | config.Get.Debug = true 26 | if err := Run(); listenerError != err { 27 | t.Errorf("With debug expected %v but got %v", listenerError, err) 28 | } 29 | } 30 | 31 | func TestHandlerSelector(t *testing.T) { 32 | // This test only exercises function branches. 33 | testFolder := "/web" 34 | testPrefix := "/url/prefix" 35 | var ignoreReferrer []string 36 | testReferrer := []string{"http://localhost"} 37 | testAccessKey := "access-key" 38 | 39 | testCases := []struct { 40 | name string 41 | folder string 42 | prefix string 43 | listing bool 44 | debug bool 45 | refer []string 46 | cors bool 47 | accessKey string 48 | }{ 49 | {"Basic handler w/o debug", testFolder, "", true, false, ignoreReferrer, false, ""}, 50 | {"Prefix handler w/o debug", testFolder, testPrefix, true, false, ignoreReferrer, false, ""}, 51 | {"Basic and hide listing handler w/o debug", testFolder, "", false, false, ignoreReferrer, false, ""}, 52 | {"Prefix and hide listing handler w/o debug", testFolder, testPrefix, false, false, ignoreReferrer, false, ""}, 53 | {"Basic handler w/debug", testFolder, "", true, true, ignoreReferrer, false, ""}, 54 | {"Prefix handler w/debug", testFolder, testPrefix, true, true, ignoreReferrer, false, ""}, 55 | {"Basic and hide listing handler w/debug", testFolder, "", false, true, ignoreReferrer, false, ""}, 56 | {"Prefix and hide listing handler w/debug", testFolder, testPrefix, false, true, ignoreReferrer, false, ""}, 57 | {"Basic handler w/o debug w/refer", testFolder, "", true, false, testReferrer, false, ""}, 58 | {"Prefix handler w/o debug w/refer", testFolder, testPrefix, true, false, testReferrer, false, ""}, 59 | {"Basic and hide listing handler w/o debug w/refer", testFolder, "", false, false, testReferrer, false, ""}, 60 | {"Prefix and hide listing handler w/o debug w/refer", testFolder, testPrefix, false, false, testReferrer, false, ""}, 61 | {"Basic handler w/debug w/refer w/o cors", testFolder, "", true, true, testReferrer, false, ""}, 62 | {"Prefix handler w/debug w/refer w/o cors", testFolder, testPrefix, true, true, testReferrer, false, ""}, 63 | {"Basic and hide listing handler w/debug w/refer w/o cors", testFolder, "", false, true, testReferrer, false, ""}, 64 | {"Prefix and hide listing handler w/debug w/refer w/o cors", testFolder, testPrefix, false, true, testReferrer, false, ""}, 65 | {"Basic handler w/debug w/refer w/cors", testFolder, "", true, true, testReferrer, true, ""}, 66 | {"Prefix handler w/debug w/refer w/cors", testFolder, testPrefix, true, true, testReferrer, true, ""}, 67 | {"Basic and hide listing handler w/debug w/refer w/cors", testFolder, "", false, true, testReferrer, true, ""}, 68 | {"Prefix and hide listing handler w/debug w/refer w/cors", testFolder, testPrefix, false, true, testReferrer, true, ""}, 69 | {"Access Key and Basic handler w/o debug", testFolder, "", true, false, ignoreReferrer, false, testAccessKey}, 70 | {"Access Key and Prefix handler w/o debug", testFolder, testPrefix, true, false, ignoreReferrer, false, testAccessKey}, 71 | {"Access Key and Basic and hide listing handler w/o debug", testFolder, "", false, false, ignoreReferrer, false, testAccessKey}, 72 | {"Access Key and Prefix and hide listing handler w/o debug", testFolder, testPrefix, false, false, ignoreReferrer, false, testAccessKey}, 73 | {"Access Key and Basic handler w/debug", testFolder, "", true, true, ignoreReferrer, false, testAccessKey}, 74 | {"Access Key and Prefix handler w/debug", testFolder, testPrefix, true, true, ignoreReferrer, false, testAccessKey}, 75 | {"Access Key and Basic and hide listing handler w/debug", testFolder, "", false, true, ignoreReferrer, false, testAccessKey}, 76 | {"Access Key and Prefix and hide listing handler w/debug", testFolder, testPrefix, false, true, ignoreReferrer, false, testAccessKey}, 77 | {"Access Key and Basic handler w/o debug w/refer", testFolder, "", true, false, testReferrer, false, testAccessKey}, 78 | {"Access Key and Prefix handler w/o debug w/refer", testFolder, testPrefix, true, false, testReferrer, false, testAccessKey}, 79 | {"Access Key and Basic and hide listing handler w/o debug w/refer", testFolder, "", false, false, testReferrer, false, testAccessKey}, 80 | {"Access Key and Prefix and hide listing handler w/o debug w/refer", testFolder, testPrefix, false, false, testReferrer, false, testAccessKey}, 81 | {"Access Key and Basic handler w/debug w/refer w/o cors", testFolder, "", true, true, testReferrer, false, testAccessKey}, 82 | {"Access Key and Prefix handler w/debug w/refer w/o cors", testFolder, testPrefix, true, true, testReferrer, false, testAccessKey}, 83 | {"Access Key and Basic and hide listing handler w/debug w/refer w/o cors", testFolder, "", false, true, testReferrer, false, testAccessKey}, 84 | {"Access Key and Prefix and hide listing handler w/debug w/refer w/o cors", testFolder, testPrefix, false, true, testReferrer, false, testAccessKey}, 85 | {"Access Key and Basic handler w/debug w/refer w/cors", testFolder, "", true, true, testReferrer, true, testAccessKey}, 86 | {"Access Key and Prefix handler w/debug w/refer w/cors", testFolder, testPrefix, true, true, testReferrer, true, testAccessKey}, 87 | {"Access Key and Basic and hide listing handler w/debug w/refer w/cors", testFolder, "", false, true, testReferrer, true, testAccessKey}, 88 | {"Access Key and Prefix and hide listing handler w/debug w/refer w/cors", testFolder, testPrefix, false, true, testReferrer, true, testAccessKey}, 89 | } 90 | 91 | for _, tc := range testCases { 92 | t.Run(tc.name, func(t *testing.T) { 93 | config.Get.Debug = tc.debug 94 | config.Get.Folder = tc.folder 95 | config.Get.ShowListing = tc.listing 96 | config.Get.URLPrefix = tc.prefix 97 | config.Get.Referrers = tc.refer 98 | config.Get.Cors = tc.cors 99 | config.Get.AccessKey = tc.accessKey 100 | 101 | handlerSelector() 102 | }) 103 | } 104 | } 105 | 106 | func TestListenerSelector(t *testing.T) { 107 | // This test only exercises function branches. 108 | testCert := "file.crt" 109 | testKey := "file.key" 110 | 111 | testCases := []struct { 112 | name string 113 | cert string 114 | key string 115 | }{ 116 | {"HTTP", "", ""}, 117 | {"HTTPS", testCert, testKey}, 118 | } 119 | 120 | for _, tc := range testCases { 121 | t.Run(tc.name, func(t *testing.T) { 122 | config.Get.TLSCert = tc.cert 123 | config.Get.TLSKey = tc.key 124 | listenerSelector() 125 | }) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /cli/help/help.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Run print operation. 8 | func Run() error { 9 | fmt.Println(Text) 10 | return nil 11 | } 12 | 13 | var ( 14 | // Text for directly accessing help. 15 | Text = ` 16 | NAME 17 | static-file-server 18 | 19 | SYNOPSIS 20 | static-file-server 21 | static-file-server [ -c | -config | --config ] /path/to/config.yml 22 | static-file-server [ help | -help | --help ] 23 | static-file-server [ version | -version | --version ] 24 | 25 | DESCRIPTION 26 | The Static File Server is intended to be a tiny, fast and simple solution 27 | for serving files over HTTP. The features included are limited to make to 28 | binding to a host name and port, selecting a folder to serve, choosing a 29 | URL path prefix and selecting TLS certificates. If you want really awesome 30 | reverse proxy features, I recommend Nginx. 31 | 32 | DEPENDENCIES 33 | None... not even libc! 34 | 35 | ENVIRONMENT VARIABLES 36 | CORS 37 | When set to 'true' it enables resource access from any domain. All 38 | responses will include the headers 'Access-Control-Allow-Origin' and 39 | 'Access-Control-Allow-Headers' with a wildcard value ('*'). 40 | DEBUG 41 | When set to 'true' enables additional logging, including the 42 | configuration used and an access log for each request. IMPORTANT NOTE: 43 | The configuration summary is printed to stdout while logs generated 44 | during execution are printed to stderr. Default value is 'false'. 45 | FOLDER 46 | The path to the folder containing the contents to be served over 47 | HTTP(s). If not supplied, defaults to '/web' (for Docker reasons). 48 | HOST 49 | The hostname used for binding. If not supplied, contents will be served 50 | to a client without regard for the hostname. 51 | PORT 52 | The port used for binding. If not supplied, defaults to port '8080'. 53 | REFERRERS 54 | A comma-separated list of acceped Referrers based on the 'Referer' HTTP 55 | header. If incoming header value is not in the list, a 403 HTTP error is 56 | returned. To accept requests without a 'Referer' HTTP header in addition 57 | to the whitelisted values, include an empty value (either with a leading 58 | comma in the environment variable or with an empty list item in the YAML 59 | configuration file) as demonstrated in the second example. If not 60 | supplied the 'Referer' HTTP header is ignored. 61 | Examples: 62 | REFERRERS='http://localhost,https://some.site,http://other.site:8080' 63 | REFERRERS=',http://localhost,https://some.site,http://other.site:8080' 64 | ALLOW_INDEX 65 | When set to 'true' the index.html file in the folder(not include the 66 | sub folders) will be served. And the file list will not be served. 67 | For example, if the client requests 'http://127.0.0.1/' the 'index.html' 68 | file in the root of the directory being served is returned. Default value 69 | is 'true'. 70 | SHOW_LISTING 71 | Automatically serve the index file for the directory if requested. For 72 | example, if the client requests 'http://127.0.0.1/' the 'index.html' 73 | file in the root of the directory being served is returned. If the value 74 | is set to 'false', the same request will return a 'NOT FOUND'. Default 75 | value is 'true'. 76 | TLS_CERT 77 | Path to the TLS certificate file to serve files using HTTPS. If supplied 78 | then TLS_KEY must also be supplied. If not supplied, contents will be 79 | served via HTTP. 80 | TLS_KEY 81 | Path to the TLS key file to serve files using HTTPS. If supplied then 82 | TLS_CERT must also be supplied. If not supplied, contents will be served 83 | via HTTPS 84 | TLS_MIN_VERS 85 | The minimum TLS version to use. If not supplied, defaults to TLS1.0. 86 | Acceptable values are 'TLS10', 'TLS11', 'TLS12' and 'TLS13' for TLS1.0, 87 | TLS1.1, TLS1.2 and TLS1.3, respectively. Values are not case-sensitive. 88 | URL_PREFIX 89 | The prefix to use in the URL path. If supplied, then the prefix must 90 | start with a forward-slash and NOT end with a forward-slash. If not 91 | supplied then no prefix is used. 92 | 93 | CONFIGURATION FILE 94 | Configuration can also managed used a YAML configuration file. To select the 95 | configuration values using the YAML file, pass in the path to the file using 96 | the appropriate flags (-c, --config). Environment variables take priority 97 | over the configuration file. The following is an example configuration using 98 | the default values. 99 | 100 | Example config.yml with defaults: 101 | ---------------------------------------------------------------------------- 102 | cors: false 103 | debug: false 104 | folder: /web 105 | host: "" 106 | port: 8080 107 | referrers: [] 108 | show-listing: true 109 | tls-cert: "" 110 | tls-key: "" 111 | tls-min-vers: "" 112 | url-prefix: "" 113 | ---------------------------------------------------------------------------- 114 | 115 | Example config.yml with possible alternative values: 116 | ---------------------------------------------------------------------------- 117 | debug: true 118 | folder: /var/www 119 | port: 80 120 | referrers: 121 | - http://localhost 122 | - https://mydomain.com 123 | ---------------------------------------------------------------------------- 124 | 125 | USAGE 126 | FILE LAYOUT 127 | /var/www/sub/my.file 128 | /var/www/index.html 129 | 130 | COMMAND 131 | export FOLDER=/var/www/sub 132 | static-file-server 133 | Retrieve with: wget http://localhost:8080/my.file 134 | wget http://my.machine:8080/my.file 135 | 136 | export FOLDER=/var/www 137 | export HOST=my.machine 138 | export PORT=80 139 | static-file-server 140 | Retrieve with: wget http://my.machine/sub/my.file 141 | 142 | export FOLDER=/var/www 143 | static-file-server -c config.yml 144 | Result: Runs with values from config.yml, but with the folder being 145 | served overridden by the FOLDER environment variable. 146 | 147 | export FOLDER=/var/www/sub 148 | export HOST=my.machine 149 | export PORT=80 150 | export URL_PREFIX=/my/stuff 151 | static-file-server 152 | Retrieve with: wget http://my.machine/my/stuff/my.file 153 | 154 | export FOLDER=/var/www/sub 155 | export TLS_CERT=/etc/server/my.machine.crt 156 | export TLS_KEY=/etc/server/my.machine.key 157 | static-file-server 158 | Retrieve with: wget https://my.machine:8080/my.file 159 | 160 | export FOLDER=/var/www/sub 161 | export PORT=443 162 | export TLS_CERT=/etc/server/my.machine.crt 163 | export TLS_KEY=/etc/server/my.machine.key 164 | export TLS_MIN_VERS=TLS12 165 | static-file-server 166 | Retrieve with: wget https://my.machine/my.file 167 | 168 | export FOLDER=/var/www 169 | export PORT=80 170 | export ALLOW_INDEX=true # Default behavior 171 | export SHOW_LISTING=true # Default behavior 172 | static-file-server 173 | Retrieve 'index.html' with: wget http://my.machine/ 174 | 175 | export FOLDER=/var/www 176 | export PORT=80 177 | export ALLOW_INDEX=true # Default behavior 178 | export SHOW_LISTING=false 179 | static-file-server 180 | Retrieve 'index.html' with: wget http://my.machine/ 181 | Returns 'NOT FOUND': wget http://my.machine/dir/ 182 | 183 | export FOLDER=/var/www 184 | export PORT=80 185 | export ALLOW_INDEX=false 186 | export SHOW_LISTING=false 187 | static-file-server 188 | Returns 'NOT FOUND': wget http://my.machine/ 189 | ` 190 | ) 191 | -------------------------------------------------------------------------------- /handle/handle.go: -------------------------------------------------------------------------------- 1 | package handle 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/tls" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "path" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | // These assignments are for unit testing. 16 | listenAndServe = http.ListenAndServe 17 | listenAndServeTLS = defaultListenAndServeTLS 18 | setHandler = http.HandleFunc 19 | ) 20 | 21 | var ( 22 | // Server options to be set prior to calling the listening function. 23 | // minTLSVersion is the minimum allowed TLS version to be used by the 24 | // server. 25 | minTLSVersion uint16 = tls.VersionTLS10 26 | ) 27 | 28 | // defaultListenAndServeTLS is the default implementation of the listening 29 | // function for serving with TLS enabled. This is, effectively, a copy from 30 | // the standard library but with the ability to set the minimum TLS version. 31 | func defaultListenAndServeTLS( 32 | binding, certFile, keyFile string, handler http.Handler, 33 | ) error { 34 | if handler == nil { 35 | handler = http.DefaultServeMux 36 | } 37 | server := &http.Server{ 38 | Addr: binding, 39 | Handler: handler, 40 | TLSConfig: &tls.Config{ 41 | MinVersion: minTLSVersion, 42 | }, 43 | } 44 | return server.ListenAndServeTLS(certFile, keyFile) 45 | } 46 | 47 | // SetMinimumTLSVersion to be used by the server. 48 | func SetMinimumTLSVersion(version uint16) { 49 | if version < tls.VersionTLS10 { 50 | version = tls.VersionTLS10 51 | } else if version > tls.VersionTLS13 { 52 | version = tls.VersionTLS13 53 | } 54 | minTLSVersion = version 55 | } 56 | 57 | // ListenerFunc accepts the {hostname:port} binding string required by HTTP 58 | // listeners and the handler (router) function and returns any errors that 59 | // occur. 60 | type ListenerFunc func(string, http.HandlerFunc) error 61 | 62 | // FileServerFunc is used to serve the file from the local file system to the 63 | // requesting client. 64 | type FileServerFunc func(http.ResponseWriter, *http.Request, string) 65 | 66 | // WithReferrers returns a function that evaluates the HTTP 'Referer' header 67 | // value and returns HTTP error 403 if the value is not found in the whitelist. 68 | // If one of the whitelisted referrers are an empty string, then it is allowed 69 | // for the 'Referer' HTTP header key to not be set. 70 | func WithReferrers(serveFile FileServerFunc, referrers []string) FileServerFunc { 71 | return func(w http.ResponseWriter, r *http.Request, name string) { 72 | if !validReferrer(referrers, r.Referer()) { 73 | http.Error( 74 | w, 75 | fmt.Sprintf("Invalid source '%s'", r.Referer()), 76 | http.StatusForbidden, 77 | ) 78 | return 79 | } 80 | serveFile(w, r, name) 81 | } 82 | } 83 | 84 | // WithLogging returns a function that logs information about the request prior 85 | // to serving the requested file. 86 | func WithLogging(serveFile FileServerFunc) FileServerFunc { 87 | return func(w http.ResponseWriter, r *http.Request, name string) { 88 | referer := r.Referer() 89 | if len(referer) == 0 { 90 | log.Printf( 91 | "REQ from '%s': %s %s %s%s -> %s\n", 92 | r.RemoteAddr, 93 | r.Method, 94 | r.Proto, 95 | r.Host, 96 | r.URL.Path, 97 | name, 98 | ) 99 | } else { 100 | log.Printf( 101 | "REQ from '%s' (REFERER: '%s'): %s %s %s%s -> %s\n", 102 | r.RemoteAddr, 103 | referer, 104 | r.Method, 105 | r.Proto, 106 | r.Host, 107 | r.URL.Path, 108 | name, 109 | ) 110 | } 111 | serveFile(w, r, name) 112 | } 113 | } 114 | 115 | // Basic file handler servers files from the passed folder. 116 | func Basic(serveFile FileServerFunc, folder string) http.HandlerFunc { 117 | return func(w http.ResponseWriter, r *http.Request) { 118 | serveFile(w, r, folder+r.URL.Path) 119 | } 120 | } 121 | 122 | // Prefix file handler is an alternative to Basic where a URL prefix is removed 123 | // prior to serving a file (http://my.machine/prefix/file.txt will serve 124 | // file.txt from the root of the folder being served (ignoring 'prefix')). 125 | func Prefix(serveFile FileServerFunc, folder, urlPrefix string) http.HandlerFunc { 126 | return func(w http.ResponseWriter, r *http.Request) { 127 | if !strings.HasPrefix(r.URL.Path, urlPrefix) { 128 | http.NotFound(w, r) 129 | return 130 | } 131 | serveFile(w, r, folder+strings.TrimPrefix(r.URL.Path, urlPrefix)) 132 | } 133 | } 134 | 135 | // PreventListings returns a function that prevents listing of directories but 136 | // still allows index.html to be served. 137 | func PreventListings(serve http.HandlerFunc, folder string, urlPrefix string) http.HandlerFunc { 138 | return func(w http.ResponseWriter, r *http.Request) { 139 | if strings.HasSuffix(r.URL.Path, "/") { 140 | // If the directory does not contain an index.html file, then 141 | // return 'NOT FOUND' to prevent listing of the directory. 142 | stat, err := os.Stat(path.Join(folder, strings.TrimPrefix(r.URL.Path, urlPrefix), "index.html")) 143 | if err != nil || (err == nil && !stat.Mode().IsRegular()) { 144 | http.NotFound(w, r) 145 | return 146 | } 147 | } 148 | serve(w, r) 149 | } 150 | } 151 | 152 | // IgnoreIndex wraps an HTTP request. In the event of a folder root request, 153 | // this function will automatically return 'NOT FOUND' as opposed to default 154 | // behavior where the index file for that directory is retrieved. 155 | func IgnoreIndex(serve http.HandlerFunc) http.HandlerFunc { 156 | return func(w http.ResponseWriter, r *http.Request) { 157 | if strings.HasSuffix(r.URL.Path, "/") { 158 | http.NotFound(w, r) 159 | return 160 | } 161 | serve(w, r) 162 | } 163 | } 164 | 165 | // AddCorsWildcardHeaders wraps an HTTP request to notify client browsers that 166 | // resources should be allowed to be retrieved by any other domain. 167 | func AddCorsWildcardHeaders(serve http.HandlerFunc) http.HandlerFunc { 168 | return func(w http.ResponseWriter, r *http.Request) { 169 | w.Header().Set("Access-Control-Allow-Origin", "*") 170 | w.Header().Set("Access-Control-Allow-Headers", "*") 171 | w.Header().Set("Cross-Origin-Resource-Policy", "cross-origin") 172 | serve(w, r) 173 | } 174 | } 175 | 176 | // AddAccessKey provides Access Control through url parameters. The access key 177 | // is set by ACCESS_KEY. md5sum is computed by queried path + access key 178 | // (e.g. "/my/file" + ACCESS_KEY) 179 | func AddAccessKey(serve http.HandlerFunc, accessKey string) http.HandlerFunc { 180 | return func(w http.ResponseWriter, r *http.Request) { 181 | // Get key or md5sum from this access. 182 | keys, keyOk := r.URL.Query()["key"] 183 | var code string 184 | if !keyOk || len(keys[0]) < 1 { 185 | // In case a code is provided 186 | codes, codeOk := r.URL.Query()["code"] 187 | if !codeOk || len(codes[0]) < 1 { 188 | http.NotFound(w, r) 189 | return 190 | } 191 | code = strings.ToUpper(codes[0]) 192 | } else { 193 | // In case a key is provided, convert to code. 194 | data := []byte(r.URL.Path + keys[0]) 195 | hash := md5.Sum(data) 196 | code = fmt.Sprintf("%X", hash) 197 | } 198 | 199 | // Compute the correct md5sum of this access. 200 | localData := []byte(r.URL.Path + accessKey) 201 | hash := md5.Sum(localData) 202 | localCode := fmt.Sprintf("%X", hash) 203 | 204 | // Compare the two. 205 | if code != localCode { 206 | http.NotFound(w, r) 207 | return 208 | } 209 | serve(w, r) 210 | } 211 | } 212 | 213 | // Listening function for serving the handler function. 214 | func Listening() ListenerFunc { 215 | return func(binding string, handler http.HandlerFunc) error { 216 | setHandler("/", handler) 217 | return listenAndServe(binding, nil) 218 | } 219 | } 220 | 221 | // TLSListening function for serving the handler function with encryption. 222 | func TLSListening(tlsCert, tlsKey string) ListenerFunc { 223 | return func(binding string, handler http.HandlerFunc) error { 224 | setHandler("/", handler) 225 | return listenAndServeTLS(binding, tlsCert, tlsKey, nil) 226 | } 227 | } 228 | 229 | // validReferrer returns true if the passed referrer can be resolved by the 230 | // passed list of referrers. 231 | func validReferrer(s []string, e string) bool { 232 | // Whitelisted referer list is empty. All requests are allowed. 233 | if len(s) == 0 { 234 | return true 235 | } 236 | 237 | for _, a := range s { 238 | // Handle blank HTTP Referer header, if configured 239 | if a == "" { 240 | if e == "" { 241 | return true 242 | } 243 | // Continue loop (all strings start with "") 244 | continue 245 | } 246 | 247 | // Compare header with allowed prefixes 248 | if strings.HasPrefix(e, a) { 249 | return true 250 | } 251 | } 252 | return false 253 | } 254 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "strconv" 11 | "strings" 12 | 13 | yaml "gopkg.in/yaml.v3" 14 | ) 15 | 16 | var ( 17 | // Get the desired configuration value. 18 | Get struct { 19 | Cors bool `yaml:"cors"` 20 | Debug bool `yaml:"debug"` 21 | Folder string `yaml:"folder"` 22 | Host string `yaml:"host"` 23 | Port uint16 `yaml:"port"` 24 | AllowIndex bool `yaml:"allow-index"` 25 | ShowListing bool `yaml:"show-listing"` 26 | TLSCert string `yaml:"tls-cert"` 27 | TLSKey string `yaml:"tls-key"` 28 | TLSMinVers uint16 `yaml:"-"` 29 | TLSMinVersStr string `yaml:"tls-min-vers"` 30 | URLPrefix string `yaml:"url-prefix"` 31 | Referrers []string `yaml:"referrers"` 32 | AccessKey string `yaml:"access-key"` 33 | } 34 | ) 35 | 36 | const ( 37 | corsKey = "CORS" 38 | debugKey = "DEBUG" 39 | folderKey = "FOLDER" 40 | hostKey = "HOST" 41 | portKey = "PORT" 42 | referrersKey = "REFERRERS" 43 | allowIndexKey = "ALLOW_INDEX" 44 | showListingKey = "SHOW_LISTING" 45 | tlsCertKey = "TLS_CERT" 46 | tlsKeyKey = "TLS_KEY" 47 | tlsMinVersKey = "TLS_MIN_VERS" 48 | urlPrefixKey = "URL_PREFIX" 49 | accessKeyKey = "ACCESS_KEY" 50 | ) 51 | 52 | var ( 53 | defaultDebug = false 54 | defaultFolder = "/web" 55 | defaultHost = "" 56 | defaultPort = uint16(8080) 57 | defaultReferrers = []string{} 58 | defaultAllowIndex = true 59 | defaultShowListing = true 60 | defaultTLSCert = "" 61 | defaultTLSKey = "" 62 | defaultTLSMinVers = "" 63 | defaultURLPrefix = "" 64 | defaultCors = false 65 | defaultAccessKey = "" 66 | ) 67 | 68 | func init() { 69 | // init calls setDefaults to better support testing. 70 | setDefaults() 71 | } 72 | 73 | func setDefaults() { 74 | Get.Debug = defaultDebug 75 | Get.Folder = defaultFolder 76 | Get.Host = defaultHost 77 | Get.Port = defaultPort 78 | Get.Referrers = defaultReferrers 79 | Get.AllowIndex = defaultAllowIndex 80 | Get.ShowListing = defaultShowListing 81 | Get.TLSCert = defaultTLSCert 82 | Get.TLSKey = defaultTLSKey 83 | Get.TLSMinVersStr = defaultTLSMinVers 84 | Get.URLPrefix = defaultURLPrefix 85 | Get.Cors = defaultCors 86 | Get.AccessKey = defaultAccessKey 87 | } 88 | 89 | // Load the configuration file. 90 | func Load(filename string) (err error) { 91 | // If no filename provided, assign envvars. 92 | if filename == "" { 93 | overrideWithEnvVars() 94 | return validate() 95 | } 96 | 97 | // Read contents from configuration file. 98 | var contents []byte 99 | if contents, err = ioutil.ReadFile(filename); nil != err { 100 | return 101 | } 102 | 103 | // Parse contents into 'Get' configuration. 104 | if err = yaml.Unmarshal(contents, &Get); nil != err { 105 | return 106 | } 107 | 108 | overrideWithEnvVars() 109 | return validate() 110 | } 111 | 112 | // Log the current configuration. 113 | func Log() { 114 | // YAML marshalling should never error, but if it could, the result is that 115 | // the contents of the configuration are not logged. 116 | contents, _ := yaml.Marshal(&Get) 117 | 118 | // Log the configuration. 119 | fmt.Println("Using the following configuration:") 120 | fmt.Println(string(contents)) 121 | } 122 | 123 | // overrideWithEnvVars the default values and the configuration file values. 124 | func overrideWithEnvVars() { 125 | // Assign envvars, if set. 126 | Get.Cors = envAsBool(corsKey, Get.Cors) 127 | Get.Debug = envAsBool(debugKey, Get.Debug) 128 | Get.Folder = envAsStr(folderKey, Get.Folder) 129 | Get.Host = envAsStr(hostKey, Get.Host) 130 | Get.Port = envAsUint16(portKey, Get.Port) 131 | Get.AllowIndex = envAsBool(allowIndexKey, Get.AllowIndex) 132 | Get.ShowListing = envAsBool(showListingKey, Get.ShowListing) 133 | Get.TLSCert = envAsStr(tlsCertKey, Get.TLSCert) 134 | Get.TLSKey = envAsStr(tlsKeyKey, Get.TLSKey) 135 | Get.TLSMinVersStr = envAsStr(tlsMinVersKey, Get.TLSMinVersStr) 136 | Get.URLPrefix = envAsStr(urlPrefixKey, Get.URLPrefix) 137 | Get.Referrers = envAsStrSlice(referrersKey, Get.Referrers) 138 | Get.AccessKey = envAsStr(accessKeyKey, Get.AccessKey) 139 | } 140 | 141 | // validate the configuration. 142 | func validate() error { 143 | // If HTTPS is to be used, verify both TLS_* environment variables are set. 144 | useTLS := false 145 | if 0 < len(Get.TLSCert) || 0 < len(Get.TLSKey) { 146 | if len(Get.TLSCert) == 0 || len(Get.TLSKey) == 0 { 147 | msg := "if value for either 'TLS_CERT' or 'TLS_KEY' is set then " + 148 | "then value for the other must also be set (values are " + 149 | "currently '%s' and '%s', respectively)" 150 | return fmt.Errorf(msg, Get.TLSCert, Get.TLSKey) 151 | } 152 | if _, err := os.Stat(Get.TLSCert); nil != err { 153 | msg := "value of TLS_CERT is set with filename '%s' that returns %v" 154 | return fmt.Errorf(msg, Get.TLSCert, err) 155 | } 156 | if _, err := os.Stat(Get.TLSKey); nil != err { 157 | msg := "value of TLS_KEY is set with filename '%s' that returns %v" 158 | return fmt.Errorf(msg, Get.TLSKey, err) 159 | } 160 | useTLS = true 161 | } 162 | 163 | // Verify TLS_MIN_VERS is only (optionally) set if TLS is to be used. 164 | Get.TLSMinVers = tls.VersionTLS10 165 | if useTLS { 166 | if 0 < len(Get.TLSMinVersStr) { 167 | var err error 168 | if Get.TLSMinVers, err = tlsMinVersAsUint16( 169 | Get.TLSMinVersStr, 170 | ); nil != err { 171 | return err 172 | } 173 | } 174 | 175 | // For logging minimum TLS version being used while debugging, backfill 176 | // the TLSMinVersStr field. 177 | switch Get.TLSMinVers { 178 | case tls.VersionTLS10: 179 | Get.TLSMinVersStr = "TLS1.0" 180 | case tls.VersionTLS11: 181 | Get.TLSMinVersStr = "TLS1.1" 182 | case tls.VersionTLS12: 183 | Get.TLSMinVersStr = "TLS1.2" 184 | case tls.VersionTLS13: 185 | Get.TLSMinVersStr = "TLS1.3" 186 | } 187 | } else { 188 | if 0 < len(Get.TLSMinVersStr) { 189 | msg := "value for 'TLS_MIN_VERS' is set but 'TLS_CERT' and 'TLS_KEY' are not" 190 | return errors.New(msg) 191 | } 192 | } 193 | 194 | // If the URL path prefix is to be used, verify it is properly formatted. 195 | if 0 < len(Get.URLPrefix) && 196 | (!strings.HasPrefix(Get.URLPrefix, "/") || strings.HasSuffix(Get.URLPrefix, "/")) { 197 | msg := "if value for 'URL_PREFIX' is set then the value must start " + 198 | "with '/' and not end with '/' (current value of '%s' vs valid " + 199 | "example of '/my/prefix'" 200 | return fmt.Errorf(msg, Get.URLPrefix) 201 | } 202 | 203 | return nil 204 | } 205 | 206 | // envAsStr returns the value of the environment variable as a string if set. 207 | func envAsStr(key, fallback string) string { 208 | if value := os.Getenv(key); value != "" { 209 | return value 210 | } 211 | return fallback 212 | } 213 | 214 | // envAsStrSlice returns the value of the environment variable as a slice of 215 | // strings if set. 216 | func envAsStrSlice(key string, fallback []string) []string { 217 | if value := os.Getenv(key); value != "" { 218 | return strings.Split(value, ",") 219 | } 220 | return fallback 221 | } 222 | 223 | // envAsUint16 returns the value of the environment variable as a uint16 if set. 224 | func envAsUint16(key string, fallback uint16) uint16 { 225 | // Retrieve the string value of the environment variable. If not set, 226 | // fallback is used. 227 | valueStr := os.Getenv(key) 228 | if valueStr == "" { 229 | return fallback 230 | } 231 | 232 | // Parse the string into a uint16. 233 | base := 10 234 | bitSize := 16 235 | valueAsUint64, err := strconv.ParseUint(valueStr, base, bitSize) 236 | if nil != err { 237 | log.Printf( 238 | "Invalid value for '%s': %v\nUsing fallback: %d", 239 | key, err, fallback, 240 | ) 241 | return fallback 242 | } 243 | return uint16(valueAsUint64) 244 | } 245 | 246 | // envAsBool returns the value for an environment variable or, if not set, a 247 | // fallback value as a boolean. 248 | func envAsBool(key string, fallback bool) bool { 249 | // Retrieve the string value of the environment variable. If not set, 250 | // fallback is used. 251 | valueStr := os.Getenv(key) 252 | if valueStr == "" { 253 | return fallback 254 | } 255 | 256 | // Parse the string into a boolean. 257 | value, err := strAsBool(valueStr) 258 | if nil != err { 259 | log.Printf( 260 | "Invalid value for '%s': %v\nUsing fallback: %t", 261 | key, err, fallback, 262 | ) 263 | return fallback 264 | } 265 | return value 266 | } 267 | 268 | // strAsBool converts the intent of the passed value into a boolean 269 | // representation. 270 | func strAsBool(value string) (result bool, err error) { 271 | lvalue := strings.ToLower(value) 272 | switch lvalue { 273 | case "0", "false", "f", "no", "n": 274 | result = false 275 | case "1", "true", "t", "yes", "y": 276 | result = true 277 | default: 278 | result = false 279 | msg := "unknown conversion from string to bool for value '%s'" 280 | err = fmt.Errorf(msg, value) 281 | } 282 | return 283 | } 284 | 285 | // tlsMinVersAsUint16 converts the intent of the passed value into an 286 | // enumeration for the crypto/tls package. 287 | func tlsMinVersAsUint16(value string) (result uint16, err error) { 288 | switch strings.ToLower(value) { 289 | case "tls10": 290 | result = tls.VersionTLS10 291 | case "tls11": 292 | result = tls.VersionTLS11 293 | case "tls12": 294 | result = tls.VersionTLS12 295 | case "tls13": 296 | result = tls.VersionTLS13 297 | default: 298 | err = fmt.Errorf("unknown value for TLS_MIN_VERS: %s", value) 299 | } 300 | return 301 | } 302 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strconv" 9 | "testing" 10 | 11 | yaml "gopkg.in/yaml.v3" 12 | ) 13 | 14 | func TestLoad(t *testing.T) { 15 | // Verify envvars are set. 16 | testFolder := "/my/directory" 17 | os.Setenv(folderKey, testFolder) 18 | if err := Load(""); nil != err { 19 | t.Errorf( 20 | "While loading an empty file name expected no error but got %v", 21 | err, 22 | ) 23 | } 24 | if Get.Folder != testFolder { 25 | t.Errorf( 26 | "While loading an empty file name expected folder %s but got %s", 27 | testFolder, Get.Folder, 28 | ) 29 | } 30 | 31 | // Verify error if file doesn't exist. 32 | if err := Load("/this/file/should/never/exist"); nil == err { 33 | t.Error("While loading non-existing file expected error but got nil") 34 | } 35 | 36 | // Verify bad YAML returns an error. 37 | func(t *testing.T) { 38 | filename := "testing.tmp" 39 | contents := []byte("{") 40 | defer os.Remove(filename) 41 | 42 | if err := ioutil.WriteFile(filename, contents, 0666); nil != err { 43 | t.Errorf("Failed to save bad YAML file with: %v\n", err) 44 | } 45 | if err := Load(filename); nil == err { 46 | t.Error("While loading bad YAML expected error but got nil") 47 | } 48 | }(t) 49 | 50 | // Verify good YAML returns no error and sets value. 51 | func(t *testing.T) { 52 | filename := "testing.tmp" 53 | testFolder := "/test/folder" 54 | contents := []byte(fmt.Sprintf( 55 | `{"folder": "%s"}`, testFolder, 56 | )) 57 | defer os.Remove(filename) 58 | 59 | if err := ioutil.WriteFile(filename, contents, 0666); nil != err { 60 | t.Errorf("Failed to save good YAML file with: %v\n", err) 61 | } 62 | if err := Load(filename); nil != err { 63 | t.Errorf( 64 | "While loading good YAML expected nil but got %v", 65 | err, 66 | ) 67 | } 68 | }(t) 69 | } 70 | 71 | func TestLog(t *testing.T) { 72 | // Test whether YAML marshalling works, as that is the only error case. 73 | if _, err := yaml.Marshal(&Get); nil != err { 74 | t.Errorf("While testing YAML marshalling for config Log() got %v", err) 75 | } 76 | Log() 77 | } 78 | 79 | func TestOverrideWithEnvvars(t *testing.T) { 80 | // Choose values that are different than defaults. 81 | testDebug := true 82 | testFolder := "/my/directory" 83 | testHost := "apets.life" 84 | testPort := uint16(666) 85 | testAllowIndex := false 86 | testShowListing := false 87 | testTLSCert := "my.pem" 88 | testTLSKey := "my.key" 89 | testURLPrefix := "/url/prefix" 90 | 91 | // Set all environment variables with test values. 92 | os.Setenv(debugKey, fmt.Sprintf("%t", testDebug)) 93 | os.Setenv(folderKey, testFolder) 94 | os.Setenv(hostKey, testHost) 95 | os.Setenv(portKey, strconv.Itoa(int(testPort))) 96 | os.Setenv(allowIndexKey, fmt.Sprintf("%t", testAllowIndex)) 97 | os.Setenv(showListingKey, fmt.Sprintf("%t", testShowListing)) 98 | os.Setenv(tlsCertKey, testTLSCert) 99 | os.Setenv(tlsKeyKey, testTLSKey) 100 | os.Setenv(urlPrefixKey, testURLPrefix) 101 | 102 | // Verification functions. 103 | equalStrings := func(t *testing.T, name, key, expected, result string) { 104 | if expected != result { 105 | t.Errorf( 106 | "While checking %s for '%s' expected '%s' but got '%s'", 107 | name, key, expected, result, 108 | ) 109 | } 110 | } 111 | equalUint16 := func(t *testing.T, name, key string, expected, result uint16) { 112 | if expected != result { 113 | t.Errorf( 114 | "While checking %s for '%s' expected %d but got %d", 115 | name, key, expected, result, 116 | ) 117 | } 118 | } 119 | equalBool := func(t *testing.T, name, key string, expected, result bool) { 120 | if expected != result { 121 | t.Errorf( 122 | "While checking %s for '%s' expected %t but got %t", 123 | name, key, expected, result, 124 | ) 125 | } 126 | } 127 | 128 | // Verify defaults. 129 | setDefaults() 130 | phase := "defaults" 131 | equalBool(t, phase, debugKey, defaultDebug, Get.Debug) 132 | equalStrings(t, phase, folderKey, defaultFolder, Get.Folder) 133 | equalStrings(t, phase, hostKey, defaultHost, Get.Host) 134 | equalUint16(t, phase, portKey, defaultPort, Get.Port) 135 | equalBool(t, phase, showListingKey, defaultShowListing, Get.ShowListing) 136 | equalStrings(t, phase, tlsCertKey, defaultTLSCert, Get.TLSCert) 137 | equalStrings(t, phase, tlsKeyKey, defaultTLSKey, Get.TLSKey) 138 | equalStrings(t, phase, urlPrefixKey, defaultURLPrefix, Get.URLPrefix) 139 | 140 | // Apply overrides. 141 | overrideWithEnvVars() 142 | 143 | // Verify overrides. 144 | phase = "overrides" 145 | equalBool(t, phase, debugKey, testDebug, Get.Debug) 146 | equalStrings(t, phase, folderKey, testFolder, Get.Folder) 147 | equalStrings(t, phase, hostKey, testHost, Get.Host) 148 | equalUint16(t, phase, portKey, testPort, Get.Port) 149 | equalBool(t, phase, showListingKey, testShowListing, Get.ShowListing) 150 | equalStrings(t, phase, tlsCertKey, testTLSCert, Get.TLSCert) 151 | equalStrings(t, phase, tlsKeyKey, testTLSKey, Get.TLSKey) 152 | equalStrings(t, phase, urlPrefixKey, testURLPrefix, Get.URLPrefix) 153 | } 154 | 155 | func TestValidate(t *testing.T) { 156 | validPath := "config.go" 157 | invalidPath := "should/never/exist.txt" 158 | empty := "" 159 | prefix := "/my/prefix" 160 | 161 | testCases := []struct { 162 | name string 163 | cert string 164 | key string 165 | prefix string 166 | minTLS string 167 | isError bool 168 | }{ 169 | {"Valid paths w/prefix", validPath, validPath, prefix, "", false}, 170 | {"Valid paths wo/prefix", validPath, validPath, empty, "", false}, 171 | {"Empty paths w/prefix", empty, empty, prefix, "", false}, 172 | {"Empty paths wo/prefix", empty, empty, empty, "", false}, 173 | {"Mixed paths w/prefix", empty, validPath, prefix, "", true}, 174 | {"Alt mixed paths w/prefix", validPath, empty, prefix, "", true}, 175 | {"Mixed paths wo/prefix", empty, validPath, empty, "", true}, 176 | {"Alt mixed paths wo/prefix", validPath, empty, empty, "", true}, 177 | {"Invalid cert w/prefix", invalidPath, validPath, prefix, "", true}, 178 | {"Invalid key w/prefix", validPath, invalidPath, prefix, "", true}, 179 | {"Invalid cert & key w/prefix", invalidPath, invalidPath, prefix, "", true}, 180 | {"Prefix missing leading /", empty, empty, "my/prefix", "", true}, 181 | {"Prefix with trailing /", empty, empty, "/my/prefix/", "", true}, 182 | {"Valid paths w/min ok TLS", validPath, validPath, prefix, "tls11", false}, 183 | {"Valid paths w/min ok TLS", validPath, validPath, prefix, "tls12", false}, 184 | {"Valid paths w/min ok TLS", validPath, validPath, prefix, "tls13", false}, 185 | {"Valid paths w/min bad TLS", validPath, validPath, prefix, "bad", true}, 186 | {"Empty paths w/min ok TLS", empty, empty, prefix, "tls11", true}, 187 | {"Empty paths w/min bad TLS", empty, empty, prefix, "bad", true}, 188 | } 189 | 190 | for _, tc := range testCases { 191 | t.Run(tc.name, func(t *testing.T) { 192 | Get.TLSCert = tc.cert 193 | Get.TLSKey = tc.key 194 | Get.TLSMinVersStr = tc.minTLS 195 | Get.URLPrefix = tc.prefix 196 | err := validate() 197 | hasError := nil != err 198 | if hasError && !tc.isError { 199 | t.Errorf("Expected no error but got %v", err) 200 | } 201 | if !hasError && tc.isError { 202 | t.Error("Expected an error but got no error") 203 | } 204 | }) 205 | } 206 | } 207 | 208 | func TestEnvAsStr(t *testing.T) { 209 | sv := "STRING_VALUE" 210 | fv := "FLOAT_VALUE" 211 | iv := "INT_VALUE" 212 | bv := "BOOL_VALUE" 213 | ev := "EMPTY_VALUE" 214 | uv := "UNSET_VALUE" 215 | 216 | sr := "String Cheese" // String result 217 | fr := "123.456" // Float result 218 | ir := "-123" // Int result 219 | br := "true" // Bool result 220 | er := "" // Empty result 221 | fbr := "fallback result" // Fallback result 222 | efbr := "" // Empty fallback result 223 | 224 | os.Setenv(sv, sr) 225 | os.Setenv(fv, fr) 226 | os.Setenv(iv, ir) 227 | os.Setenv(bv, br) 228 | os.Setenv(ev, er) 229 | 230 | testCases := []struct { 231 | name string 232 | key string 233 | fallback string 234 | result string 235 | }{ 236 | {"Good string", sv, fbr, sr}, 237 | {"Float string", fv, fbr, fr}, 238 | {"Int string", iv, fbr, ir}, 239 | {"Bool string", bv, fbr, br}, 240 | {"Empty string", ev, fbr, fbr}, 241 | {"Unset", uv, fbr, fbr}, 242 | {"Good string with empty fallback", sv, efbr, sr}, 243 | {"Unset with empty fallback", uv, efbr, efbr}, 244 | } 245 | 246 | for _, tc := range testCases { 247 | t.Run(tc.name, func(t *testing.T) { 248 | result := envAsStr(tc.key, tc.fallback) 249 | if tc.result != result { 250 | t.Errorf( 251 | "For %s with a '%s' fallback expected '%s' but got '%s'", 252 | tc.key, tc.fallback, tc.result, result, 253 | ) 254 | } 255 | }) 256 | } 257 | } 258 | 259 | func TestEnvAsStrSlice(t *testing.T) { 260 | oe := "ONE_ENTRY" 261 | oewc := "ONE_ENTRY_WITH_COMMA" 262 | oewtc := "ONE_ENTRY_WITH_TRAILING_COMMA" 263 | te := "TWO_ENTRY" 264 | tewc := "TWO_ENTRY_WITH_COMMA" 265 | oc := "ONLY_COMMA" 266 | ev := "EMPTY_VALUE" 267 | uv := "UNSET_VALUE" 268 | 269 | fs := "http://my.site" 270 | ts := "http://other.site" 271 | fbr := []string{"one", "two"} 272 | var efbr []string 273 | 274 | oes := fs 275 | oer := []string{fs} 276 | oewcs := "," + fs 277 | oewcr := []string{"", fs} 278 | oewtcs := fs + "," 279 | oewtcr := []string{fs, ""} 280 | tes := fs + "," + ts 281 | ter := []string{fs, ts} 282 | tewcs := "," + fs + "," + ts 283 | tewcr := []string{"", fs, ts} 284 | ocs := "," 285 | ocr := []string{"", ""} 286 | evs := "" 287 | 288 | os.Setenv(oe, oes) 289 | os.Setenv(oewc, oewcs) 290 | os.Setenv(oewtc, oewtcs) 291 | os.Setenv(te, tes) 292 | os.Setenv(tewc, tewcs) 293 | os.Setenv(oc, ocs) 294 | os.Setenv(ev, evs) 295 | 296 | testCases := []struct { 297 | name string 298 | key string 299 | fallback []string 300 | result []string 301 | }{ 302 | {"One entry", oe, fbr, oer}, 303 | {"One entry w/comma", oewc, fbr, oewcr}, 304 | {"One entry w/trailing comma", oewtc, fbr, oewtcr}, 305 | {"Two entry", te, fbr, ter}, 306 | {"Two entry w/comma", tewc, fbr, tewcr}, 307 | {"Only comma", oc, fbr, ocr}, 308 | {"Empty value w/fallback", ev, fbr, fbr}, 309 | {"Empty value wo/fallback", ev, efbr, efbr}, 310 | {"Unset w/fallback", uv, fbr, fbr}, 311 | {"Unset wo/fallback", uv, efbr, efbr}, 312 | } 313 | 314 | matches := func(a, b []string) bool { 315 | if len(a) != len(b) { 316 | return false 317 | } 318 | tally := make(map[int]bool) 319 | for i := range a { 320 | tally[i] = false 321 | } 322 | for _, val := range a { 323 | for i, other := range b { 324 | if other == val && !tally[i] { 325 | tally[i] = true 326 | break 327 | } 328 | } 329 | } 330 | for _, found := range tally { 331 | if !found { 332 | return false 333 | } 334 | } 335 | return true 336 | } 337 | 338 | for _, tc := range testCases { 339 | t.Run(tc.name, func(t *testing.T) { 340 | result := envAsStrSlice(tc.key, tc.fallback) 341 | if !matches(tc.result, result) { 342 | t.Errorf( 343 | "For %s with a '%v' fallback expected '%v' but got '%v'", 344 | tc.key, tc.fallback, tc.result, result, 345 | ) 346 | } 347 | }) 348 | } 349 | } 350 | 351 | func TestEnvAsUint16(t *testing.T) { 352 | ubv := "UPPER_BOUNDS_VALUE" 353 | lbv := "LOWER_BOUNDS_VALUE" 354 | hv := "HIGH_VALUE" 355 | lv := "LOW_VALUE" 356 | bv := "BOOL_VALUE" 357 | sv := "STRING_VALUE" 358 | uv := "UNSET_VALUE" 359 | 360 | fbr := uint16(666) // Fallback result 361 | ubr := uint16(65535) // Upper bounds result 362 | lbr := uint16(0) // Lower bounds result 363 | 364 | os.Setenv(ubv, "65535") 365 | os.Setenv(lbv, "0") 366 | os.Setenv(hv, "65536") 367 | os.Setenv(lv, "-1") 368 | os.Setenv(bv, "true") 369 | os.Setenv(sv, "Cheese") 370 | 371 | testCases := []struct { 372 | name string 373 | key string 374 | fallback uint16 375 | result uint16 376 | }{ 377 | {"Upper bounds", ubv, fbr, ubr}, 378 | {"Lower bounds", lbv, fbr, lbr}, 379 | {"Out-of-bounds high", hv, fbr, fbr}, 380 | {"Out-of-bounds low", lv, fbr, fbr}, 381 | {"Boolean", bv, fbr, fbr}, 382 | {"String", sv, fbr, fbr}, 383 | {"Unset", uv, fbr, fbr}, 384 | } 385 | 386 | for _, tc := range testCases { 387 | t.Run(tc.name, func(t *testing.T) { 388 | result := envAsUint16(tc.key, tc.fallback) 389 | if tc.result != result { 390 | t.Errorf( 391 | "For %s with a %d fallback expected %d but got %d", 392 | tc.key, tc.fallback, tc.result, result, 393 | ) 394 | } 395 | }) 396 | } 397 | } 398 | 399 | func TestEnvAsBool(t *testing.T) { 400 | tv := "TRUE_VALUE" 401 | fv := "FALSE_VALUE" 402 | bv := "BAD_VALUE" 403 | uv := "UNSET_VALUE" 404 | 405 | os.Setenv(tv, "True") 406 | os.Setenv(fv, "NO") 407 | os.Setenv(bv, "BAD") 408 | 409 | testCases := []struct { 410 | name string 411 | key string 412 | fallback bool 413 | result bool 414 | }{ 415 | {"True with true fallback", tv, true, true}, 416 | {"True with false fallback", tv, false, true}, 417 | {"False with true fallback", fv, true, false}, 418 | {"False with false fallback", fv, false, false}, 419 | {"Bad with true fallback", bv, true, true}, 420 | {"Bad with false fallback", bv, false, false}, 421 | {"Unset with true fallback", uv, true, true}, 422 | {"Unset with false fallback", uv, false, false}, 423 | } 424 | 425 | for _, tc := range testCases { 426 | t.Run(tc.name, func(t *testing.T) { 427 | result := envAsBool(tc.key, tc.fallback) 428 | if tc.result != result { 429 | t.Errorf( 430 | "For %s with a %t fallback expected %t but got %t", 431 | tc.key, tc.fallback, tc.result, result, 432 | ) 433 | } 434 | }) 435 | } 436 | } 437 | 438 | func TestStrAsBool(t *testing.T) { 439 | testCases := []struct { 440 | name string 441 | value string 442 | result bool 443 | isError bool 444 | }{ 445 | {"Empty value", "", false, true}, 446 | {"False value", "0", false, false}, 447 | {"True value", "1", true, false}, 448 | } 449 | 450 | for _, tc := range testCases { 451 | t.Run(tc.name, func(t *testing.T) { 452 | result, err := strAsBool(tc.value) 453 | if result != tc.result { 454 | t.Errorf( 455 | "Expected %t for %s but got %t", 456 | tc.result, tc.value, result, 457 | ) 458 | } 459 | if tc.isError && nil == err { 460 | t.Errorf( 461 | "Expected error for %s but got no error", 462 | tc.value, 463 | ) 464 | } 465 | if !tc.isError && nil != err { 466 | t.Errorf( 467 | "Expected no error for %s but got %v", 468 | tc.value, err, 469 | ) 470 | } 471 | }) 472 | 473 | } 474 | } 475 | 476 | func TestTlsMinVersAsUint16(t *testing.T) { 477 | testCases := []struct { 478 | name string 479 | value string 480 | result uint16 481 | isError bool 482 | }{ 483 | {"Empty value", "", 0, true}, 484 | {"Valid TLS1.0", "TLS10", tls.VersionTLS10, false}, 485 | {"Valid TLS1.1", "tls11", tls.VersionTLS11, false}, 486 | {"Valid TLS1.2", "tls12", tls.VersionTLS12, false}, 487 | {"Valid TLS1.3", "tLS13", tls.VersionTLS13, false}, 488 | {"Invalid TLS1.4", "tls14", 0, true}, 489 | } 490 | 491 | for _, tc := range testCases { 492 | t.Run(tc.name, func(t *testing.T) { 493 | result, err := tlsMinVersAsUint16(tc.value) 494 | if result != tc.result { 495 | t.Errorf( 496 | "Expected %d for %s but got %d", 497 | tc.result, tc.value, result, 498 | ) 499 | } 500 | if tc.isError && nil == err { 501 | t.Errorf( 502 | "Expected error for %s but got no error", 503 | tc.value, 504 | ) 505 | } else if !tc.isError && nil != err { 506 | t.Errorf( 507 | "Expected no error for %s but got %v", 508 | tc.value, err, 509 | ) 510 | } 511 | }) 512 | } 513 | } 514 | -------------------------------------------------------------------------------- /img/sponsor.svg: -------------------------------------------------------------------------------- 1 | 2 | 148 | -------------------------------------------------------------------------------- /handle/handle_test.go: -------------------------------------------------------------------------------- 1 | package handle 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "net/http/httptest" 12 | "os" 13 | "path" 14 | "testing" 15 | ) 16 | 17 | var ( 18 | baseDir = "tmp/" 19 | subDir = "sub/" 20 | subDeepDir = "sub/deep/" 21 | tmpIndexName = "index.html" 22 | tmpFileName = "file.txt" 23 | tmpBadName = "bad.txt" 24 | tmpSubIndexName = "sub/index.html" 25 | tmpSubFileName = "sub/file.txt" 26 | tmpSubBadName = "sub/bad.txt" 27 | tmpSubDeepIndexName = "sub/deep/index.html" 28 | tmpSubDeepFileName = "sub/deep/file.txt" 29 | tmpSubDeepBadName = "sub/deep/bad.txt" 30 | tmpNoIndexDir = "noindex/" 31 | tmpNoIndexName = "noindex/noindex.txt" 32 | 33 | tmpIndex = "Space: the final frontier" 34 | tmpFile = "These are the voyages of the starship Enterprise." 35 | tmpSubIndex = "Its continuing mission:" 36 | tmpSubFile = "To explore strange new worlds" 37 | tmpSubDeepIndex = "To seek out new life and new civilizations" 38 | tmpSubDeepFile = "To boldly go where no one has gone before" 39 | 40 | nothing = "" 41 | ok = http.StatusOK 42 | missing = http.StatusNotFound 43 | redirect = http.StatusMovedPermanently 44 | notFound = "404 page not found\n" 45 | 46 | files = map[string]string{ 47 | baseDir + tmpIndexName: tmpIndex, 48 | baseDir + tmpFileName: tmpFile, 49 | baseDir + tmpSubIndexName: tmpSubIndex, 50 | baseDir + tmpSubFileName: tmpSubFile, 51 | baseDir + tmpSubDeepIndexName: tmpSubDeepIndex, 52 | baseDir + tmpSubDeepFileName: tmpSubDeepFile, 53 | baseDir + tmpNoIndexName: tmpSubDeepFile, 54 | } 55 | 56 | serveFileFuncs = []FileServerFunc{ 57 | http.ServeFile, 58 | WithLogging(http.ServeFile), 59 | } 60 | ) 61 | 62 | func TestMain(m *testing.M) { 63 | code := func(m *testing.M) int { 64 | if err := setup(); nil != err { 65 | log.Fatalf("While setting up test got: %v\n", err) 66 | } 67 | defer teardown() 68 | return m.Run() 69 | }(m) 70 | os.Exit(code) 71 | } 72 | 73 | func setup() (err error) { 74 | for filename, contents := range files { 75 | if err = os.MkdirAll(path.Dir(filename), 0700); nil != err { 76 | return 77 | } 78 | if err = ioutil.WriteFile( 79 | filename, 80 | []byte(contents), 81 | 0600, 82 | ); nil != err { 83 | return 84 | } 85 | } 86 | return 87 | } 88 | 89 | func teardown() (err error) { 90 | return os.RemoveAll("tmp") 91 | } 92 | 93 | func TestSetMinimumTLSVersion(t *testing.T) { 94 | testCases := []struct { 95 | name string 96 | value uint16 97 | expected uint16 98 | }{ 99 | {"Too low", tls.VersionTLS10 - 1, tls.VersionTLS10}, 100 | {"Lower bounds", tls.VersionTLS10, tls.VersionTLS10}, 101 | {"Upper bounds", tls.VersionTLS13, tls.VersionTLS13}, 102 | {"Too high", tls.VersionTLS13 + 1, tls.VersionTLS13}, 103 | } 104 | 105 | for _, tc := range testCases { 106 | t.Run(tc.name, func(t *testing.T) { 107 | SetMinimumTLSVersion(tc.value) 108 | if tc.expected != minTLSVersion { 109 | t.Errorf("Expected %d but got %d", tc.expected, minTLSVersion) 110 | } 111 | }) 112 | } 113 | } 114 | 115 | func TestWithReferrers(t *testing.T) { 116 | forbidden := http.StatusForbidden 117 | 118 | ok1 := "http://valid.com" 119 | ok2 := "https://valid.com" 120 | ok3 := "http://localhost" 121 | bad := "http://other.pl" 122 | 123 | var noRefer []string 124 | emptyRefer := []string{} 125 | onlyNoRefer := []string{""} 126 | refer := []string{ok1, ok2, ok3} 127 | noWithRefer := []string{"", ok1, ok2, ok3} 128 | 129 | testCases := []struct { 130 | name string 131 | refers []string 132 | refer string 133 | code int 134 | }{ 135 | {"Nil refer list", noRefer, bad, ok}, 136 | {"Empty refer list", emptyRefer, bad, ok}, 137 | {"Unassigned allowed & unassigned", onlyNoRefer, "", ok}, 138 | {"Unassigned allowed & assigned", onlyNoRefer, bad, forbidden}, 139 | {"Whitelist with unassigned", refer, "", forbidden}, 140 | {"Whitelist with bad", refer, bad, forbidden}, 141 | {"Whitelist with ok1", refer, ok1, ok}, 142 | {"Whitelist with ok2", refer, ok2, ok}, 143 | {"Whitelist with ok3", refer, ok3, ok}, 144 | {"Whitelist and none with unassigned", noWithRefer, "", ok}, 145 | {"Whitelist with bad", noWithRefer, bad, forbidden}, 146 | {"Whitelist with ok1", noWithRefer, ok1, ok}, 147 | {"Whitelist with ok2", noWithRefer, ok2, ok}, 148 | {"Whitelist with ok3", noWithRefer, ok3, ok}, 149 | } 150 | 151 | success := func(w http.ResponseWriter, r *http.Request, name string) { 152 | defer r.Body.Close() 153 | w.WriteHeader(ok) 154 | } 155 | 156 | for _, tc := range testCases { 157 | t.Run(tc.name, func(t *testing.T) { 158 | handler := WithReferrers(success, tc.refers) 159 | 160 | fullpath := "http://localhost/" + tmpIndexName 161 | req := httptest.NewRequest("GET", fullpath, nil) 162 | req.Header.Add("Referer", tc.refer) 163 | w := httptest.NewRecorder() 164 | 165 | handler(w, req, "") 166 | 167 | resp := w.Result() 168 | _, err := ioutil.ReadAll(resp.Body) 169 | if nil != err { 170 | t.Errorf("While reading body got %v", err) 171 | } 172 | if tc.code != resp.StatusCode { 173 | t.Errorf( 174 | "With referer '%s' in '%v' expected status code %d but got %d", 175 | tc.refer, tc.refers, tc.code, resp.StatusCode, 176 | ) 177 | } 178 | }) 179 | } 180 | } 181 | 182 | func TestBasicWithAndWithoutLogging(t *testing.T) { 183 | referer := "http://localhost" 184 | noReferer := "" 185 | testCases := []struct { 186 | name string 187 | path string 188 | code int 189 | refer string 190 | contents string 191 | }{ 192 | {"Good base dir", "", ok, referer, tmpIndex}, 193 | {"Good base index", tmpIndexName, redirect, referer, nothing}, 194 | {"Good base file", tmpFileName, ok, referer, tmpFile}, 195 | {"Bad base file", tmpBadName, missing, referer, notFound}, 196 | {"Good subdir dir", subDir, ok, referer, tmpSubIndex}, 197 | {"Good subdir index", tmpSubIndexName, redirect, referer, nothing}, 198 | {"Good subdir file", tmpSubFileName, ok, referer, tmpSubFile}, 199 | {"Good base dir", "", ok, noReferer, tmpIndex}, 200 | {"Good base index", tmpIndexName, redirect, noReferer, nothing}, 201 | {"Good base file", tmpFileName, ok, noReferer, tmpFile}, 202 | {"Bad base file", tmpBadName, missing, noReferer, notFound}, 203 | {"Good subdir dir", subDir, ok, noReferer, tmpSubIndex}, 204 | {"Good subdir index", tmpSubIndexName, redirect, noReferer, nothing}, 205 | {"Good subdir file", tmpSubFileName, ok, noReferer, tmpSubFile}, 206 | } 207 | 208 | for _, serveFile := range serveFileFuncs { 209 | handler := Basic(serveFile, baseDir) 210 | for _, tc := range testCases { 211 | t.Run(tc.name, func(t *testing.T) { 212 | fullpath := "http://localhost/" + tc.path 213 | req := httptest.NewRequest("GET", fullpath, nil) 214 | req.Header.Add("Referer", tc.refer) 215 | w := httptest.NewRecorder() 216 | 217 | handler(w, req) 218 | 219 | resp := w.Result() 220 | body, err := ioutil.ReadAll(resp.Body) 221 | if nil != err { 222 | t.Errorf("While reading body got %v", err) 223 | } 224 | contents := string(body) 225 | if tc.code != resp.StatusCode { 226 | t.Errorf( 227 | "While retrieving %s expected status code of %d but got %d", 228 | fullpath, tc.code, resp.StatusCode, 229 | ) 230 | } 231 | if tc.contents != contents { 232 | t.Errorf( 233 | "While retrieving %s expected contents '%s' but got '%s'", 234 | fullpath, tc.contents, contents, 235 | ) 236 | } 237 | }) 238 | } 239 | } 240 | } 241 | 242 | func TestPrefix(t *testing.T) { 243 | prefix := "/my/prefix/path/" 244 | 245 | testCases := []struct { 246 | name string 247 | path string 248 | code int 249 | contents string 250 | }{ 251 | {"Good base dir", prefix, ok, tmpIndex}, 252 | {"Good base index", prefix + tmpIndexName, redirect, nothing}, 253 | {"Good base file", prefix + tmpFileName, ok, tmpFile}, 254 | {"Bad base file", prefix + tmpBadName, missing, notFound}, 255 | {"Good subdir dir", prefix + subDir, ok, tmpSubIndex}, 256 | {"Good subdir index", prefix + tmpSubIndexName, redirect, nothing}, 257 | {"Good subdir file", prefix + tmpSubFileName, ok, tmpSubFile}, 258 | {"Unknown prefix", tmpFileName, missing, notFound}, 259 | } 260 | 261 | for _, serveFile := range serveFileFuncs { 262 | handler := Prefix(serveFile, baseDir, prefix) 263 | for _, tc := range testCases { 264 | t.Run(tc.name, func(t *testing.T) { 265 | fullpath := "http://localhost" + tc.path 266 | req := httptest.NewRequest("GET", fullpath, nil) 267 | w := httptest.NewRecorder() 268 | 269 | handler(w, req) 270 | 271 | resp := w.Result() 272 | body, err := ioutil.ReadAll(resp.Body) 273 | if nil != err { 274 | t.Errorf("While reading body got %v", err) 275 | } 276 | contents := string(body) 277 | if tc.code != resp.StatusCode { 278 | t.Errorf( 279 | "While retrieving %s expected status code of %d but got %d", 280 | fullpath, tc.code, resp.StatusCode, 281 | ) 282 | } 283 | if tc.contents != contents { 284 | t.Errorf( 285 | "While retrieving %s expected contents '%s' but got '%s'", 286 | fullpath, tc.contents, contents, 287 | ) 288 | } 289 | }) 290 | } 291 | } 292 | } 293 | 294 | func TestIgnoreIndex(t *testing.T) { 295 | testCases := []struct { 296 | name string 297 | path string 298 | code int 299 | contents string 300 | }{ 301 | {"Good base dir", "", missing, notFound}, 302 | {"Good base index", tmpIndexName, redirect, nothing}, 303 | {"Good base file", tmpFileName, ok, tmpFile}, 304 | {"Bad base file", tmpBadName, missing, notFound}, 305 | {"Good subdir dir", subDir, missing, notFound}, 306 | {"Good subdir index", tmpSubIndexName, redirect, nothing}, 307 | {"Good subdir file", tmpSubFileName, ok, tmpSubFile}, 308 | } 309 | 310 | for _, serveFile := range serveFileFuncs { 311 | handler := IgnoreIndex(Basic(serveFile, baseDir)) 312 | for _, tc := range testCases { 313 | t.Run(tc.name, func(t *testing.T) { 314 | fullpath := "http://localhost/" + tc.path 315 | req := httptest.NewRequest("GET", fullpath, nil) 316 | w := httptest.NewRecorder() 317 | 318 | handler(w, req) 319 | 320 | resp := w.Result() 321 | body, err := ioutil.ReadAll(resp.Body) 322 | if nil != err { 323 | t.Errorf("While reading body got %v", err) 324 | } 325 | contents := string(body) 326 | if tc.code != resp.StatusCode { 327 | t.Errorf( 328 | "While retrieving %s expected status code of %d but got %d", 329 | fullpath, tc.code, resp.StatusCode, 330 | ) 331 | } 332 | if tc.contents != contents { 333 | t.Errorf( 334 | "While retrieving %s expected contents '%s' but got '%s'", 335 | fullpath, tc.contents, contents, 336 | ) 337 | } 338 | }) 339 | } 340 | } 341 | } 342 | 343 | func TestPreventListings(t *testing.T) { 344 | testCases := []struct { 345 | name string 346 | path string 347 | code int 348 | contents string 349 | }{ 350 | {"Good base dir", "", ok, tmpIndex}, 351 | {"Good base index", tmpIndexName, redirect, nothing}, 352 | {"Good base file", tmpFileName, ok, tmpFile}, 353 | {"Bad base file", tmpBadName, missing, notFound}, 354 | {"Good subdir dir", subDir, ok, tmpSubIndex}, 355 | {"Good subdir index", tmpSubIndexName, redirect, nothing}, 356 | {"Good subdir file", tmpSubFileName, ok, tmpSubFile}, 357 | {"Dir without index", tmpNoIndexDir, missing, notFound}, 358 | } 359 | 360 | for _, serveFile := range serveFileFuncs { 361 | handler := PreventListings(Basic(serveFile, baseDir), baseDir, "") 362 | for _, tc := range testCases { 363 | t.Run(tc.name, func(t *testing.T) { 364 | fullpath := "http://localhost/" + tc.path 365 | req := httptest.NewRequest("GET", fullpath, nil) 366 | w := httptest.NewRecorder() 367 | 368 | handler(w, req) 369 | 370 | resp := w.Result() 371 | body, err := ioutil.ReadAll(resp.Body) 372 | if nil != err { 373 | t.Errorf("While reading body got %v", err) 374 | } 375 | contents := string(body) 376 | if tc.code != resp.StatusCode { 377 | t.Errorf( 378 | "While retrieving %s expected status code of %d but got %d", 379 | fullpath, tc.code, resp.StatusCode, 380 | ) 381 | } 382 | if tc.contents != contents { 383 | t.Errorf( 384 | "While retrieving %s expected contents '%s' but got '%s'", 385 | fullpath, tc.contents, contents, 386 | ) 387 | } 388 | }) 389 | } 390 | } 391 | } 392 | 393 | func TestAddAccessKey(t *testing.T) { 394 | // Prepare testing data. 395 | accessKey := "my-access-key" 396 | 397 | code := func(path, key string) string { 398 | data := []byte("/" + path + key) 399 | fmt.Printf("TEST: '%s'\n", data) 400 | return fmt.Sprintf("%X", md5.Sum(data)) 401 | } 402 | 403 | // Define test cases. 404 | testCases := []struct { 405 | name string 406 | path string 407 | key string 408 | value string 409 | code int 410 | contents string 411 | }{ 412 | { 413 | "Good base file with code", tmpFileName, 414 | "code", code(tmpFileName, accessKey), 415 | ok, tmpFile, 416 | }, 417 | { 418 | "Good base file with key", tmpFileName, 419 | "key", accessKey, 420 | ok, tmpFile, 421 | }, 422 | { 423 | "Bad base file with code", tmpBadName, 424 | "code", code(tmpBadName, accessKey), 425 | missing, notFound, 426 | }, 427 | { 428 | "Bad base file with key", tmpBadName, 429 | "key", accessKey, 430 | missing, notFound, 431 | }, 432 | { 433 | "Good base file with no code or key", tmpFileName, 434 | "my", "value", 435 | missing, notFound, 436 | }, 437 | { 438 | "Good base file with bad code", tmpFileName, 439 | "code", code(tmpFileName, "bad-access-key"), 440 | missing, notFound, 441 | }, 442 | { 443 | "Good base file with bad key", tmpFileName, 444 | "key", "bad-access-key", 445 | missing, notFound, 446 | }, 447 | } 448 | 449 | for _, serveFile := range serveFileFuncs { 450 | handler := AddAccessKey(Basic(serveFile, baseDir), accessKey) 451 | for _, tc := range testCases { 452 | t.Run(tc.name, func(t *testing.T) { 453 | fullpath := fmt.Sprintf( 454 | "http://localhost/%s?%s=%s", 455 | tc.path, tc.key, tc.value, 456 | ) 457 | req := httptest.NewRequest("GET", fullpath, nil) 458 | w := httptest.NewRecorder() 459 | 460 | handler(w, req) 461 | 462 | resp := w.Result() 463 | body, err := ioutil.ReadAll(resp.Body) 464 | if nil != err { 465 | t.Errorf("While reading body got %v", err) 466 | } 467 | contents := string(body) 468 | if tc.code != resp.StatusCode { 469 | t.Errorf( 470 | "While retrieving %s expected status code of %d but got %d", 471 | fullpath, tc.code, resp.StatusCode, 472 | ) 473 | } 474 | if tc.contents != contents { 475 | t.Errorf( 476 | "While retrieving %s expected contents '%s' but got '%s'", 477 | fullpath, tc.contents, contents, 478 | ) 479 | } 480 | }) 481 | } 482 | } 483 | } 484 | 485 | func TestListening(t *testing.T) { 486 | // Choose values for testing. 487 | called := false 488 | testBinding := "host:port" 489 | testError := errors.New("random problem") 490 | 491 | // Create an empty placeholder router function. 492 | handler := func(http.ResponseWriter, *http.Request) {} 493 | 494 | // Override setHandler so that multiple calls to 'http.HandleFunc' doesn't 495 | // panic. 496 | setHandler = func(string, func(http.ResponseWriter, *http.Request)) {} 497 | 498 | // Override listenAndServe with a function with more introspection and 499 | // control than 'http.ListenAndServe'. 500 | listenAndServe = func( 501 | binding string, handler http.Handler, 502 | ) error { 503 | if testBinding != binding { 504 | t.Errorf( 505 | "While serving expected binding of %s but got %s", 506 | testBinding, binding, 507 | ) 508 | } 509 | called = !called 510 | if called { 511 | return nil 512 | } 513 | return testError 514 | } 515 | 516 | // Perform test. 517 | listener := Listening() 518 | if err := listener(testBinding, handler); nil != err { 519 | t.Errorf("While serving first expected nil error but got %v", err) 520 | } 521 | if err := listener(testBinding, handler); nil == err { 522 | t.Errorf( 523 | "While serving second got nil while expecting %v", testError, 524 | ) 525 | } 526 | } 527 | 528 | func TestTLSListening(t *testing.T) { 529 | // Choose values for testing. 530 | called := false 531 | testBinding := "host:port" 532 | testTLSCert := "test/file.pem" 533 | testTLSKey := "test/file.key" 534 | testError := errors.New("random problem") 535 | 536 | // Create an empty placeholder router function. 537 | handler := func(http.ResponseWriter, *http.Request) {} 538 | 539 | // Override setHandler so that multiple calls to 'http.HandleFunc' doesn't 540 | // panic. 541 | setHandler = func(string, func(http.ResponseWriter, *http.Request)) {} 542 | 543 | // Override listenAndServeTLS with a function with more introspection and 544 | // control than 'http.ListenAndServeTLS'. 545 | listenAndServeTLS = func( 546 | binding, tlsCert, tlsKey string, handler http.Handler, 547 | ) error { 548 | if testBinding != binding { 549 | t.Errorf( 550 | "While serving TLS expected binding of %s but got %s", 551 | testBinding, binding, 552 | ) 553 | } 554 | if testTLSCert != tlsCert { 555 | t.Errorf( 556 | "While serving TLS expected TLS cert of %s but got %s", 557 | testTLSCert, tlsCert, 558 | ) 559 | } 560 | if testTLSKey != tlsKey { 561 | t.Errorf( 562 | "While serving TLS expected TLS key of %s but got %s", 563 | testTLSKey, tlsKey, 564 | ) 565 | } 566 | called = !called 567 | if called { 568 | return nil 569 | } 570 | return testError 571 | } 572 | 573 | // Perform test. 574 | listener := TLSListening(testTLSCert, testTLSKey) 575 | if err := listener(testBinding, handler); nil != err { 576 | t.Errorf("While serving first TLS expected nil error but got %v", err) 577 | } 578 | if err := listener(testBinding, handler); nil == err { 579 | t.Errorf( 580 | "While serving second TLS got nil while expecting %v", testError, 581 | ) 582 | } 583 | } 584 | 585 | func TestValidReferrer(t *testing.T) { 586 | ok1 := "http://valid.com" 587 | ok2 := "https://valid.com" 588 | ok3 := "http://localhost" 589 | bad := "http://other.pl" 590 | 591 | var noRefer []string 592 | emptyRefer := []string{} 593 | onlyNoRefer := []string{""} 594 | refer := []string{ok1, ok2, ok3} 595 | noWithRefer := []string{"", ok1, ok2, ok3} 596 | 597 | testCases := []struct { 598 | name string 599 | refers []string 600 | refer string 601 | result bool 602 | }{ 603 | {"Nil refer list", noRefer, bad, true}, 604 | {"Empty refer list", emptyRefer, bad, true}, 605 | {"Unassigned allowed & unassigned", onlyNoRefer, "", true}, 606 | {"Unassigned allowed & assigned", onlyNoRefer, bad, false}, 607 | {"Whitelist with unassigned", refer, "", false}, 608 | {"Whitelist with bad", refer, bad, false}, 609 | {"Whitelist with ok1", refer, ok1, true}, 610 | {"Whitelist with ok2", refer, ok2, true}, 611 | {"Whitelist with ok3", refer, ok3, true}, 612 | {"Whitelist and none with unassigned", noWithRefer, "", true}, 613 | {"Whitelist with bad", noWithRefer, bad, false}, 614 | {"Whitelist with ok1", noWithRefer, ok1, true}, 615 | {"Whitelist with ok2", noWithRefer, ok2, true}, 616 | {"Whitelist with ok3", noWithRefer, ok3, true}, 617 | } 618 | 619 | for _, tc := range testCases { 620 | t.Run(tc.name, func(t *testing.T) { 621 | result := validReferrer(tc.refers, tc.refer) 622 | if result != tc.result { 623 | t.Errorf( 624 | "With referrers of '%v' and a value of '%s' expected %t but got %t", 625 | tc.refers, tc.refer, tc.result, result, 626 | ) 627 | } 628 | }) 629 | } 630 | } 631 | 632 | func TestAddCorsWildcardHeaders(t *testing.T) { 633 | testCases := []struct { 634 | name string 635 | corsEnabled bool 636 | }{ 637 | {"CORS disabled", false}, 638 | {"CORS enabled", true}, 639 | } 640 | 641 | corsHeaders := map[string]string{ 642 | "Access-Control-Allow-Origin": "*", 643 | "Access-Control-Allow-Headers": "*", 644 | } 645 | 646 | for _, serveFile := range serveFileFuncs { 647 | for _, tc := range testCases { 648 | t.Run(tc.name, func(t *testing.T) { 649 | var handler http.HandlerFunc 650 | if tc.corsEnabled { 651 | handler = AddCorsWildcardHeaders(Basic(serveFile, baseDir)) 652 | } else { 653 | handler = Basic(serveFile, baseDir) 654 | } 655 | 656 | fullpath := "http://localhost/" + tmpFileName 657 | req := httptest.NewRequest("GET", fullpath, nil) 658 | w := httptest.NewRecorder() 659 | 660 | handler(w, req) 661 | 662 | resp := w.Result() 663 | body, err := ioutil.ReadAll(resp.Body) 664 | if nil != err { 665 | t.Errorf("While reading body got %v", err) 666 | } 667 | contents := string(body) 668 | if ok != resp.StatusCode { 669 | t.Errorf( 670 | "While retrieving %s expected status code of %d but got %d", 671 | fullpath, ok, resp.StatusCode, 672 | ) 673 | } 674 | if tmpFile != contents { 675 | t.Errorf( 676 | "While retrieving %s expected contents '%s' but got '%s'", 677 | fullpath, tmpFile, contents, 678 | ) 679 | } 680 | 681 | if tc.corsEnabled { 682 | for k, v := range corsHeaders { 683 | if v != resp.Header.Get(k) { 684 | t.Errorf( 685 | "With CORS enabled expect header '%s' to return '%s' but got '%s'", 686 | k, v, resp.Header.Get(k), 687 | ) 688 | } 689 | } 690 | } else { 691 | for k := range corsHeaders { 692 | if "" != resp.Header.Get(k) { 693 | t.Errorf( 694 | "With CORS disabled expected header '%s' to return '' but got '%s'", 695 | k, resp.Header.Get(k), 696 | ) 697 | } 698 | } 699 | } 700 | }) 701 | } 702 | } 703 | } 704 | --------------------------------------------------------------------------------