Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functions to httputil (autocert, letsencrypt, middleware examples) #44

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
57 changes: 57 additions & 0 deletions httputil/autocert.go
Original file line number Diff line number Diff line change
@@ -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
}
106 changes: 106 additions & 0 deletions httputil/examples/server_example.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
39 changes: 39 additions & 0 deletions httputil/handlers.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
12 changes: 12 additions & 0 deletions httputil/httputil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
9 changes: 9 additions & 0 deletions tlsutil/tlsutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down