├── .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 |
--------------------------------------------------------------------------------