commit 69e5688156aca3acbec8c121d025a9fab2752058 from: amacleod date: Sat Jan 20 18:53:47 2024 UTC Initial commit commit - /dev/null commit + 69e5688156aca3acbec8c121d025a9fab2752058 blob - /dev/null blob + 1c363d2841cdff3fecddc69509795cad4461aabf (mode 644) --- /dev/null +++ LICENCE @@ -0,0 +1,14 @@ +BSD Zero Clause License + +Copyright (c) Alisdair MacLeod + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. blob - /dev/null blob + b4fae62430f05b3185b8d0997ac89418fdecbcf2 (mode 644) --- /dev/null +++ README.md @@ -0,0 +1,13 @@ +A Web Server +============ + +This was a joke to be able to say my website ran on `aws`. + +Don't sue me. + +Also, don't use this. Use [Caddy](https://caddyserver.com/) instead, +unless you fit the very specific requirements met by this. + +It's overly "secure" and will only really work on static sites. + +For more information check the man page. \ No newline at end of file blob - /dev/null blob + 5facc20491a58c5de2abcfcc3ff24604eb3a7efe (mode 644) --- /dev/null +++ aws.1 @@ -0,0 +1,95 @@ +.\" Copyright (C) 2020 by Alisdair MacLeod +.\" +.\" Permission to use, copy, modify, and/or distribute this software for any purpose +.\" with or without fee is hereby granted. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +.\" REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +.\" AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +.\" INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +.\" LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +.\" OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +.\" PERFORMANCE OF THIS SOFTWARE. +.Dd July 2, 2020 +.Dt AWS 1 +.Os +.Sh NAME +.Nm aws +.Nd simple secure (-ish) static webserver +.Sh SYNOPSIS +.Nm +.Op Fl c Pa directory +.Ar hostname ... +.Sh DESCRIPTION +.Nm +serves the files and subdirectories of the directory from which it is run. +.Pp +TLS certificates will be automatically sourced from +.Lk https://letsencrypt.org/ "Let's Encrypt" +for all hostnames specified when +.Nm +is called. +.Pp +.Nm +sets the following HTTP headers for all responses: +.Bd -literal +"Content-Security-Policy": "default-src 'none'; style-src 'self'; img-src 'self'; object-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; plugin-types application/pdf" +"Referrer-Policy": "no-referrer" +"Strict-Transport-Security": "max-age=63072000; includeSubDomains" +"X-Content-Type-Options": "nosniff" +"X-Frame-Options": "DENY" +"X-XSS-Protection": "1; mode=block" +.Ed +.Pp +Further to this it applies a Modern TLS config +.Pf ( +.Lk https://wiki.mozilla.org/Security/Server_Side_TLS "as defined by mozilla" +.Ns ) and a fairly restrictive set of HTTP security headers. +.Pp +Whilst running aws will log any errors that occur to the standard error stream. +It will also log successful connections to the standard output stream. +These successful connection log messages follow the +.Lk https://httpd.apache.org/docs/current/logs.html#combined "Apache Combined Log Format" +so they can be analysed using any tools that can accept such a log format. +.Pp +The following options are available: +.Bl -tag -width indent +.It Fl c Ar directory +Use the specified directory to store generated certificates in. +If the directory does not exist then it will be created with the mode 700. +By default the directory used is +.Pa ../certs +.Ns . +.Ed +.Sh EXIT STATUS +If no hostname is specified then +.Nm +will exit 2. +.Pp +For all other errors it will exit 1. +.Sh EXAMPLES +Serve +.Pa /var/www/htdocs +as +.Em www.alisdairmacleod.co.uk +.Ns : +.Pp +.Dl # cd /var/www/htdocs && aws www.alisdairmacleod.co.uk +.Pp +Multiple hostnames should be space delimited: +.Pp +.Dl # aws www.alisdairmacleod.co.uk www.alisdairmacleod.com +.Pp +Using +.Pa /var/certs +as the directory to store certificates: +.Pp +.Dl # aws -c /var/certs www.alisdairmacleod.co.uk +.Pp +.Sh BUGS +It might be possible to put certificates into the certificate directory to encourage +.Nm +to use the specified certificates rather than having ones generated. +.Sh SECURITY CONSIDERATIONS +.Nm +must have access to ports 80 and 443 and so likely will have to be run as root. blob - /dev/null blob + 72a98fc3426674dcdcb96b64574b9bd1a914bb6f (mode 644) --- /dev/null +++ aws.go @@ -0,0 +1,101 @@ +// Copyright (c) Alisdair MacLeod +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + "time" + + server "github.com/admacleod/aws/internal" + + "golang.org/x/crypto/acme/autocert" +) + +const ( + usage = `Usage: %s [OPTION] HOSTNAME ... +Serve the current directory over HTTPS using ACME certificates for HOST(s). + +` + narg = `%[1]s: missing host operand +Try '%[1]s -h' for more information. +` +) + +func main() { + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), usage, os.Args[0]) + flag.PrintDefaults() + } + + var ( + certDir string + ) + flag.StringVar(&certDir, "c", "../certs", "certificate directory") + flag.Parse() + + if flag.NArg() == 0 { + fmt.Fprintf(flag.CommandLine.Output(), narg, os.Args[0]) + os.Exit(2) + } + + // Configure TLS and certificate management + mgr := autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(flag.Args()...), + Cache: autocert.DirCache(certDir), + } + tlsCfg := mgr.TLSConfig() + server.ModerniseTLS(tlsCfg) + + // Setup our handler + mux := &http.ServeMux{} + mw := server.ChainMiddleware( + server.SecureHeaders, + server.CombinedLogFormatLogger(os.Stdout), + ) + handler := http.FileServer(http.Dir(".")) + mux.Handle("/", mw(handler)) + + timeout := 10 * time.Second + errLog := log.New(os.Stderr, "aws: ", log.LstdFlags) + // We need two servers, one for HTTP redirect and the other for HTTPS + srv := server.New( + server.Timeout(timeout), + server.ErrorLog(errLog), + server.Handle(mgr.HTTPHandler(nil)), + ) + srvTLS := server.New( + server.Timeout(timeout), + server.ErrorLog(errLog), + server.Handle(mux), + server.TLS(tlsCfg), + ) + + // Spool up and listen for errors + e := make(chan error, 1) + go func() { + e <- srv.ListenAndServe() + }() + go func() { + e <- srvTLS.ListenAndServeTLS("", "") + }() + err := <-e + if err != nil { + errLog.Fatalf("%v", err) + } +} blob - /dev/null blob + d4eb16af0ef5ccf35cb6e82409f56b04395259eb (mode 644) --- /dev/null +++ go.mod @@ -0,0 +1,10 @@ +module github.com/admacleod/aws + +go 1.17 + +require golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce + +require ( + golang.org/x/net v0.0.0-20220111093109-d55c255bac03 // indirect + golang.org/x/text v0.3.7 // indirect +) blob - /dev/null blob + 37908d6816efd3890d73b3e6e708a33a2088ca09 (mode 644) --- /dev/null +++ go.sum @@ -0,0 +1,13 @@ +golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce h1:Roh6XWxHFKrPgC/EQhVubSAGQ6Ozk6IdxHSzt1mR0EI= +golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220111093109-d55c255bac03 h1:0FB83qp0AzVJm+0wcIlauAjJ+tNdh7jLuacRYCIVv7s= +golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= blob - /dev/null blob + 65f36fbb2cdedc6c3181524e1cedc3f7a07c73bb (mode 644) --- /dev/null +++ internal/headers.go @@ -0,0 +1,51 @@ +// Copyright (c) Alisdair MacLeod +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package internal + +import "net/http" + +// CSP defines the Content-Security-Policy applied by the SecureHeaders function. +// The policy is very restrictive. Currently only allowing self-hosted CSS, images, +// and PDF documents. JavaScript, forms, and iframes are disallowed. +const CSP = "default-src 'none';" + + "style-src 'self';" + + "img-src 'self';" + + "object-src 'self';" + + "base-uri 'none';" + + "form-action 'none';" + + "frame-ancestors 'none';" + + "plugin-types application/pdf" + +// SecureHeaders is a http middleware for adding security headers to server responses. +// Applying the middleware will add the following header values, inspired by +// https://securityheaders.com, to responses from the wrapped handler. +// +// Content-Security-Policy: [see CSP constant] +// Referrer-Policy: no-referrer +// Strict-Transport-Security: max-age=63072000; includeSubDomains +// X-Content-Type-Options: nosniff +// X-Frame-Options: DENY +// X-XSS-Protection: 1; mode=block +func SecureHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Clacks-Overhead", "GNU Terry Pratchett") + w.Header().Set("Content-Security-Policy", CSP) + w.Header().Set("Referrer-Policy", "no-referrer") + w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-XSS-Protection", "1; mode=block") + next.ServeHTTP(w, r) + }) +} blob - /dev/null blob + f8457874bdcdc71f97255a5a8fb3076956d3ae9d (mode 644) --- /dev/null +++ internal/headers_test.go @@ -0,0 +1,59 @@ +// Copyright (c) Alisdair MacLeod +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package internal_test + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + server "github.com/admacleod/aws/internal" +) + +func TestSecureHeaders(t *testing.T) { + testBody := "test" + testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if _, err := io.WriteString(w, testBody); err != nil { + t.Errorf("could not write testBody in testHandler: %v", err) + } + }) + + req := httptest.NewRequest("GET", "http://test.example.com", nil) + w := httptest.NewRecorder() + + server.SecureHeaders(testHandler).ServeHTTP(w, req) + + res := w.Result() + body, _ := io.ReadAll(res.Body) + res.Body.Close() + + if string(body) != testBody { + t.Errorf("returned body is incorrect: expected=%s, got=%s", testBody, body) + } + + for header, expected := range map[string]string{ + "Content-Security-Policy": server.CSP, + "Referrer-Policy": "no-referrer", + "Strict-Transport-Security": "max-age=63072000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "X-XSS-Protection": "1; mode=block", + } { + got := res.Header.Get(header) + if expected != got { + t.Errorf("%s header is incorrect: expected=%s, got=%s", header, expected, got) + } + } +} blob - /dev/null blob + fb7db25d36c2f58af027d86bff21e86ce264291b (mode 644) --- /dev/null +++ internal/logger.go @@ -0,0 +1,73 @@ +// Copyright (c) Alisdair MacLeod +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package internal + +import ( + "fmt" + "io" + "log" + "net/http" + "strings" + "time" +) + +type loggerResponseWriter struct { + http.ResponseWriter + Status int + ContentLength int +} + +func (lrw *loggerResponseWriter) WriteHeader(code int) { + lrw.ResponseWriter.WriteHeader(code) + lrw.Status = code +} + +func (lrw *loggerResponseWriter) Write(bb []byte) (int, error) { + length, err := lrw.ResponseWriter.Write(bb) + lrw.ContentLength += length + + return length, err +} + +// CombinedLogFormatLogger is a middleware generator function that will write an Apache Combined Log Format +// to the passed output Writer for all requests to the wrapped handler. +// +// The definition of the Combined Log Format can be found at: https://httpd.apache.org/docs/2.4/logs.html#combined +func CombinedLogFormatLogger(output io.Writer) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + lrw := loggerResponseWriter{w, 200, 0} + next.ServeHTTP(&lrw, r) + fmt.Fprintf(output, "%s - - [%s] \"%s %s %s\" %d %d \"%s\" \"%s\"\n", + strings.Split(r.RemoteAddr, ":")[0], // Remove potential port number from remote address + start.Format("02/Jan/2006:15:04:05 -0700"), + r.Method, + r.RequestURI, + r.Proto, + lrw.Status, + lrw.ContentLength, + r.Referer(), + r.UserAgent(), + ) + }) + } +} + +// ErrorLog creates a server.Option function that will apply the passed log.Logger to the server as the ErrorLog. +func ErrorLog(logger *log.Logger) Option { + return func(srv *Server) { + srv.ErrorLog = logger + } +} blob - /dev/null blob + 7845a8582e4201307981b721e339336d43332238 (mode 644) --- /dev/null +++ internal/logger_test.go @@ -0,0 +1,92 @@ +// Copyright (c) Alisdair MacLeod +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package internal_test + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + "net/http/httptest" + "os" + "reflect" + "strings" + "testing" + "time" + + server "github.com/admacleod/aws/internal" +) + +func TestLogger(t *testing.T) { + testBody := "test" + testMethod := "GET" + testRequestURI := "http://test.example.com" + testStatusCode := http.StatusCreated + testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(testStatusCode) + if _, err := io.WriteString(w, testBody); err != nil { + t.Errorf("could not write testBody in testHandler: %v", err) + } + }) + + req := httptest.NewRequest(testMethod, testRequestURI, nil) + w := httptest.NewRecorder() + + var output bytes.Buffer + middleware := server.CombinedLogFormatLogger(&output) + start := time.Now() + + middleware(testHandler).ServeHTTP(w, req) + + res := w.Result() + body, _ := io.ReadAll(res.Body) + res.Body.Close() + + if string(body) != testBody { + t.Errorf("returned body is incorrect: expected=%s, got=%s", testBody, body) + } + + if testStatusCode != res.StatusCode { + t.Errorf("returned status code is incorrect: expected=%d, got=%d", testStatusCode, res.StatusCode) + } + + expected := fmt.Sprintf("%s - - [%s] \"%s %s %s\" %d %d \"%s\" \"%s\"\n", + strings.Split(req.RemoteAddr, ":")[0], + start.Format("02/Jan/2006:15:04:05 -0700"), + req.Method, + req.RequestURI, + req.Proto, + res.StatusCode, + len(body), + req.Referer(), + req.UserAgent(), + ) + got := output.String() + + if got != expected { + t.Errorf("error with log output:\nexpect=\"%s\"\nactual=\"%s\"", expected, got) + } +} + +func TestServerLoggerOption(t *testing.T) { + testLogger := log.New(os.Stdout, "test: ", log.LUTC) + testSrv := server.New( + server.ErrorLog(testLogger), + ) + + if !reflect.DeepEqual(testLogger, testSrv.ErrorLog) { + t.Errorf("incorrect server error log: expected=%v, got=%v", testLogger, testSrv.ErrorLog) + } +} blob - /dev/null blob + e4d859a72629f7e2e0d9487e6325ccca955c7291 (mode 644) --- /dev/null +++ internal/middleware.go @@ -0,0 +1,34 @@ +// Copyright (c) Alisdair MacLeod +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package internal + +import "net/http" + +// ChainMiddleware combines all passed middlewares into a single middleware function. +// Middlewares will be executed from the inside out in the order that they are passed in. +// +// server.ChainMiddleware(middleware1, middleware2)(handler) +// +// Is equivalent to: +// +// middleware2(middleware1(handler)) +func ChainMiddleware(mm ...func(http.Handler) http.Handler) func(http.Handler) http.Handler { + return func(final http.Handler) http.Handler { + for _, m := range mm { + final = m(final) + } + + return final + } +} blob - /dev/null blob + 6a66a9f9da9d8eacbcb25603ccbee607e29caa47 (mode 644) --- /dev/null +++ internal/middleware_test.go @@ -0,0 +1,58 @@ +// Copyright (c) Alisdair MacLeod +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package internal_test + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" + + server "github.com/admacleod/aws/internal" +) + +func TestMiddleware(t *testing.T) { + var testMiddlewareCalls []string + + middleware1 := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + testMiddlewareCalls = append(testMiddlewareCalls, "middleware1") + next.ServeHTTP(w, r) + }) + } + middleware2 := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + testMiddlewareCalls = append(testMiddlewareCalls, "middleware2") + next.ServeHTTP(w, r) + }) + } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + testMiddlewareCalls = append(testMiddlewareCalls, "handler") + }) + + req := httptest.NewRequest("GET", "http://test.example.com", nil) + w := httptest.NewRecorder() + + testMiddleware := server.ChainMiddleware(middleware1, middleware2) + testMiddleware(handler).ServeHTTP(w, req) + + expect := []string{ + "middleware2", + "middleware1", + "handler", + } + if !reflect.DeepEqual(expect, testMiddlewareCalls) { + t.Errorf("incorrect middleware calls: expected=%v, got=%v", expect, testMiddlewareCalls) + } +} blob - /dev/null blob + 2b9678523c8539733dc0db193e1af5fc53b9a3d3 (mode 644) --- /dev/null +++ internal/server.go @@ -0,0 +1,74 @@ +// Copyright (c) Alisdair MacLeod +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +// Package internal provides a higher level wrapper around the standard library http.Server. +// +// This wrapping allows for the addition of helper functions that can be used to trivialise +// the setup of a secure web server. +// +// The default http.Server is close to being safe to expose directly to the internet but misses some important settings: +// Timeouts, TLS settings, and Response headers. +// +// Creating a safe, modern, web server with this package is as easy as: +// +// srv := server.New( +// server.Timeout(120 * time.Second), +// server.TLS(server.ModerniseTLS(&tls.Config{})), +// server.Handle(server.SecureHeaders(handler)), +// ) +// +// Additional configurations are also provided to simplify server creation in general. +package internal + +import ( + "net/http" + "time" +) + +// Server defines a http server that allows for extension of the standard http.Server struct. +type Server struct { + http.Server +} + +// Option is a function that will apply some option to a Server object. +type Option func(*Server) + +// New creates a new Server with the passed Options applied to it. +// +// If no options are passed then a default server implementation is returned. +func New(opts ...Option) *Server { + srv := &Server{} + + for _, o := range opts { + o(srv) + } + + return srv +} + +// Timeout creates a server.Option function that will set the passed time.Duration as the +// ReadTimeout, WriteTimeout, and IdleTimeout for the server. +func Timeout(timeout time.Duration) Option { + return func(srv *Server) { + srv.ReadTimeout = timeout + srv.WriteTimeout = timeout + srv.IdleTimeout = timeout + } +} + +// Handle creates a server.Option function that will set the passed http.Handler to the server as the Handler. +func Handle(handler http.Handler) Option { + return func(srv *Server) { + srv.Handler = handler + } +} blob - /dev/null blob + af36d6b4a33e977cd36c95b9bb032202ae59bbc8 (mode 644) --- /dev/null +++ internal/server_test.go @@ -0,0 +1,56 @@ +// Copyright (c) Alisdair MacLeod +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package internal_test + +import ( + "net/http" + "reflect" + "testing" + "time" + + server "github.com/admacleod/aws/internal" +) + +func TestServerInstantiation(t *testing.T) { + testSrv := server.New( + server.Timeout(10 * time.Second), + ) + + for _, tt := range []struct { + name string + got time.Duration + expected time.Duration + }{ + {"ReadTimeout", testSrv.ReadTimeout, 10 * time.Second}, + {"WriteTimeout", testSrv.WriteTimeout, 10 * time.Second}, + {"IdleTimeout", testSrv.IdleTimeout, 10 * time.Second}, + } { + if tt.got != tt.expected { + t.Errorf("incorrect %s: expected=%v, got=%v", tt.name, tt.expected, tt.got) + } + } +} + +func TestServerHandlerOption(t *testing.T) { + testHandler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) + })) + testSrv := server.New( + server.Handle(testHandler), + ) + + if reflect.DeepEqual(testHandler, testSrv.Handler) { + t.Errorf("incorrect server handler: expected=%v, got=%v", testHandler, testSrv.Handler) + } +} blob - /dev/null blob + 2a319ba2c486f53d0dfb8b88eb993670252b19c5 (mode 644) --- /dev/null +++ internal/tls.go @@ -0,0 +1,49 @@ +// Copyright (c) Alisdair MacLeod +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package internal + +import ( + "crypto/tls" +) + +// ModerniseTLS modifies a tls.Config to meet Mozilla's intermediate compatibility recommendations +// https://wiki.mozilla.org/Security/Server_Side_TLS. +// +// The passed tls.Config is both modified and returned so that the function may +// optionally be used in a functional chain. +func ModerniseTLS(t *tls.Config) *tls.Config { + t.CurvePreferences = []tls.CurveID{ + tls.X25519, + tls.CurveP256, + tls.CurveP384, + } + t.MinVersion = tls.VersionTLS12 + t.CipherSuites = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + } + + return t +} + +// TLS creates a server.Option function that will set the passed tls.Config as the server TLSConfig. +func TLS(cfg *tls.Config) Option { + return func(srv *Server) { + srv.TLSConfig = cfg + } +} blob - /dev/null blob + 8aa0c42429c138cea33488b8281dba8a58a52ba2 (mode 644) --- /dev/null +++ internal/tls_test.go @@ -0,0 +1,74 @@ +// Copyright (c) Alisdair MacLeod +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package internal_test + +import ( + "crypto/tls" + "reflect" + "testing" + + server "github.com/admacleod/aws/internal" +) + +func TestTLSModerniseConfig(t *testing.T) { + testConfig := &tls.Config{} + + server.ModerniseTLS(testConfig) + + if !reflect.DeepEqual([]tls.CurveID{ + tls.X25519, + tls.CurveP256, + tls.CurveP384, + }, testConfig.CurvePreferences) { + t.Errorf("incorrect curve preferences: expected=%v, got=%v", []tls.CurveID{ + tls.X25519, + tls.CurveP256, + tls.CurveP384, + }, testConfig.CurvePreferences) + } + + if testConfig.MinVersion != uint16(tls.VersionTLS12) { + t.Errorf("incorrect minimum tls setting: expected=%v, got=%v", uint16(tls.VersionTLS12), testConfig.MinVersion) + } + + if !reflect.DeepEqual([]uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + }, testConfig.CipherSuites) { + t.Errorf("incorrect cipher suite preferences: expected=%v, got=%v", []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + }, testConfig.CipherSuites) + } +} + +func TestTLSServerOption(t *testing.T) { + testConfig := &tls.Config{} + server.ModerniseTLS(testConfig) + testSrv := server.New( + server.TLS(testConfig), + ) + + if !reflect.DeepEqual(testConfig, testSrv.TLSConfig) { + t.Errorf("incorrect tls config: expected=%v, got=%v", testConfig, testSrv.TLSConfig) + } +}