commit - /dev/null
commit + 69e5688156aca3acbec8c121d025a9fab2752058
blob - /dev/null
blob + 1c363d2841cdff3fecddc69509795cad4461aabf (mode 644)
--- /dev/null
+++ LICENCE
+BSD Zero Clause License
+
+Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+
+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
+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
+.\" Copyright (C) 2020 by Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+.\"
+.\" 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
+// Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+//
+// 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
+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
+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
+// Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+//
+// 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
+// Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+//
+// 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
+// Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+//
+// 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
+// Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+//
+// 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
+// Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+//
+// 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
+// Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+//
+// 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
+// Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+//
+// 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
+// Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+//
+// 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
+// Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+//
+// 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
+// Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+//
+// 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)
+ }
+}