From ebe05b15fcb7e351971f85263fcd92e03d712cfc Mon Sep 17 00:00:00 2001 From: seph Date: Mon, 19 Aug 2019 10:57:24 -0400 Subject: [PATCH 1/2] Expand httputil This expands the functionality in httputil. It adds simple handlers for BasicAuth and redirecting to https This adds several autocert related mechanisms to `httputil` --- go.mod | 2 ++ go.sum | 10 ++++++++ httputil/autocert.go | 57 ++++++++++++++++++++++++++++++++++++++++++++ httputil/handlers.go | 39 ++++++++++++++++++++++++++++++ httputil/httputil.go | 12 ++++++++++ tlsutil/tlsutil.go | 9 +++++++ 6 files changed, 129 insertions(+) create mode 100644 httputil/autocert.go create mode 100644 httputil/handlers.go diff --git a/go.mod b/go.mod index 58806d1..8292845 100644 --- a/go.mod +++ b/go.mod @@ -12,12 +12,14 @@ require ( github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect github.com/lib/pq v1.0.0 // indirect github.com/mattn/go-sqlite3 v1.10.0 // indirect + github.com/oklog/run v1.0.0 github.com/oklog/ulid v0.3.0 github.com/opencensus-integrations/ocsql v0.1.1 github.com/pkg/errors v0.8.0 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.2.1 go.opencensus.io v0.18.0 + golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect google.golang.org/appengine v1.3.0 // indirect google.golang.org/grpc v1.14.0 diff --git a/go.sum b/go.sum index 2d4a4e9..f50aeb6 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v0.3.0 h1:yEMMWFnYiPX/ytx1StIE0E1a35sm8MmWD/uSL9ZtKhg= github.com/oklog/ulid v0.3.0/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/opencensus-integrations/ocsql v0.1.1 h1:+J5BmLX1kNWCH9/5wJdleej2oRyJrhVEt+FAjq1VqaI= @@ -48,14 +50,22 @@ github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7 github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= go.opencensus.io v0.18.0 h1:Mk5rgZcggtbvtAun5aJzAtjKKN/t0R3jJPlWILlv938= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 h1:rOhMmluY6kLMhdnrivzec6lLgaVbMHMn2ISQXJeJ5EM= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= diff --git a/httputil/autocert.go b/httputil/autocert.go new file mode 100644 index 0000000..15e3be6 --- /dev/null +++ b/httputil/autocert.go @@ -0,0 +1,57 @@ +package httputil + +import ( + "net/http" + "time" + + "github.com/pkg/errors" + "golang.org/x/crypto/acme" + "golang.org/x/crypto/acme/autocert" +) + +type AcmOpt func(*autocert.Manager) error + +func WithLetsEncryptStaging() AcmOpt { + return func(m *autocert.Manager) error { + m.Client.DirectoryURL = "https://acme-staging.api.letsencrypt.org/directory" + return nil + } +} + +func WithEmail(e string) AcmOpt { + return func(m *autocert.Manager) error { + m.Email = e + return nil + } +} + +func WithRenewBefore(t time.Duration) AcmOpt { + return func(m *autocert.Manager) error { + m.RenewBefore = t + return nil + } +} + +func WithHttpClient(c *http.Client) AcmOpt { + return func(m *autocert.Manager) error { + m.Client.HTTPClient = c + return nil + } +} + +func NewAutocertManager(cache autocert.Cache, allowedHosts []string, opts ...AcmOpt) (*autocert.Manager, error) { + m := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(allowedHosts...), + Cache: cache, + Client: &acme.Client{}, + } + + for _, opt := range opts { + if err := opt(m); err != nil { + return nil, errors.Wrap(err, "applying option to autocert manager") + } + } + + return m, nil +} diff --git a/httputil/handlers.go b/httputil/handlers.go new file mode 100644 index 0000000..b7ec4fd --- /dev/null +++ b/httputil/handlers.go @@ -0,0 +1,39 @@ +package httputil + +import ( + "crypto/subtle" + "net/http" +) + +// BasicAuthMiddleware is http middleware to authenticate based on a +// predefined map of usernames and passwords. +func BasicAuthMiddleware(basicauthPairs map[string][]byte, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if !ok || username == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // username and password must match + expectedPassword, ok := basicauthPairs[username] + if !ok || subtle.ConstantTimeCompare([]byte(password), expectedPassword) != 1 { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // handoff to the next handler + next.ServeHTTP(w, r) + }) +} + +// RedirectToSecureHandler is a simple handler to redirect to the secure URL. +func RedirectToSecureHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Connection", "close") + url := r.URL + url.Scheme = "https" + url.Host = r.Host + http.Redirect(w, r, url.String(), http.StatusMovedPermanently) + }) +} diff --git a/httputil/httputil.go b/httputil/httputil.go index 9bb09e6..299e392 100644 --- a/httputil/httputil.go +++ b/httputil/httputil.go @@ -19,6 +19,18 @@ func WithTLSConfig(cfg *tls.Config) Option { } } +func WithReadTimeout(t time.Duration) Option { + return func(s *http.Server) { + s.ReadTimeout = t + } +} + +func WithWriteTimeout(t time.Duration) Option { + return func(s *http.Server) { + s.WriteTimeout = t + } +} + // NewServer creates an HTTP Server with pre-configured timeouts and a secure TLS Config. func NewServer(addr string, h http.Handler, opts ...Option) *http.Server { srv := http.Server{ diff --git a/tlsutil/tlsutil.go b/tlsutil/tlsutil.go index bfdd180..c62faa8 100644 --- a/tlsutil/tlsutil.go +++ b/tlsutil/tlsutil.go @@ -61,6 +61,15 @@ func WithCertificates(certs []tls.Certificate) Option { } } +// WithGetCertificate sets the GetCertificate hook on +// tls.Config.GetCertificate. It's the usual integration point for +// autocert. +func WithGetCertificate(getCertFunc func(*tls.ClientHelloInfo) (*tls.Certificate, error)) Option { + return func(config *tls.Config) { + config.GetCertificate = getCertFunc + } +} + // NewConfig returns a configured *tls.Config. By default, the TLS Config is set to // MinVersion of TLS 1.2 and a Modern Profile. // From 00f5fe47dc73f73050684f14a5a442d9590e6150 Mon Sep 17 00:00:00 2001 From: seph Date: Sat, 12 Oct 2019 09:33:44 -0400 Subject: [PATCH 2/2] add example --- httputil/examples/server_example.go | 106 ++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 httputil/examples/server_example.go diff --git a/httputil/examples/server_example.go b/httputil/examples/server_example.go new file mode 100644 index 0000000..e1badef --- /dev/null +++ b/httputil/examples/server_example.go @@ -0,0 +1,106 @@ +// Example program using kit/httputil to launch a web server. Content +// is served via https, with a redirect on http. It uses autocert to +// fetch a letsencrypt cert (from staging). +package main + +import ( + "crypto/tls" + "flag" + "fmt" + "net/http" + "os" + "os/signal" + "time" + + "github.com/kolide/kit/httputil" + "github.com/oklog/run" + "golang.org/x/crypto/acme/autocert" +) + +func main() { + + var ( + flHostName = flag.String("hostname", "", "External hostname for service. Needed for autocert") + flCertDir = flag.String("certdir", "", "Directory to store certificates in") + ) + flag.Parse() + + if *flHostName == "" { + fmt.Println("Must specify hostname") + os.Exit(1) + } + + if *flCertDir == "" { + fmt.Println("Must specify certdir") + os.Exit(1) + } + + m, err := httputil.NewAutocertManager( + autocert.DirCache(*flCertDir), + []string{*flHostName}, + httputil.WithLetsEncryptStaging(), + ) + if err != nil { + panic(err) + } + + var g run.Group + { + srv := httputil.NewServer( + ":443", + stringMiddleware("secure!", nil), + httputil.WithTLSConfig(&tls.Config{GetCertificate: m.GetCertificate}), + ) + + g.Add(func() error { + fmt.Println("Starting port 443") + return srv.ListenAndServeTLS("", "") + }, func(err error) { + srv.Close() + return + }) + } + { + srv := httputil.NewServer( + ":80", + m.HTTPHandler(httputil.RedirectToSecureHandler()), + httputil.WithReadTimeout(5*time.Second), + httputil.WithWriteTimeout(5*time.Second), + ) + + g.Add(func() error { + fmt.Println("Starting port 80") + return srv.ListenAndServe() + }, func(err error) { + srv.Close() + return + }) + } + + { + // this actor handles an os interrupt signal and terminates the server. + sig := make(chan os.Signal, 1) + g.Add(func() error { + signal.Notify(sig, os.Interrupt) + <-sig + fmt.Println("beginning shutdown") + return nil + }, func(err error) { + fmt.Println("process interrupted") + close(sig) + }) + } + + if err := g.Run(); err != nil { + panic(err) + } +} + +func stringMiddleware(s string, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, s) + if next != nil { + next.ServeHTTP(w, r) + } + }) +}