mirror of
https://github.com/halverneus/static-file-server.git
synced 2025-05-10 05:22:09 +00:00
Merge pull request #6 from halverneus/feature/refactor_and_unit_testing
Feature/refactor and unit testing
This commit is contained in:
commit
24c4662416
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
vendor/
|
||||||
|
|
21
Dockerfile
21
Dockerfile
@ -1,7 +1,20 @@
|
|||||||
FROM golang:latest as builder
|
FROM golang:1.10.3 as builder
|
||||||
COPY serve.go /
|
|
||||||
WORKDIR /
|
ENV BUILD_DIR /go/src/github.com/halverneus/static-file-server
|
||||||
RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o serve .
|
ENV MAIN github.com/halverneus/static-file-server/bin/serve
|
||||||
|
ENV DEP_VERSION v0.4.1
|
||||||
|
|
||||||
|
RUN curl -fsSL -o /usr/local/bin/dep \
|
||||||
|
https://github.com/golang/dep/releases/download/$DEP_VERSION/dep-linux-amd64 && \
|
||||||
|
chmod +x /usr/local/bin/dep
|
||||||
|
|
||||||
|
RUN mkdir -p ${BUILD_DIR}
|
||||||
|
WORKDIR ${BUILD_DIR}
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN dep ensure -vendor-only
|
||||||
|
RUN go test -race -cover ./...
|
||||||
|
RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o /serve ${MAIN}
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=builder /serve /
|
COPY --from=builder /serve /
|
||||||
|
15
Gopkg.lock
generated
Normal file
15
Gopkg.lock
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||||
|
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "gopkg.in/yaml.v2"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||||
|
version = "v2.2.1"
|
||||||
|
|
||||||
|
[solve-meta]
|
||||||
|
analyzer-name = "dep"
|
||||||
|
analyzer-version = 1
|
||||||
|
inputs-digest = "ce4cfa8aa3eb29f45e7ba341fdeac9820969e663181e81bddfc4a3aa2d5169bb"
|
||||||
|
solver-name = "gps-cdcl"
|
||||||
|
solver-version = 1
|
34
Gopkg.toml
Normal file
34
Gopkg.toml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Gopkg.toml example
|
||||||
|
#
|
||||||
|
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||||
|
# for detailed Gopkg.toml documentation.
|
||||||
|
#
|
||||||
|
# required = ["github.com/user/thing/cmd/thing"]
|
||||||
|
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||||
|
#
|
||||||
|
# [[constraint]]
|
||||||
|
# name = "github.com/user/project"
|
||||||
|
# version = "1.0.0"
|
||||||
|
#
|
||||||
|
# [[constraint]]
|
||||||
|
# name = "github.com/user/project2"
|
||||||
|
# branch = "dev"
|
||||||
|
# source = "github.com/myfork/project2"
|
||||||
|
#
|
||||||
|
# [[override]]
|
||||||
|
# name = "github.com/x/y"
|
||||||
|
# version = "2.4.0"
|
||||||
|
#
|
||||||
|
# [prune]
|
||||||
|
# non-go = false
|
||||||
|
# go-tests = true
|
||||||
|
# unused-packages = true
|
||||||
|
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "gopkg.in/yaml.v2"
|
||||||
|
version = "2.2.1"
|
||||||
|
|
||||||
|
[prune]
|
||||||
|
go-tests = true
|
||||||
|
unused-packages = true
|
25
README.md
25
README.md
@ -25,25 +25,40 @@ TLS_CERT=
|
|||||||
TLS_KEY=
|
TLS_KEY=
|
||||||
```
|
```
|
||||||
|
|
||||||
### Without Docker
|
Matching configuration YAML settings. YAML settings are individually overridden
|
||||||
|
by the corresponding environment variable. The following is an example
|
||||||
|
configuration file with defaults. Pass in the path to the configuration file
|
||||||
|
using the command line option ('-c', '-config', '--config').
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
host: ""
|
||||||
|
port: 8080
|
||||||
|
show-listing: true
|
||||||
|
folder: /web
|
||||||
|
url-prefix: ""
|
||||||
|
tls-cert: ""
|
||||||
|
tls-key: ""
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Without Docker
|
||||||
|
```bash
|
||||||
PORT=8888 FOLDER=. ./serve
|
PORT=8888 FOLDER=. ./serve
|
||||||
```
|
```
|
||||||
Files can then be accessed by going to http://localhost:8888/my/file.txt
|
Files can then be accessed by going to http://localhost:8888/my/file.txt
|
||||||
|
|
||||||
### With Docker
|
### With Docker
|
||||||
```
|
```bash
|
||||||
docker run -d -v /my/folder:/web -p 8080:8080 halverneus/static-file-server:latest
|
docker run -d -v /my/folder:/web -p 8080:8080 halverneus/static-file-server:latest
|
||||||
```
|
```
|
||||||
This will serve the folder "/my/folder" over http://localhost:9090/my/file.txt
|
This will serve the folder "/my/folder" over http://localhost:8080/my/file.txt
|
||||||
|
|
||||||
Any of the variables can also be modified:
|
Any of the variables can also be modified:
|
||||||
```
|
```bash
|
||||||
docker run -d -v /home/me/dev/source:/content/html -v /home/me/dev/files:/content/more/files -e FOLDER=/content -p 8080:8080 halverneus/static-file-server:latest
|
docker run -d -v /home/me/dev/source:/content/html -v /home/me/dev/files:/content/more/files -e FOLDER=/content -p 8080:8080 halverneus/static-file-server:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Also try...
|
### Also try...
|
||||||
```
|
```bash
|
||||||
./serve help
|
./serve help
|
||||||
# OR
|
# OR
|
||||||
docker run -it halverneus/static-file-server:latest help
|
docker run -it halverneus/static-file-server:latest help
|
||||||
|
13
bin/serve/main.go
Normal file
13
bin/serve/main.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/halverneus/static-file-server/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := cli.Execute(); nil != err {
|
||||||
|
log.Fatalf("Error: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
27
cli/args.go
Normal file
27
cli/args.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
// Args parsed from the command-line.
|
||||||
|
type Args []string
|
||||||
|
|
||||||
|
// Parse command-line arguments into Args. Value is returned to support daisy
|
||||||
|
// chaining.
|
||||||
|
func Parse(values []string) Args {
|
||||||
|
args := Args(values)
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches is used to determine if the arguments match the provided pattern.
|
||||||
|
func (args Args) Matches(pattern ...string) bool {
|
||||||
|
// If lengths don't match then nothing does.
|
||||||
|
if len(pattern) != len(args) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare slices using '*' as a wildcard.
|
||||||
|
for index, value := range pattern {
|
||||||
|
if "*" != value && value != args[index] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
81
cli/args_test.go
Normal file
81
cli/args_test.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
matches := func(args Args, orig []string) bool {
|
||||||
|
if nil == orig {
|
||||||
|
return nil == args
|
||||||
|
}
|
||||||
|
if len(orig) != len(args) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for index, value := range args {
|
||||||
|
if orig[index] != value {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
value []string
|
||||||
|
}{
|
||||||
|
{"Nil arguments", nil},
|
||||||
|
{"No arguments", []string{}},
|
||||||
|
{"Arguments", []string{"first", "second", "*"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if args := Parse(tc.value); !matches(args, tc.value) {
|
||||||
|
t.Errorf("Expected [%v] but got [%v]", tc.value, args)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatches(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
value []string
|
||||||
|
pattern []string
|
||||||
|
result bool
|
||||||
|
}{
|
||||||
|
{"Nil args and nil pattern", nil, nil, true},
|
||||||
|
{"No args and nil pattern", []string{}, nil, true},
|
||||||
|
{"Nil args and no pattern", nil, []string{}, true},
|
||||||
|
{"No args and no pattern", []string{}, []string{}, true},
|
||||||
|
{"Nil args and pattern", nil, []string{"test"}, false},
|
||||||
|
{"No args and pattern", []string{}, []string{"test"}, false},
|
||||||
|
{"Args and nil pattern", []string{"test"}, nil, false},
|
||||||
|
{"Args and no pattern", []string{"test"}, []string{}, false},
|
||||||
|
{"Simple single compare", []string{"test"}, []string{"test"}, true},
|
||||||
|
{"Simple double compare", []string{"one", "two"}, []string{"one", "two"}, true},
|
||||||
|
{"Bad single", []string{"one"}, []string{"two"}, false},
|
||||||
|
{"Bad double", []string{"one", "two"}, []string{"one", "owt"}, false},
|
||||||
|
{"Count mismatch", []string{"one", "two"}, []string{"one"}, false},
|
||||||
|
{"Nil args and wild", nil, []string{"*"}, false},
|
||||||
|
{"No args and wild", []string{}, []string{"*"}, false},
|
||||||
|
{"Single arg and wild", []string{"one"}, []string{"*"}, true},
|
||||||
|
{"Double arg and first wild", []string{"one", "two"}, []string{"*", "two"}, true},
|
||||||
|
{"Double arg and second wild", []string{"one", "two"}, []string{"one", "*"}, true},
|
||||||
|
{"Double arg and first wild mismatched", []string{"one", "two"}, []string{"*", "owt"}, false},
|
||||||
|
{"Double arg and second wild mismatched", []string{"one", "two"}, []string{"eno", "*"}, false},
|
||||||
|
{"Double arg and double wild", []string{"one", "two"}, []string{"*", "*"}, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
args := Parse(tc.value)
|
||||||
|
if resp := args.Matches(tc.pattern...); tc.result != resp {
|
||||||
|
msg := "For arguments [%v] matched to pattern [%v] expected " +
|
||||||
|
"%b but got %b"
|
||||||
|
t.Errorf(msg, tc.value, tc.pattern, tc.result, resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
95
cli/execute.go
Normal file
95
cli/execute.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/halverneus/static-file-server/cli/help"
|
||||||
|
"github.com/halverneus/static-file-server/cli/server"
|
||||||
|
"github.com/halverneus/static-file-server/cli/version"
|
||||||
|
"github.com/halverneus/static-file-server/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
option struct {
|
||||||
|
configFile string
|
||||||
|
helpFlag bool
|
||||||
|
versionFlag bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assignments used to simplify testing.
|
||||||
|
var (
|
||||||
|
selectRoutine = selectionRoutine
|
||||||
|
unknownArgsFunc = unknownArgs
|
||||||
|
runServerFunc = server.Run
|
||||||
|
runHelpFunc = help.Run
|
||||||
|
runVersionFunc = version.Run
|
||||||
|
loadConfig = config.Load
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
setupFlags()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupFlags() {
|
||||||
|
flag.StringVar(&option.configFile, "config", "", "")
|
||||||
|
flag.StringVar(&option.configFile, "c", "", "")
|
||||||
|
flag.BoolVar(&option.helpFlag, "help", false, "")
|
||||||
|
flag.BoolVar(&option.helpFlag, "h", false, "")
|
||||||
|
flag.BoolVar(&option.versionFlag, "version", false, "")
|
||||||
|
flag.BoolVar(&option.versionFlag, "v", false, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute CLI arguments.
|
||||||
|
func Execute() (err error) {
|
||||||
|
// Parse flag options, then parse commands arguments.
|
||||||
|
flag.Parse()
|
||||||
|
args := Parse(flag.Args())
|
||||||
|
|
||||||
|
job := selectRoutine(args)
|
||||||
|
return job()
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectionRoutine(args Args) func() error {
|
||||||
|
switch {
|
||||||
|
|
||||||
|
// serve help
|
||||||
|
// serve --help
|
||||||
|
// serve -h
|
||||||
|
case args.Matches("help") || option.helpFlag:
|
||||||
|
return runHelpFunc
|
||||||
|
|
||||||
|
// serve version
|
||||||
|
// serve --version
|
||||||
|
// serve -v
|
||||||
|
case args.Matches("version") || option.versionFlag:
|
||||||
|
return runVersionFunc
|
||||||
|
|
||||||
|
// serve
|
||||||
|
case args.Matches():
|
||||||
|
return withConfig(runServerFunc)
|
||||||
|
|
||||||
|
// Unknown arguments.
|
||||||
|
default:
|
||||||
|
return unknownArgsFunc(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unknownArgs(args Args) func() error {
|
||||||
|
return func() error {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unknown arguments provided [%v], try: 'help'",
|
||||||
|
args,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withConfig(routine func() error) func() error {
|
||||||
|
return func() (err error) {
|
||||||
|
if err = loadConfig(option.configFile); nil != err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return routine()
|
||||||
|
}
|
||||||
|
}
|
162
cli/execute_test.go
Normal file
162
cli/execute_test.go
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetupFlags(t *testing.T) {
|
||||||
|
app := os.Args[0]
|
||||||
|
|
||||||
|
file := "file.txt"
|
||||||
|
wConfig := "Config (file.txt)"
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
config string
|
||||||
|
help bool
|
||||||
|
version bool
|
||||||
|
}{
|
||||||
|
{"Empty args", []string{app}, "", false, false},
|
||||||
|
{"Help (--help)", []string{app, "--help"}, "", true, false},
|
||||||
|
{"Help (-help)", []string{app, "-help"}, "", true, false},
|
||||||
|
{"Help (-h)", []string{app, "-h"}, "", true, false},
|
||||||
|
{"Version (--version)", []string{app, "--version"}, "", false, true},
|
||||||
|
{"Version (-version)", []string{app, "-version"}, "", false, true},
|
||||||
|
{"Version (-v)", []string{app, "-v"}, "", false, true},
|
||||||
|
{"Config ()", []string{app, "--config", ""}, "", false, false},
|
||||||
|
{wConfig, []string{app, "--config", file}, file, false, false},
|
||||||
|
{wConfig, []string{app, "--config=file.txt"}, file, false, false},
|
||||||
|
{wConfig, []string{app, "-config", file}, file, false, false},
|
||||||
|
{wConfig, []string{app, "-config=file.txt"}, file, false, false},
|
||||||
|
{wConfig, []string{app, "-c", file}, file, false, false},
|
||||||
|
{"All set", []string{app, "-h", "-v", "-c", file}, file, true, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
reset := func() {
|
||||||
|
option.configFile = ""
|
||||||
|
option.helpFlag = false
|
||||||
|
option.versionFlag = false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
reset()
|
||||||
|
os.Args = tc.args
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if option.configFile != tc.config {
|
||||||
|
t.Errorf(
|
||||||
|
"For options [%v] expected a config file of %s but got %s",
|
||||||
|
tc.args, tc.config, option.configFile,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if option.helpFlag != tc.help {
|
||||||
|
t.Errorf(
|
||||||
|
"For options [%v] expected help flag of %t but got %t",
|
||||||
|
tc.args, tc.help, option.helpFlag,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if option.versionFlag != tc.version {
|
||||||
|
t.Errorf(
|
||||||
|
"For options [%v] expected version flag of %t but got %t",
|
||||||
|
tc.args, tc.version, option.versionFlag,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteAndSelection(t *testing.T) {
|
||||||
|
app := os.Args[0]
|
||||||
|
|
||||||
|
runHelpFuncError := errors.New("help")
|
||||||
|
runHelpFunc = func() error {
|
||||||
|
return runHelpFuncError
|
||||||
|
}
|
||||||
|
runVersionFuncError := errors.New("version")
|
||||||
|
runVersionFunc = func() error {
|
||||||
|
return runVersionFuncError
|
||||||
|
}
|
||||||
|
runServerFuncError := errors.New("server")
|
||||||
|
runServerFunc = func() error {
|
||||||
|
return runServerFuncError
|
||||||
|
}
|
||||||
|
unknownArgsFuncError := errors.New("unknown")
|
||||||
|
unknownArgsFunc = func(Args) func() error {
|
||||||
|
return func() error {
|
||||||
|
return unknownArgsFuncError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset := func() {
|
||||||
|
option.configFile = ""
|
||||||
|
option.helpFlag = false
|
||||||
|
option.versionFlag = false
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
result error
|
||||||
|
}{
|
||||||
|
{"Help", []string{app, "help"}, runHelpFuncError},
|
||||||
|
{"Help", []string{app, "--help"}, runHelpFuncError},
|
||||||
|
{"Version", []string{app, "version"}, runVersionFuncError},
|
||||||
|
{"Version", []string{app, "--version"}, runVersionFuncError},
|
||||||
|
{"Serve", []string{app}, runServerFuncError},
|
||||||
|
{"Unknown", []string{app, "unknown"}, unknownArgsFuncError},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
reset()
|
||||||
|
os.Args = tc.args
|
||||||
|
|
||||||
|
if err := Execute(); tc.result != err {
|
||||||
|
t.Errorf(
|
||||||
|
"Expected error for %v but got %v",
|
||||||
|
tc.result, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnknownArgs(t *testing.T) {
|
||||||
|
errFunc := unknownArgs(Args{"unknown"})
|
||||||
|
if err := errFunc(); nil == err {
|
||||||
|
t.Errorf(
|
||||||
|
"Expected a given unknown argument error but got %v",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithConfig(t *testing.T) {
|
||||||
|
configError := errors.New("config")
|
||||||
|
routineError := errors.New("routine")
|
||||||
|
routine := func() error { return routineError }
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
loadConfig func(string) error
|
||||||
|
result error
|
||||||
|
}{
|
||||||
|
{"Config error", func(string) error { return configError }, configError},
|
||||||
|
{"Routine error", func(string) error { return nil }, routineError},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
loadConfig = tc.loadConfig
|
||||||
|
errFunc := withConfig(routine)
|
||||||
|
if err := errFunc(); tc.result != err {
|
||||||
|
t.Errorf("Expected error %v but got %v", tc.result, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
135
cli/help/help.go
Normal file
135
cli/help/help.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package help
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run print operation.
|
||||||
|
func Run() error {
|
||||||
|
fmt.Println(Text)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Text for directly accessing help.
|
||||||
|
Text = `
|
||||||
|
NAME
|
||||||
|
static-file-server
|
||||||
|
|
||||||
|
SYNOPSIS
|
||||||
|
static-file-server
|
||||||
|
static-file-server [ -c | -config | --config ] /path/to/config.yml
|
||||||
|
static-file-server [ help | -help | --help ]
|
||||||
|
static-file-server [ version | -version | --version ]
|
||||||
|
|
||||||
|
DESCRIPTION
|
||||||
|
The Static File Server is intended to be a tiny, fast and simple solution
|
||||||
|
for serving files over HTTP. The features included are limited to make to
|
||||||
|
binding to a host name and port, selecting a folder to serve, choosing a
|
||||||
|
URL path prefix and selecting TLS certificates. If you want really awesome
|
||||||
|
reverse proxy features, I recommend Nginx.
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
None... not even libc!
|
||||||
|
|
||||||
|
ENVIRONMENT VARIABLES
|
||||||
|
FOLDER
|
||||||
|
The path to the folder containing the contents to be served over
|
||||||
|
HTTP(s). If not supplied, defaults to '/web' (for Docker reasons).
|
||||||
|
HOST
|
||||||
|
The hostname used for binding. If not supplied, contents will be served
|
||||||
|
to a client without regard for the hostname.
|
||||||
|
PORT
|
||||||
|
The port used for binding. If not supplied, defaults to port '8080'.
|
||||||
|
SHOW_LISTING
|
||||||
|
Automatically serve the index file for the directory if requested. For
|
||||||
|
example, if the client requests 'http://127.0.0.1/' the 'index.html'
|
||||||
|
file in the root of the directory being served is returned. If the value
|
||||||
|
is set to 'false', the same request will return a 'NOT FOUND'. Default
|
||||||
|
value is 'true'.
|
||||||
|
TLS_CERT
|
||||||
|
Path to the TLS certificate file to serve files using HTTPS. If supplied
|
||||||
|
then TLS_KEY must also be supplied. If not supplied, contents will be
|
||||||
|
served via HTTP.
|
||||||
|
TLS_KEY
|
||||||
|
Path to the TLS key file to serve files using HTTPS. If supplied then
|
||||||
|
TLS_CERT must also be supplied. If not supplied, contents will be served
|
||||||
|
via HTTPS
|
||||||
|
URL_PREFIX
|
||||||
|
The prefix to use in the URL path. If supplied, then the prefix must
|
||||||
|
start with a forward-slash and NOT end with a forward-slash. If not
|
||||||
|
supplied then no prefix is used.
|
||||||
|
|
||||||
|
CONFIGURATION FILE
|
||||||
|
Configuration can also managed used a YAML configuration file. To select the
|
||||||
|
configuration values using the YAML file, pass in the path to the file using
|
||||||
|
the appropriate flags (-c, --config). Environment variables take priority
|
||||||
|
over the configuration file. The following is an example configuration using
|
||||||
|
the default values.
|
||||||
|
|
||||||
|
Example config.yml with defaults:
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
folder: /web
|
||||||
|
host: ""
|
||||||
|
port: 8080
|
||||||
|
show-listing: true
|
||||||
|
tls-cert: ""
|
||||||
|
tls-key: ""
|
||||||
|
url-prefix: ""
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
USAGE
|
||||||
|
FILE LAYOUT
|
||||||
|
/var/www/sub/my.file
|
||||||
|
/var/www/index.html
|
||||||
|
|
||||||
|
COMMAND
|
||||||
|
export FOLDER=/var/www/sub
|
||||||
|
static-file-server
|
||||||
|
Retrieve with: wget http://localhost:8080/my.file
|
||||||
|
wget http://my.machine:8080/my.file
|
||||||
|
|
||||||
|
export FOLDER=/var/www
|
||||||
|
export HOST=my.machine
|
||||||
|
export PORT=80
|
||||||
|
static-file-server
|
||||||
|
Retrieve with: wget http://my.machine/sub/my.file
|
||||||
|
|
||||||
|
export FOLDER=/var/www
|
||||||
|
static-file-server -c config.yml
|
||||||
|
Result: Runs with values from config.yml, but with the folder being
|
||||||
|
served overridden by the FOLDER environment variable.
|
||||||
|
|
||||||
|
export FOLDER=/var/www/sub
|
||||||
|
export HOST=my.machine
|
||||||
|
export PORT=80
|
||||||
|
export URL_PREFIX=/my/stuff
|
||||||
|
static-file-server
|
||||||
|
Retrieve with: wget http://my.machine/my/stuff/my.file
|
||||||
|
|
||||||
|
export FOLDER=/var/www/sub
|
||||||
|
export TLS_CERT=/etc/server/my.machine.crt
|
||||||
|
export TLS_KEY=/etc/server/my.machine.key
|
||||||
|
static-file-server
|
||||||
|
Retrieve with: wget https://my.machine:8080/my.file
|
||||||
|
|
||||||
|
export FOLDER=/var/www/sub
|
||||||
|
export PORT=443
|
||||||
|
export TLS_CERT=/etc/server/my.machine.crt
|
||||||
|
export TLS_KEY=/etc/server/my.machine.key
|
||||||
|
static-file-server
|
||||||
|
Retrieve with: wget https://my.machine/my.file
|
||||||
|
|
||||||
|
export FOLDER=/var/www
|
||||||
|
export PORT=80
|
||||||
|
export SHOW_LISTING=true # Default behavior
|
||||||
|
static-file-server
|
||||||
|
Retrieve 'index.html' with: wget http://my.machine/
|
||||||
|
|
||||||
|
export FOLDER=/var/www
|
||||||
|
export PORT=80
|
||||||
|
export SHOW_LISTING=false
|
||||||
|
static-file-server
|
||||||
|
Returns 'NOT FOUND': wget http://my.machine/
|
||||||
|
`
|
||||||
|
)
|
9
cli/help/help_test.go
Normal file
9
cli/help/help_test.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package help
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestRun(t *testing.T) {
|
||||||
|
if err := Run(); nil != err {
|
||||||
|
t.Errorf("While running help got %v", err)
|
||||||
|
}
|
||||||
|
}
|
61
cli/server/server.go
Normal file
61
cli/server/server.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/halverneus/static-file-server/config"
|
||||||
|
"github.com/halverneus/static-file-server/handle"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Values to be overridden to simplify unit testing.
|
||||||
|
selectHandler = handlerSelector
|
||||||
|
selectListener = listenerSelector
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run server.
|
||||||
|
func Run() error {
|
||||||
|
// Choose and set the appropriate, optimized static file serving function.
|
||||||
|
handler := selectHandler()
|
||||||
|
|
||||||
|
// Serve files over HTTP or HTTPS based on paths to TLS files being
|
||||||
|
// provided.
|
||||||
|
listener := selectListener()
|
||||||
|
|
||||||
|
binding := fmt.Sprintf("%s:%d", config.Get.Host, config.Get.Port)
|
||||||
|
return listener(binding, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlerSelector returns the appropriate request handler based on
|
||||||
|
// configuration.
|
||||||
|
func handlerSelector() (handler http.HandlerFunc) {
|
||||||
|
// Choose and set the appropriate, optimized static file serving function.
|
||||||
|
if 0 == len(config.Get.URLPrefix) {
|
||||||
|
handler = handle.Basic(config.Get.Folder)
|
||||||
|
} else {
|
||||||
|
handler = handle.Prefix(config.Get.Folder, config.Get.URLPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine whether index files should hidden.
|
||||||
|
if !config.Get.ShowListing {
|
||||||
|
handler = handle.IgnoreIndex(handler)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// listenerSelector returns the appropriate listener handler based on
|
||||||
|
// configuration.
|
||||||
|
func listenerSelector() (listener handle.ListenerFunc) {
|
||||||
|
// Serve files over HTTP or HTTPS based on paths to TLS files being
|
||||||
|
// provided.
|
||||||
|
if 0 < len(config.Get.TLSCert) {
|
||||||
|
listener = handle.TLSListening(
|
||||||
|
config.Get.TLSCert,
|
||||||
|
config.Get.TLSKey,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listener = handle.Listening()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
74
cli/server/server_test.go
Normal file
74
cli/server/server_test.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/halverneus/static-file-server/config"
|
||||||
|
"github.com/halverneus/static-file-server/handle"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRun(t *testing.T) {
|
||||||
|
listenerError := errors.New("listener")
|
||||||
|
selectListener = func() handle.ListenerFunc {
|
||||||
|
return func(string, http.HandlerFunc) error {
|
||||||
|
return listenerError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Run(); listenerError != err {
|
||||||
|
t.Errorf("Expected %v but got %v", listenerError, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandlerSelector(t *testing.T) {
|
||||||
|
// This test only exercises function branches.
|
||||||
|
testFolder := "/web"
|
||||||
|
testPrefix := "/url/prefix"
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
folder string
|
||||||
|
prefix string
|
||||||
|
listing bool
|
||||||
|
}{
|
||||||
|
{"Basic handler", testFolder, "", true},
|
||||||
|
{"Prefix handler", testFolder, testPrefix, true},
|
||||||
|
{"Basic and hide listing handler", testFolder, "", false},
|
||||||
|
{"Prefix and hide listing handler", testFolder, testPrefix, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
config.Get.Folder = tc.folder
|
||||||
|
config.Get.URLPrefix = tc.prefix
|
||||||
|
config.Get.ShowListing = tc.listing
|
||||||
|
|
||||||
|
handlerSelector()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListenerSelector(t *testing.T) {
|
||||||
|
// This test only exercises function branches.
|
||||||
|
testCert := "file.crt"
|
||||||
|
testKey := "file.key"
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
cert string
|
||||||
|
key string
|
||||||
|
}{
|
||||||
|
{"HTTP", "", ""},
|
||||||
|
{"HTTPS", testCert, testKey},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
config.Get.TLSCert = tc.cert
|
||||||
|
config.Get.TLSKey = tc.key
|
||||||
|
listenerSelector()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
35
cli/version/version.go
Normal file
35
cli/version/version.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run print operation.
|
||||||
|
func Run() error {
|
||||||
|
fmt.Printf("%s\n%s\n", Text, GoVersionText)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// MajorVersion of static-file-server.
|
||||||
|
MajorVersion = 1
|
||||||
|
|
||||||
|
// MinorVersion of static-file-server.
|
||||||
|
MinorVersion = 3
|
||||||
|
|
||||||
|
// FixVersion of static-file-server.
|
||||||
|
FixVersion = 0
|
||||||
|
|
||||||
|
// Text for directly accessing the static-file-server version.
|
||||||
|
Text = fmt.Sprintf(
|
||||||
|
"Version %d.%d.%d",
|
||||||
|
MajorVersion,
|
||||||
|
MinorVersion,
|
||||||
|
FixVersion,
|
||||||
|
)
|
||||||
|
|
||||||
|
// GoVersionText for directly accessing the version of the Go runtime
|
||||||
|
// compiled with the static-file-server.
|
||||||
|
GoVersionText = runtime.Version()
|
||||||
|
)
|
9
cli/version/version_test.go
Normal file
9
cli/version/version_test.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestVersion(t *testing.T) {
|
||||||
|
if err := Run(); nil != err {
|
||||||
|
t.Errorf("While running version got %v", err)
|
||||||
|
}
|
||||||
|
}
|
197
config/config.go
Normal file
197
config/config.go
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Get the desired configuration value.
|
||||||
|
Get struct {
|
||||||
|
Folder string `yaml:"folder"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port uint16 `yaml:"port"`
|
||||||
|
ShowListing bool `yaml:"show-listing"`
|
||||||
|
TLSCert string `yaml:"tls-cert"`
|
||||||
|
TLSKey string `yaml:"tls-key"`
|
||||||
|
URLPrefix string `yaml:"url-prefix"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
folderKey = "FOLDER"
|
||||||
|
hostKey = "HOST"
|
||||||
|
portKey = "PORT"
|
||||||
|
showListingKey = "SHOW_LISTING"
|
||||||
|
tlsCertKey = "TLS_CERT"
|
||||||
|
tlsKeyKey = "TLS_KEY"
|
||||||
|
urlPrefixKey = "URL_PREFIX"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultFolder = "/web"
|
||||||
|
defaultHost = ""
|
||||||
|
defaultPort = uint16(8080)
|
||||||
|
defaultShowListing = true
|
||||||
|
defaultTLSCert = ""
|
||||||
|
defaultTLSKey = ""
|
||||||
|
defaultURLPrefix = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// init calls setDefaults to better support testing.
|
||||||
|
setDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDefaults() {
|
||||||
|
Get.Folder = defaultFolder
|
||||||
|
Get.Host = defaultHost
|
||||||
|
Get.Port = defaultPort
|
||||||
|
Get.ShowListing = defaultShowListing
|
||||||
|
Get.TLSCert = defaultTLSCert
|
||||||
|
Get.TLSKey = defaultTLSKey
|
||||||
|
Get.URLPrefix = defaultURLPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the configuration file.
|
||||||
|
func Load(filename string) (err error) {
|
||||||
|
// If no filename provided, assign envvars.
|
||||||
|
if "" == filename {
|
||||||
|
overrideWithEnvVars()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read contents from configuration file.
|
||||||
|
var contents []byte
|
||||||
|
if contents, err = ioutil.ReadFile(filename); nil != err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse contents into 'Get' configuration.
|
||||||
|
if err = yaml.Unmarshal(contents, &Get); nil != err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
overrideWithEnvVars()
|
||||||
|
return validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideWithEnvVars the default values and the configuration file values.
|
||||||
|
func overrideWithEnvVars() {
|
||||||
|
// Assign envvars, if set.
|
||||||
|
Get.Folder = envAsStr(folderKey, Get.Folder)
|
||||||
|
Get.Host = envAsStr(hostKey, Get.Host)
|
||||||
|
Get.Port = envAsUint16(portKey, Get.Port)
|
||||||
|
Get.ShowListing = envAsBool(showListingKey, Get.ShowListing)
|
||||||
|
Get.TLSCert = envAsStr(tlsCertKey, Get.TLSCert)
|
||||||
|
Get.TLSKey = envAsStr(tlsKeyKey, Get.TLSKey)
|
||||||
|
Get.URLPrefix = envAsStr(urlPrefixKey, Get.URLPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate the configuration.
|
||||||
|
func validate() error {
|
||||||
|
// If HTTPS is to be used, verify both TLS_* environment variables are set.
|
||||||
|
if 0 < len(Get.TLSCert) || 0 < len(Get.TLSKey) {
|
||||||
|
if 0 == len(Get.TLSCert) || 0 == len(Get.TLSKey) {
|
||||||
|
msg := "if value for either 'TLS_CERT' or 'TLS_KEY' is set then " +
|
||||||
|
"then value for the other must also be set (values are " +
|
||||||
|
"currently '%s' and '%s', respectively)"
|
||||||
|
return fmt.Errorf(msg, Get.TLSCert, Get.TLSKey)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(Get.TLSCert); nil != err {
|
||||||
|
msg := "value of TLS_CERT is set with filename '%s' that returns %v"
|
||||||
|
return fmt.Errorf(msg, err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(Get.TLSKey); nil != err {
|
||||||
|
msg := "value of TLS_KEY is set with filename '%s' that returns %v"
|
||||||
|
return fmt.Errorf(msg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the URL path prefix is to be used, verify it is properly formatted.
|
||||||
|
if 0 < len(Get.URLPrefix) &&
|
||||||
|
(!strings.HasPrefix(Get.URLPrefix, "/") || strings.HasSuffix(Get.URLPrefix, "/")) {
|
||||||
|
msg := "if value for 'URL_PREFIX' is set then the value must start " +
|
||||||
|
"with '/' and not end with '/' (current value of '%s' vs valid " +
|
||||||
|
"example of '/my/prefix'"
|
||||||
|
return fmt.Errorf(msg, Get.URLPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// envAsStr returns the value of the environment variable as a string if set.
|
||||||
|
func envAsStr(key, fallback string) string {
|
||||||
|
if value := os.Getenv(key); "" != value {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// envAsUint16 returns the value of the environment variable as a uint16 if set.
|
||||||
|
func envAsUint16(key string, fallback uint16) uint16 {
|
||||||
|
// Retrieve the string value of the environment variable. If not set,
|
||||||
|
// fallback is used.
|
||||||
|
valueStr := os.Getenv(key)
|
||||||
|
if "" == valueStr {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the string into a uint16.
|
||||||
|
base := 10
|
||||||
|
bitSize := 16
|
||||||
|
valueAsUint64, err := strconv.ParseUint(valueStr, base, bitSize)
|
||||||
|
if nil != err {
|
||||||
|
log.Printf(
|
||||||
|
"Invalid value for '%s': %v\nUsing fallback: %d",
|
||||||
|
key, err, fallback,
|
||||||
|
)
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return uint16(valueAsUint64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// envAsBool returns the value for an environment variable or, if not set, a
|
||||||
|
// fallback value as a boolean.
|
||||||
|
func envAsBool(key string, fallback bool) bool {
|
||||||
|
// Retrieve the string value of the environment variable. If not set,
|
||||||
|
// fallback is used.
|
||||||
|
valueStr := os.Getenv(key)
|
||||||
|
if "" == valueStr {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the string into a boolean.
|
||||||
|
value, err := strAsBool(valueStr)
|
||||||
|
if nil != err {
|
||||||
|
log.Printf(
|
||||||
|
"Invalid value for '%s': %v\nUsing fallback: %t",
|
||||||
|
key, err, fallback,
|
||||||
|
)
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// strAsBool converts the intent of the passed value into a boolean
|
||||||
|
// representation.
|
||||||
|
func strAsBool(value string) (result bool, err error) {
|
||||||
|
lvalue := strings.ToLower(value)
|
||||||
|
switch lvalue {
|
||||||
|
case "0", "false", "f", "no", "n":
|
||||||
|
result = false
|
||||||
|
case "1", "true", "t", "yes", "y":
|
||||||
|
result = true
|
||||||
|
default:
|
||||||
|
result = false
|
||||||
|
msg := "Unknown conversion from string to bool for value '%s'"
|
||||||
|
err = fmt.Errorf(msg, value)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
357
config/config_test.go
Normal file
357
config/config_test.go
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoad(t *testing.T) {
|
||||||
|
// Verify envvars are set.
|
||||||
|
testFolder := "/my/directory"
|
||||||
|
os.Setenv(folderKey, testFolder)
|
||||||
|
if err := Load(""); nil != err {
|
||||||
|
t.Errorf(
|
||||||
|
"While loading an empty file name expected no error but got %v",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if Get.Folder != testFolder {
|
||||||
|
t.Errorf(
|
||||||
|
"While loading an empty file name expected folder %s but got %s",
|
||||||
|
testFolder, Get.Folder,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify error if file doesn't exist.
|
||||||
|
if err := Load("/this/file/should/never/exist"); nil == err {
|
||||||
|
t.Error("While loading non-existing file expected error but got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify bad YAML returns an error.
|
||||||
|
func(t *testing.T) {
|
||||||
|
filename := "testing.tmp"
|
||||||
|
contents := []byte("{")
|
||||||
|
defer os.Remove(filename)
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(filename, contents, 0666); nil != err {
|
||||||
|
t.Errorf("Failed to save bad YAML file with: %v\n", err)
|
||||||
|
}
|
||||||
|
if err := Load(filename); nil == err {
|
||||||
|
t.Error("While loading bad YAML expected error but got nil")
|
||||||
|
}
|
||||||
|
}(t)
|
||||||
|
|
||||||
|
// Verify good YAML returns no error and sets value.
|
||||||
|
func(t *testing.T) {
|
||||||
|
filename := "testing.tmp"
|
||||||
|
testFolder := "/test/folder"
|
||||||
|
contents := []byte(fmt.Sprintf(
|
||||||
|
`{"folder": "%s"}`, testFolder,
|
||||||
|
))
|
||||||
|
defer os.Remove(filename)
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(filename, contents, 0666); nil != err {
|
||||||
|
t.Errorf("Failed to save good YAML file with: %v\n", err)
|
||||||
|
}
|
||||||
|
if err := Load(filename); nil != err {
|
||||||
|
t.Errorf(
|
||||||
|
"While loading good YAML expected nil but got %v",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverrideWithEnvvars(t *testing.T) {
|
||||||
|
// Choose values that are different than defaults.
|
||||||
|
testFolder := "/my/directory"
|
||||||
|
testHost := "apets.life"
|
||||||
|
testPort := uint16(666)
|
||||||
|
testShowListing := false
|
||||||
|
testTLSCert := "my.pem"
|
||||||
|
testTLSKey := "my.key"
|
||||||
|
testURLPrefix := "/url/prefix"
|
||||||
|
|
||||||
|
// Set all environment variables with test values.
|
||||||
|
os.Setenv(folderKey, testFolder)
|
||||||
|
os.Setenv(hostKey, testHost)
|
||||||
|
os.Setenv(portKey, strconv.Itoa(int(testPort)))
|
||||||
|
os.Setenv(showListingKey, fmt.Sprintf("%t", testShowListing))
|
||||||
|
os.Setenv(tlsCertKey, testTLSCert)
|
||||||
|
os.Setenv(tlsKeyKey, testTLSKey)
|
||||||
|
os.Setenv(urlPrefixKey, testURLPrefix)
|
||||||
|
|
||||||
|
// Verification functions.
|
||||||
|
equalStrings := func(t *testing.T, name, key, expected, result string) {
|
||||||
|
if expected != result {
|
||||||
|
t.Errorf(
|
||||||
|
"While checking %s for '%s' expected '%s' but got '%s'",
|
||||||
|
name, key, expected, result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
equalUint16 := func(t *testing.T, name, key string, expected, result uint16) {
|
||||||
|
if expected != result {
|
||||||
|
t.Errorf(
|
||||||
|
"While checking %s for '%s' expected %d but got %d",
|
||||||
|
name, key, expected, result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
equalBool := func(t *testing.T, name, key string, expected, result bool) {
|
||||||
|
if expected != result {
|
||||||
|
t.Errorf(
|
||||||
|
"While checking %s for '%s' expected %t but got %t",
|
||||||
|
name, key, expected, result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify defaults.
|
||||||
|
setDefaults()
|
||||||
|
phase := "defaults"
|
||||||
|
equalStrings(t, phase, folderKey, defaultFolder, Get.Folder)
|
||||||
|
equalStrings(t, phase, hostKey, defaultHost, Get.Host)
|
||||||
|
equalUint16(t, phase, portKey, defaultPort, Get.Port)
|
||||||
|
equalBool(t, phase, showListingKey, defaultShowListing, Get.ShowListing)
|
||||||
|
equalStrings(t, phase, tlsCertKey, defaultTLSCert, Get.TLSCert)
|
||||||
|
equalStrings(t, phase, tlsKeyKey, defaultTLSKey, Get.TLSKey)
|
||||||
|
equalStrings(t, phase, urlPrefixKey, defaultURLPrefix, Get.URLPrefix)
|
||||||
|
|
||||||
|
// Apply overrides.
|
||||||
|
overrideWithEnvVars()
|
||||||
|
|
||||||
|
// Verify overrides.
|
||||||
|
phase = "overrides"
|
||||||
|
equalStrings(t, phase, folderKey, testFolder, Get.Folder)
|
||||||
|
equalStrings(t, phase, hostKey, testHost, Get.Host)
|
||||||
|
equalUint16(t, phase, portKey, testPort, Get.Port)
|
||||||
|
equalBool(t, phase, showListingKey, testShowListing, Get.ShowListing)
|
||||||
|
equalStrings(t, phase, tlsCertKey, testTLSCert, Get.TLSCert)
|
||||||
|
equalStrings(t, phase, tlsKeyKey, testTLSKey, Get.TLSKey)
|
||||||
|
equalStrings(t, phase, urlPrefixKey, testURLPrefix, Get.URLPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
validPath := "config.go"
|
||||||
|
invalidPath := "should/never/exist.txt"
|
||||||
|
empty := ""
|
||||||
|
prefix := "/my/prefix"
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
cert string
|
||||||
|
key string
|
||||||
|
prefix string
|
||||||
|
isError bool
|
||||||
|
}{
|
||||||
|
{"Valid paths w/prefix", validPath, validPath, prefix, false},
|
||||||
|
{"Valid paths wo/prefix", validPath, validPath, empty, false},
|
||||||
|
{"Empty paths w/prefix", empty, empty, prefix, false},
|
||||||
|
{"Empty paths wo/prefix", empty, empty, empty, false},
|
||||||
|
{"Mixed paths w/prefix", empty, validPath, prefix, true},
|
||||||
|
{"Alt mixed paths w/prefix", validPath, empty, prefix, true},
|
||||||
|
{"Mixed paths wo/prefix", empty, validPath, empty, true},
|
||||||
|
{"Alt mixed paths wo/prefix", validPath, empty, empty, true},
|
||||||
|
{"Invalid cert w/prefix", invalidPath, validPath, prefix, true},
|
||||||
|
{"Invalid key w/prefix", validPath, invalidPath, prefix, true},
|
||||||
|
{"Invalid cert & key w/prefix", invalidPath, invalidPath, prefix, true},
|
||||||
|
{"Prefix missing leading /", empty, empty, "my/prefix", true},
|
||||||
|
{"Prefix with trailing /", empty, empty, "/my/prefix/", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
Get.TLSCert = tc.cert
|
||||||
|
Get.TLSKey = tc.key
|
||||||
|
Get.URLPrefix = tc.prefix
|
||||||
|
err := validate()
|
||||||
|
hasError := nil != err
|
||||||
|
if hasError && !tc.isError {
|
||||||
|
t.Errorf("Expected no error but got %v", err)
|
||||||
|
}
|
||||||
|
if !hasError && tc.isError {
|
||||||
|
t.Error("Expected an error but got no error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvAsStr(t *testing.T) {
|
||||||
|
sv := "STRING_VALUE"
|
||||||
|
fv := "FLOAT_VALUE"
|
||||||
|
iv := "INT_VALUE"
|
||||||
|
bv := "BOOL_VALUE"
|
||||||
|
ev := "EMPTY_VALUE"
|
||||||
|
uv := "UNSET_VALUE"
|
||||||
|
|
||||||
|
sr := "String Cheese" // String result
|
||||||
|
fr := "123.456" // Float result
|
||||||
|
ir := "-123" // Int result
|
||||||
|
br := "true" // Bool result
|
||||||
|
er := "" // Empty result
|
||||||
|
fbr := "fallback result" // Fallback result
|
||||||
|
efbr := "" // Empty fallback result
|
||||||
|
|
||||||
|
os.Setenv(sv, sr)
|
||||||
|
os.Setenv(fv, fr)
|
||||||
|
os.Setenv(iv, ir)
|
||||||
|
os.Setenv(bv, br)
|
||||||
|
os.Setenv(ev, er)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
fallback string
|
||||||
|
result string
|
||||||
|
}{
|
||||||
|
{"Good string", sv, fbr, sr},
|
||||||
|
{"Float string", fv, fbr, fr},
|
||||||
|
{"Int string", iv, fbr, ir},
|
||||||
|
{"Bool string", bv, fbr, br},
|
||||||
|
{"Empty string", ev, fbr, fbr},
|
||||||
|
{"Unset", uv, fbr, fbr},
|
||||||
|
{"Good string with empty fallback", sv, efbr, sr},
|
||||||
|
{"Unset with empty fallback", uv, efbr, efbr},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := envAsStr(tc.key, tc.fallback)
|
||||||
|
if tc.result != result {
|
||||||
|
t.Errorf(
|
||||||
|
"For %s with a '%s' fallback expected '%s' but got '%s'",
|
||||||
|
tc.key, tc.fallback, tc.result, result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvAsUint16(t *testing.T) {
|
||||||
|
ubv := "UPPER_BOUNDS_VALUE"
|
||||||
|
lbv := "LOWER_BOUNDS_VALUE"
|
||||||
|
hv := "HIGH_VALUE"
|
||||||
|
lv := "LOW_VALUE"
|
||||||
|
bv := "BOOL_VALUE"
|
||||||
|
sv := "STRING_VALUE"
|
||||||
|
uv := "UNSET_VALUE"
|
||||||
|
|
||||||
|
fbr := uint16(666) // Fallback result
|
||||||
|
ubr := uint16(65535) // Upper bounds result
|
||||||
|
lbr := uint16(0) // Lower bounds result
|
||||||
|
|
||||||
|
os.Setenv(ubv, "65535")
|
||||||
|
os.Setenv(lbv, "0")
|
||||||
|
os.Setenv(hv, "65536")
|
||||||
|
os.Setenv(lv, "-1")
|
||||||
|
os.Setenv(bv, "true")
|
||||||
|
os.Setenv(sv, "Cheese")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
fallback uint16
|
||||||
|
result uint16
|
||||||
|
}{
|
||||||
|
{"Upper bounds", ubv, fbr, ubr},
|
||||||
|
{"Lower bounds", lbv, fbr, lbr},
|
||||||
|
{"Out-of-bounds high", hv, fbr, fbr},
|
||||||
|
{"Out-of-bounds low", lv, fbr, fbr},
|
||||||
|
{"Boolean", bv, fbr, fbr},
|
||||||
|
{"String", sv, fbr, fbr},
|
||||||
|
{"Unset", uv, fbr, fbr},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := envAsUint16(tc.key, tc.fallback)
|
||||||
|
if tc.result != result {
|
||||||
|
t.Errorf(
|
||||||
|
"For %s with a %d fallback expected %d but got %d",
|
||||||
|
tc.key, tc.fallback, tc.result, result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvAsBool(t *testing.T) {
|
||||||
|
tv := "TRUE_VALUE"
|
||||||
|
fv := "FALSE_VALUE"
|
||||||
|
bv := "BAD_VALUE"
|
||||||
|
uv := "UNSET_VALUE"
|
||||||
|
|
||||||
|
os.Setenv(tv, "True")
|
||||||
|
os.Setenv(fv, "NO")
|
||||||
|
os.Setenv(bv, "BAD")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
fallback bool
|
||||||
|
result bool
|
||||||
|
}{
|
||||||
|
{"True with true fallback", tv, true, true},
|
||||||
|
{"True with false fallback", tv, false, true},
|
||||||
|
{"False with true fallback", fv, true, false},
|
||||||
|
{"False with false fallback", fv, false, false},
|
||||||
|
{"Bad with true fallback", bv, true, true},
|
||||||
|
{"Bad with false fallback", bv, false, false},
|
||||||
|
{"Unset with true fallback", uv, true, true},
|
||||||
|
{"Unset with false fallback", uv, false, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := envAsBool(tc.key, tc.fallback)
|
||||||
|
if tc.result != result {
|
||||||
|
t.Errorf(
|
||||||
|
"For %s with a %t fallback expected %t but got %t",
|
||||||
|
tc.key, tc.fallback, tc.result, result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStrAsBool(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
result bool
|
||||||
|
isError bool
|
||||||
|
}{
|
||||||
|
{"Empty value", "", false, true},
|
||||||
|
{"False value", "0", false, false},
|
||||||
|
{"True value", "1", true, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result, err := strAsBool(tc.value)
|
||||||
|
if result != tc.result {
|
||||||
|
t.Errorf(
|
||||||
|
"Expected %t for %s but got %t",
|
||||||
|
tc.result, tc.value, result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if tc.isError && nil == err {
|
||||||
|
t.Errorf(
|
||||||
|
"Expected error for %s but got no error",
|
||||||
|
tc.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if !tc.isError && nil != err {
|
||||||
|
t.Errorf(
|
||||||
|
"Expected no error for %s but got %v",
|
||||||
|
tc.value, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
71
handle/handle.go
Normal file
71
handle/handle.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package handle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// These assignments are for unit testing.
|
||||||
|
listenAndServe = http.ListenAndServe
|
||||||
|
listenAndServeTLS = http.ListenAndServeTLS
|
||||||
|
setHandler = http.HandleFunc
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
server http.Server
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListenerFunc accepts the {hostname:port} binding string required by HTTP
|
||||||
|
// listeners and the handler (router) function and returns any errors that
|
||||||
|
// occur.
|
||||||
|
type ListenerFunc func(string, http.HandlerFunc) error
|
||||||
|
|
||||||
|
// Basic file handler servers files from the passed folder.
|
||||||
|
func Basic(folder string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, folder+r.URL.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix file handler is an alternative to Basic where a URL prefix is removed
|
||||||
|
// prior to serving a file (http://my.machine/prefix/file.txt will serve
|
||||||
|
// file.txt from the root of the folder being served (ignoring 'prefix')).
|
||||||
|
func Prefix(folder, urlPrefix string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.HasPrefix(r.URL.Path, urlPrefix) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.ServeFile(w, r, folder+strings.TrimPrefix(r.URL.Path, urlPrefix))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoreIndex wraps an HTTP request. In the event of a folder root request,
|
||||||
|
// this function will automatically return 'NOT FOUND' as opposed to default
|
||||||
|
// behavior where the index file for that directory is retrieved.
|
||||||
|
func IgnoreIndex(serve http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serve(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listening function for serving the handler function.
|
||||||
|
func Listening() ListenerFunc {
|
||||||
|
return func(binding string, handler http.HandlerFunc) error {
|
||||||
|
setHandler("/", handler)
|
||||||
|
return listenAndServe(binding, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLSListening function for serving the handler function with encryption.
|
||||||
|
func TLSListening(tlsCert, tlsKey string) ListenerFunc {
|
||||||
|
return func(binding string, handler http.HandlerFunc) error {
|
||||||
|
setHandler("/", handler)
|
||||||
|
return listenAndServeTLS(binding, tlsCert, tlsKey, nil)
|
||||||
|
}
|
||||||
|
}
|
351
handle/handle_test.go
Normal file
351
handle/handle_test.go
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
package handle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseDir = "tmp/"
|
||||||
|
subDir = "sub/"
|
||||||
|
subDeepDir = "sub/deep/"
|
||||||
|
tmpIndexName = "index.html"
|
||||||
|
tmpFileName = "file.txt"
|
||||||
|
tmpBadName = "bad.txt"
|
||||||
|
tmpSubIndexName = "sub/index.html"
|
||||||
|
tmpSubFileName = "sub/file.txt"
|
||||||
|
tmpSubBadName = "sub/bad.txt"
|
||||||
|
tmpSubDeepIndexName = "sub/deep/index.html"
|
||||||
|
tmpSubDeepFileName = "sub/deep/file.txt"
|
||||||
|
tmpSubDeepBadName = "sub/deep/bad.txt"
|
||||||
|
|
||||||
|
tmpIndex = "Space: the final frontier"
|
||||||
|
tmpFile = "These are the voyages of the starship Enterprise."
|
||||||
|
tmpSubIndex = "Its continuing mission:"
|
||||||
|
tmpSubFile = "To explore strange new worlds"
|
||||||
|
tmpSubDeepIndex = "To seek out new life and new civilizations"
|
||||||
|
tmpSubDeepFile = "To boldly go where no one has gone before"
|
||||||
|
|
||||||
|
nothing = ""
|
||||||
|
ok = http.StatusOK
|
||||||
|
missing = http.StatusNotFound
|
||||||
|
redirect = http.StatusMovedPermanently
|
||||||
|
notFound = "404 page not found\n"
|
||||||
|
|
||||||
|
files = map[string]string{
|
||||||
|
baseDir + tmpIndexName: tmpIndex,
|
||||||
|
baseDir + tmpFileName: tmpFile,
|
||||||
|
baseDir + tmpSubIndexName: tmpSubIndex,
|
||||||
|
baseDir + tmpSubFileName: tmpSubFile,
|
||||||
|
baseDir + tmpSubDeepIndexName: tmpSubDeepIndex,
|
||||||
|
baseDir + tmpSubDeepFileName: tmpSubDeepFile,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
code := func(m *testing.M) int {
|
||||||
|
if err := setup(); nil != err {
|
||||||
|
log.Fatalf("While setting up test got: %v\n", err)
|
||||||
|
}
|
||||||
|
defer teardown()
|
||||||
|
return m.Run()
|
||||||
|
}(m)
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup() (err error) {
|
||||||
|
for filename, contents := range files {
|
||||||
|
if err = os.MkdirAll(path.Dir(filename), 0700); nil != err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = ioutil.WriteFile(
|
||||||
|
filename,
|
||||||
|
[]byte(contents),
|
||||||
|
0600,
|
||||||
|
); nil != err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func teardown() (err error) {
|
||||||
|
return os.RemoveAll("tmp")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasic(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
code int
|
||||||
|
contents string
|
||||||
|
}{
|
||||||
|
{"Good base dir", "", ok, tmpIndex},
|
||||||
|
{"Good base index", tmpIndexName, redirect, nothing},
|
||||||
|
{"Good base file", tmpFileName, ok, tmpFile},
|
||||||
|
{"Bad base file", tmpBadName, missing, notFound},
|
||||||
|
{"Good subdir dir", subDir, ok, tmpSubIndex},
|
||||||
|
{"Good subdir index", tmpSubIndexName, redirect, nothing},
|
||||||
|
{"Good subdir file", tmpSubFileName, ok, tmpSubFile},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := Basic(baseDir)
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
fullpath := "http://localhost/" + tc.path
|
||||||
|
req := httptest.NewRequest("GET", fullpath, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if nil != err {
|
||||||
|
t.Errorf("While reading body got %v", err)
|
||||||
|
}
|
||||||
|
contents := string(body)
|
||||||
|
if tc.code != resp.StatusCode {
|
||||||
|
t.Errorf(
|
||||||
|
"While retrieving %s expected status code of %d but got %d",
|
||||||
|
fullpath, tc.code, resp.StatusCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if tc.contents != contents {
|
||||||
|
t.Errorf(
|
||||||
|
"While retrieving %s expected contents '%s' but got '%s'",
|
||||||
|
fullpath, tc.contents, contents,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrefix(t *testing.T) {
|
||||||
|
prefix := "/my/prefix/path/"
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
code int
|
||||||
|
contents string
|
||||||
|
}{
|
||||||
|
{"Good base dir", prefix, ok, tmpIndex},
|
||||||
|
{"Good base index", prefix + tmpIndexName, redirect, nothing},
|
||||||
|
{"Good base file", prefix + tmpFileName, ok, tmpFile},
|
||||||
|
{"Bad base file", prefix + tmpBadName, missing, notFound},
|
||||||
|
{"Good subdir dir", prefix + subDir, ok, tmpSubIndex},
|
||||||
|
{"Good subdir index", prefix + tmpSubIndexName, redirect, nothing},
|
||||||
|
{"Good subdir file", prefix + tmpSubFileName, ok, tmpSubFile},
|
||||||
|
{"Unknown prefix", tmpFileName, missing, notFound},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := Prefix(baseDir, prefix)
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
fullpath := "http://localhost" + tc.path
|
||||||
|
req := httptest.NewRequest("GET", fullpath, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if nil != err {
|
||||||
|
t.Errorf("While reading body got %v", err)
|
||||||
|
}
|
||||||
|
contents := string(body)
|
||||||
|
if tc.code != resp.StatusCode {
|
||||||
|
t.Errorf(
|
||||||
|
"While retrieving %s expected status code of %d but got %d",
|
||||||
|
fullpath, tc.code, resp.StatusCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if tc.contents != contents {
|
||||||
|
t.Errorf(
|
||||||
|
"While retrieving %s expected contents '%s' but got '%s'",
|
||||||
|
fullpath, tc.contents, contents,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIgnoreIndex(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
code int
|
||||||
|
contents string
|
||||||
|
}{
|
||||||
|
{"Good base dir", "", missing, notFound},
|
||||||
|
{"Good base index", tmpIndexName, redirect, nothing},
|
||||||
|
{"Good base file", tmpFileName, ok, tmpFile},
|
||||||
|
{"Bad base file", tmpBadName, missing, notFound},
|
||||||
|
{"Good subdir dir", subDir, missing, notFound},
|
||||||
|
{"Good subdir index", tmpSubIndexName, redirect, nothing},
|
||||||
|
{"Good subdir file", tmpSubFileName, ok, tmpSubFile},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := IgnoreIndex(Basic(baseDir))
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
fullpath := "http://localhost/" + tc.path
|
||||||
|
req := httptest.NewRequest("GET", fullpath, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if nil != err {
|
||||||
|
t.Errorf("While reading body got %v", err)
|
||||||
|
}
|
||||||
|
contents := string(body)
|
||||||
|
if tc.code != resp.StatusCode {
|
||||||
|
t.Errorf(
|
||||||
|
"While retrieving %s expected status code of %d but got %d",
|
||||||
|
fullpath, tc.code, resp.StatusCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if tc.contents != contents {
|
||||||
|
t.Errorf(
|
||||||
|
"While retrieving %s expected contents '%s' but got '%s'",
|
||||||
|
fullpath, tc.contents, contents,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func TestIgnoreIndex(t *testing.T) {
|
||||||
|
// handler := IgnoreIndex(Basic("tmp"))
|
||||||
|
// testCases := []struct {
|
||||||
|
// name string
|
||||||
|
// path string
|
||||||
|
// code int
|
||||||
|
// contents string
|
||||||
|
// }{}
|
||||||
|
|
||||||
|
// // Build test cases for directories.
|
||||||
|
// var dirs []string
|
||||||
|
// for filename, contents := range files {
|
||||||
|
// dir := path.Dir(filename)
|
||||||
|
// found := false
|
||||||
|
// for _, other := range dirs {
|
||||||
|
// if other == dir {
|
||||||
|
// found = true
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if !found {
|
||||||
|
// dirs = append(dirs, dir)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
func TestListening(t *testing.T) {
|
||||||
|
// Choose values for testing.
|
||||||
|
called := false
|
||||||
|
testBinding := "host:port"
|
||||||
|
testError := errors.New("random problem")
|
||||||
|
|
||||||
|
// Create an empty placeholder router function.
|
||||||
|
handler := func(http.ResponseWriter, *http.Request) {}
|
||||||
|
|
||||||
|
// Override setHandler so that multiple calls to 'http.HandleFunc' doesn't
|
||||||
|
// panic.
|
||||||
|
setHandler = func(string, func(http.ResponseWriter, *http.Request)) {}
|
||||||
|
|
||||||
|
// Override listenAndServe with a function with more introspection and
|
||||||
|
// control than 'http.ListenAndServe'.
|
||||||
|
listenAndServe = func(
|
||||||
|
binding string, handler http.Handler,
|
||||||
|
) error {
|
||||||
|
if testBinding != binding {
|
||||||
|
t.Errorf(
|
||||||
|
"While serving expected binding of %s but got %s",
|
||||||
|
testBinding, binding,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
called = !called
|
||||||
|
if called {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return testError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform test.
|
||||||
|
listener := Listening()
|
||||||
|
if err := listener(testBinding, handler); nil != err {
|
||||||
|
t.Errorf("While serving first expected nil error but got %v", err)
|
||||||
|
}
|
||||||
|
if err := listener(testBinding, handler); nil == err {
|
||||||
|
t.Errorf(
|
||||||
|
"While serving second got nil while expecting %v", testError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTLSListening(t *testing.T) {
|
||||||
|
// Choose values for testing.
|
||||||
|
called := false
|
||||||
|
testBinding := "host:port"
|
||||||
|
testTLSCert := "test/file.pem"
|
||||||
|
testTLSKey := "test/file.key"
|
||||||
|
testError := errors.New("random problem")
|
||||||
|
|
||||||
|
// Create an empty placeholder router function.
|
||||||
|
handler := func(http.ResponseWriter, *http.Request) {}
|
||||||
|
|
||||||
|
// Override setHandler so that multiple calls to 'http.HandleFunc' doesn't
|
||||||
|
// panic.
|
||||||
|
setHandler = func(string, func(http.ResponseWriter, *http.Request)) {}
|
||||||
|
|
||||||
|
// Override listenAndServeTLS with a function with more introspection and
|
||||||
|
// control than 'http.ListenAndServeTLS'.
|
||||||
|
listenAndServeTLS = func(
|
||||||
|
binding, tlsCert, tlsKey string, handler http.Handler,
|
||||||
|
) error {
|
||||||
|
if testBinding != binding {
|
||||||
|
t.Errorf(
|
||||||
|
"While serving TLS expected binding of %s but got %s",
|
||||||
|
testBinding, binding,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if testTLSCert != tlsCert {
|
||||||
|
t.Errorf(
|
||||||
|
"While serving TLS expected TLS cert of %s but got %s",
|
||||||
|
testTLSCert, tlsCert,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if testTLSKey != tlsKey {
|
||||||
|
t.Errorf(
|
||||||
|
"While serving TLS expected TLS key of %s but got %s",
|
||||||
|
testTLSKey, tlsKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
called = !called
|
||||||
|
if called {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return testError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform test.
|
||||||
|
listener := TLSListening(testTLSCert, testTLSKey)
|
||||||
|
if err := listener(testBinding, handler); nil != err {
|
||||||
|
t.Errorf("While serving first TLS expected nil error but got %v", err)
|
||||||
|
}
|
||||||
|
if err := listener(testBinding, handler); nil == err {
|
||||||
|
t.Errorf(
|
||||||
|
"While serving second TLS got nil while expecting %v", testError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
245
serve.go
245
serve.go
@ -1,245 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
version = "Version 1.1"
|
|
||||||
|
|
||||||
help = `
|
|
||||||
NAME
|
|
||||||
static-file-server
|
|
||||||
|
|
||||||
SYNOPSIS
|
|
||||||
static-file-server
|
|
||||||
static-file-server [ help | -help | --help ]
|
|
||||||
static-file-server [ version | -version | --version ]
|
|
||||||
|
|
||||||
DESCRIPTION
|
|
||||||
The Static File Server is intended to be a tiny, fast and simple solution
|
|
||||||
for serving files over HTTP. The features included are limited to make to
|
|
||||||
binding to a host name and port, selecting a folder to serve, choosing a
|
|
||||||
URL path prefix and selecting TLS certificates. If you want really awesome
|
|
||||||
reverse proxy features, I recommend Nginx.
|
|
||||||
|
|
||||||
DEPENDENCIES
|
|
||||||
None... not even libc!
|
|
||||||
|
|
||||||
ENVIRONMENT VARIABLES
|
|
||||||
FOLDER
|
|
||||||
The path to the folder containing the contents to be served over
|
|
||||||
HTTP(s). If not supplied, defaults to '/web' (for Docker reasons).
|
|
||||||
HOST
|
|
||||||
The hostname used for binding. If not supplied, contents will be served
|
|
||||||
to a client without regard for the hostname.
|
|
||||||
PORT
|
|
||||||
The port used for binding. If not supplied, defaults to port '8080'.
|
|
||||||
SHOW_LISTING
|
|
||||||
Automatically serve the index file for the directory if requested. For
|
|
||||||
example, if the client requests 'http://127.0.0.1/' the 'index.html'
|
|
||||||
file in the root of the directory being served is returned. If the value
|
|
||||||
is set to 'false', the same request will return a 'NOT FOUND'. Default
|
|
||||||
value is 'true'.
|
|
||||||
TLS_CERT
|
|
||||||
Path to the TLS certificate file to serve files using HTTPS. If supplied
|
|
||||||
then TLS_KEY must also be supplied. If not supplied, contents will be
|
|
||||||
served via HTTP.
|
|
||||||
TLS_KEY
|
|
||||||
Path to the TLS key file to serve files using HTTPS. If supplied then
|
|
||||||
TLS_CERT must also be supplied. If not supplied, contents will be served
|
|
||||||
via HTTPS
|
|
||||||
URL_PREFIX
|
|
||||||
The prefix to use in the URL path. If supplied, then the prefix must
|
|
||||||
start with a forward-slash and NOT end with a forward-slash. If not
|
|
||||||
supplied then no prefix is used.
|
|
||||||
|
|
||||||
USAGE
|
|
||||||
FILE LAYOUT
|
|
||||||
/var/www/sub/my.file
|
|
||||||
/var/www/index.html
|
|
||||||
|
|
||||||
COMMAND
|
|
||||||
export FOLDER=/var/www/sub
|
|
||||||
static-file-server
|
|
||||||
Retrieve with: wget http://localhost:8080/my.file
|
|
||||||
wget http://my.machine:8080/my.file
|
|
||||||
|
|
||||||
export FOLDER=/var/www
|
|
||||||
export HOST=my.machine
|
|
||||||
export PORT=80
|
|
||||||
static-file-server
|
|
||||||
Retrieve with: wget http://my.machine/sub/my.file
|
|
||||||
|
|
||||||
export FOLDER=/var/www/sub
|
|
||||||
export HOST=my.machine
|
|
||||||
export PORT=80
|
|
||||||
export URL_PREFIX=/my/stuff
|
|
||||||
static-file-server
|
|
||||||
Retrieve with: wget http://my.machine/my/stuff/my.file
|
|
||||||
|
|
||||||
export FOLDER=/var/www/sub
|
|
||||||
export TLS_CERT=/etc/server/my.machine.crt
|
|
||||||
export TLS_KEY=/etc/server/my.machine.key
|
|
||||||
static-file-server
|
|
||||||
Retrieve with: wget https://my.machine:8080/my.file
|
|
||||||
|
|
||||||
export FOLDER=/var/www/sub
|
|
||||||
export PORT=443
|
|
||||||
export TLS_CERT=/etc/server/my.machine.crt
|
|
||||||
export TLS_KEY=/etc/server/my.machine.key
|
|
||||||
static-file-server
|
|
||||||
Retrieve with: wget https://my.machine/my.file
|
|
||||||
|
|
||||||
export FOLDER=/var/www
|
|
||||||
export PORT=80
|
|
||||||
export SHOW_LISTING=true # Default behavior
|
|
||||||
static-file-server
|
|
||||||
Retrieve 'index.html' with: wget http://my.machine/
|
|
||||||
|
|
||||||
export FOLDER=/var/www
|
|
||||||
export PORT=80
|
|
||||||
export SHOW_LISTING=false
|
|
||||||
static-file-server
|
|
||||||
Returns 'NOT FOUND': wget http://my.machine/
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Evaluate and execute subcommand if supplied.
|
|
||||||
if 1 < len(os.Args) {
|
|
||||||
arg := os.Args[1]
|
|
||||||
switch {
|
|
||||||
case strings.Contains(arg, "help"):
|
|
||||||
fmt.Println(help)
|
|
||||||
case strings.Contains(arg, "version"):
|
|
||||||
fmt.Println(version)
|
|
||||||
default:
|
|
||||||
name := os.Args[0]
|
|
||||||
log.Fatalf("Unknown argument: %s. Try '%s help'.", arg, name)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect environment variables.
|
|
||||||
folder := env("FOLDER", "/web") + "/"
|
|
||||||
host := env("HOST", "")
|
|
||||||
port := env("PORT", "8080")
|
|
||||||
showListing := envAsBool("SHOW_LISTING", true)
|
|
||||||
tlsCert := env("TLS_CERT", "")
|
|
||||||
tlsKey := env("TLS_KEY", "")
|
|
||||||
urlPrefix := env("URL_PREFIX", "")
|
|
||||||
|
|
||||||
// If HTTPS is to be used, verify both TLS_* environment variables are set.
|
|
||||||
if 0 < len(tlsCert) || 0 < len(tlsKey) {
|
|
||||||
if 0 == len(tlsCert) || 0 == len(tlsKey) {
|
|
||||||
log.Fatalln(
|
|
||||||
"If value for environment variable 'TLS_CERT' or 'TLS_KEY' is set " +
|
|
||||||
"then value for environment variable 'TLS_KEY' or 'TLS_CERT' must " +
|
|
||||||
"also be set.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the URL path prefix is to be used, verify it is properly formatted.
|
|
||||||
if 0 < len(urlPrefix) &&
|
|
||||||
(!strings.HasPrefix(urlPrefix, "/") || strings.HasSuffix(urlPrefix, "/")) {
|
|
||||||
log.Fatalln(
|
|
||||||
"Value for environment variable 'URL_PREFIX' must start " +
|
|
||||||
"with '/' and not end with '/'. Example: '/my/prefix'",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Choose and set the appropriate, optimized static file serving function.
|
|
||||||
var handler http.HandlerFunc
|
|
||||||
if 0 == len(urlPrefix) {
|
|
||||||
handler = handleListing(showListing, basicHandler(folder))
|
|
||||||
} else {
|
|
||||||
handler = handleListing(showListing, prefixHandler(folder, urlPrefix))
|
|
||||||
}
|
|
||||||
http.HandleFunc("/", handler)
|
|
||||||
|
|
||||||
// Serve files over HTTP or HTTPS based on paths to TLS files being provided.
|
|
||||||
if 0 == len(tlsCert) {
|
|
||||||
log.Fatalln(http.ListenAndServe(host+":"+port, nil))
|
|
||||||
} else {
|
|
||||||
log.Fatalln(http.ListenAndServeTLS(host+":"+port, tlsCert, tlsKey, nil))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleListing wraps an HTTP request. In the event of a folder root request,
|
|
||||||
// setting 'show' to false will automatically return 'NOT FOUND' whereas true
|
|
||||||
// will attempt to retrieve the index file of that directory.
|
|
||||||
func handleListing(show bool, serve http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !show && strings.HasSuffix(r.URL.Path, "/") {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
serve(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// basicHandler serves files from the folder passed.
|
|
||||||
func basicHandler(folder string) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.ServeFile(w, r, folder+r.URL.Path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// prefixHandler removes the URL path prefix before serving files from the
|
|
||||||
// folder passed.
|
|
||||||
func prefixHandler(folder, urlPrefix string) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !strings.HasPrefix(r.URL.Path, urlPrefix) {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.ServeFile(w, r, folder+strings.TrimPrefix(r.URL.Path, urlPrefix))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// env returns the value for an environment variable or, if not set, a fallback
|
|
||||||
// value.
|
|
||||||
func env(key, fallback string) string {
|
|
||||||
if value := os.Getenv(key); 0 < len(value) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
// strAsBool converts the intent of the passed value into a boolean
|
|
||||||
// representation.
|
|
||||||
func strAsBool(value string) (result bool, err error) {
|
|
||||||
lvalue := strings.ToLower(value)
|
|
||||||
switch lvalue {
|
|
||||||
case "0", "false", "f", "no", "n":
|
|
||||||
result = false
|
|
||||||
case "1", "true", "t", "yes", "y":
|
|
||||||
result = true
|
|
||||||
default:
|
|
||||||
result = false
|
|
||||||
msg := "Unknown conversion from string to bool for value '%s'"
|
|
||||||
err = fmt.Errorf(msg, value)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// envAsBool returns the value for an environment variable or, if not set, a
|
|
||||||
// fallback value as a boolean.
|
|
||||||
func envAsBool(key string, fallback bool) bool {
|
|
||||||
value := env(key, fmt.Sprintf("%t", fallback))
|
|
||||||
result, err := strAsBool(value)
|
|
||||||
if nil != err {
|
|
||||||
log.Printf(
|
|
||||||
"Invalid value for '%s': %v\nUsing fallback: %t",
|
|
||||||
key, err, fallback,
|
|
||||||
)
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user