Documentation ¶
Overview ¶
Tools provides utility functions useful for web servers
Also check out the optional ask.systems/daemon/tools/flags library which provides -version and -syslog when you include it.
Common features:
- Run a web server with graceful shutdown when the quit channel is closed in one function call. Prefer RunHTTPServerTLS.
- Easily setup standard signal handlers to close your quit channel with CloseOnQuitSignals
- Generate random tokens or secret URL paths with RandomString
- Authenticate users via HTTP basic auth with BasicAuthHandler
- SecureHTTPDir which is a way to use http.FileServer and not serve directory listings, as well as password protect directories with .passwords files. ask.systems/daemon/host uses this so it's only needed if you want a file server as part a larger application.
Less common features:
- Generate self signed certificates and be your own Certificate Authority. These certificate functions are used by ask.systems/daemon/portal and the ask.systems/daemon/portal/gate client library. You only need them if you want to do extra custom certificate logic.
- Enforce HTTPS only with RedirectToHTTPS. ask.systems/daemon/portal uses this for all client connections, and will only connect to your backend via HTTPS. So you don't really need to use this unless you're accepting connections from clients other than portal.
- Create a flag that parses with no value and runs a callback when it is parsed with BoolFuncFlag. This is how -version and -syslog from ask.systems/daemon/tools/flags works.
- Prepend the current timestamp to any io.Writer.Write calls with TimestampWriter. This can be used for log files. This is already used by -syslog and the default log package prints the same timestamp format by default so this is only useful if you are working with custom output streams that you want timestamps for.
Index ¶
- Variables
- func AutorenewSelfSignedCertificate(hostname string, TTL time.Duration, isCA bool, onRenew func(*tls.Certificate), ...) (*tls.Config, error)
- func CertificateFromSignedCert(rawCert []byte, privateKey *ecdsa.PrivateKey) *tls.Certificate
- func CheckPassword(authHash, userPassword string) bool
- func CloseOnQuitSignals(quit chan struct{})
- func GenerateCertificateRequest(hostname string) ([]byte, *ecdsa.PrivateKey, error)
- func GenerateSelfSignedCertificate(hostname string, expiration time.Time, isCA bool) (*tls.Certificate, error)
- func HashPassword(password string) string
- func RandomString(bytes int) string
- func RunHTTPServer(port uint32, quit chan struct{})
- func RunHTTPServerTLS(port uint32, config *tls.Config, quit chan struct{})
- func SignCertificate(root *tls.Certificate, rawCertRequest []byte, expiration time.Time, isCA bool) ([]byte, error)
- type BasicAuthHandler
- func (h *BasicAuthHandler) Check(w http.ResponseWriter, r *http.Request) bool
- func (h *BasicAuthHandler) RemoveUser(username string)
- func (h *BasicAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (h *BasicAuthHandler) SetLogin(login string) error
- func (h *BasicAuthHandler) SetUser(username string, passwordHash string) error
- type BoolFuncFlag
- type RedirectToHTTPS
- type SecureHTTPDir
- func (s SecureHTTPDir) CheckPasswordsFiles(w http.ResponseWriter, r *http.Request) error
- func (s SecureHTTPDir) CheckPasswordsHandler(h http.Handler) http.Handler
- func (s SecureHTTPDir) FileSize(request string) (int64, error)
- func (s SecureHTTPDir) Open(name string) (http.File, error)
- func (s SecureHTTPDir) TestOpen(path string) error
- type SizeTrackerHTTPResponseWriter
- type TimestampWriter
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var PasswordsFile = ".passwords"
The filename to read username:password_hash logins per line from when using SecureHTTPDir.CheckPasswordsFiles
Functions ¶
func AutorenewSelfSignedCertificate ¶
func AutorenewSelfSignedCertificate(hostname string, TTL time.Duration, isCA bool, onRenew func(*tls.Certificate), quit chan struct{}) (*tls.Config, error)
Generate a new self signed certificate for the given hostname with the given TTL expiration time, and keep it renewed in the background until the quit channel is closed.
If isCA is true, set the capability bits to be a root Certificate Authority. So you can use the cert with SignCertificate. Certificate Authority certs cannot be used to serve webpages.
If the onRenew function is not nil, it is called every time the certificate is renewed, including the first time it is generated.
The returned config only has tls.Config.GetCertificate set, and it will return the latest certificate for any arguments (including nil).
func CertificateFromSignedCert ¶
func CertificateFromSignedCert(rawCert []byte, privateKey *ecdsa.PrivateKey) *tls.Certificate
Convert raw certificate bytes and a private key into the tls.Certificate structure, so it can be used for go connections.
You need this after your root CA has signed your certificate request.
func CheckPassword ¶
Checks passwords for BasicAuthHandler (or other uses if you want). Accepts hashes from HashPassword and will continue to accept hashes from old versions for compatibility. Empty authHash always returns false.
func CloseOnQuitSignals ¶
func CloseOnQuitSignals(quit chan struct{})
Closes the given channel when the OS sends a signal to stop. Also logs which signal was received
Catches: SIGINT, SIGKILL, SIGTERM, SIGHUP
func GenerateCertificateRequest ¶
func GenerateCertificateRequest(hostname string) ([]byte, *ecdsa.PrivateKey, error)
Generate a random certificate key and a request to send to a Certificate Authority to get your new certificate signed.
func GenerateSelfSignedCertificate ¶
func GenerateSelfSignedCertificate(hostname string, expiration time.Time, isCA bool) (*tls.Certificate, error)
Generate a self signed TLS certificate for the given hostname and expiration date.
If isCA is true, set the capability bits to be a root Certificate Authority. So you can use the cert with SignCertificate. Certificate Authority certs cannot be used to serve webpages.
func HashPassword ¶
Returns a password hash compatible with the default BasicAuthHandler hash. May change algorithms over time as hash recommendations change.
Returns an empty string if there's any error: the password is too long for the current hash function, failed reading random devices, etc.
func RandomString ¶
Returns a random base64 string of the specified number of bytes. If there's an error calling crypto/rand.Read, it returns "".
Uses base64.URLEncoding for URL safe strings.
func RunHTTPServer ¶
func RunHTTPServer(port uint32, quit chan struct{})
Starts an HTTP server on the specified port and block until the quit channel is closed and graceful shutdown has finished.
func RunHTTPServerTLS ¶
Starts an HTTPS server on the specified port using the TLS config and block until the quit channel is closed and graceful shutdown has finished.
func SignCertificate ¶
func SignCertificate(root *tls.Certificate, rawCertRequest []byte, expiration time.Time, isCA bool) ([]byte, error)
Use a root Certificate Authority certificate to sign a given certificate request and give the new certificate the specified expiration date.
Returns the raw certificate data from crypto/x509.CreateCertificate.
Types ¶
type BasicAuthHandler ¶
type BasicAuthHandler struct { // Realm is passed to the browser and the browser will automatically send the // same credentials for a realm it has logged into before. Optional. Realm string Handler http.Handler // If set, auth checks are performed using this function instead of the // default. CheckPassword is responsible for parsing the encoded parameters // from the authHash string and doing any base64 decoding, as well as doing // the hash comparison (which should be a constant time comparison). // // This allows for using any hash function that's needed with // BasicAuthHandler, or even accept multiple at once. Many are available in // [golang.org/x/crypto]. // // If not set [CheckPassword] will be used. // The default will always accept the hashes from [HashPassword] and will // continue to accept hashes from old versions for compatibility. CheckPassword func(authHash, userPassword string) bool // contains filtered or unexported fields }
Wraps another http.Handler and only calls the wrapped handler if BasicAuth passed for one of the registered users. Optionally can call BasicAuthHandler.Check in as many handlers as you want, and then you don't have to use the handler wrapping option.
- Options must be setup before any requests and then not changed.
- Methods may be called at any time, it's thread safe.
- This type must not be copied after first use (it holds sync containers)
Example (GeneratePasswordHash) ¶
/*?sr/bin/env go run "$0" "$@"; exit $? #*/ // If you prefer to run it offline copy this to a new folder and run it locally // using the go sh-bang line above. Just chmod +x hash.go && ./hash.go -pw stdin package main import ( "bufio" "flag" "fmt" "log" "os" "strings" "ask.systems/daemon/tools" ) // Change the password here to run in online in the docs site var Password = flag.String("pw", "hunter2", "The password to hash") func main() { flag.Parse() if *Password == "stdin" { // so you don't put the password in .bash_history fmt.Printf("Type your password (not hidden) then press enter: ") if pwStr, err := bufio.NewReader(os.Stdin).ReadString('\n'); err == nil { *Password = strings.TrimSpace(pwStr) } else { log.Fatal(err) } } fmt.Println(tools.HashPassword(*Password)) }
Output:
func (*BasicAuthHandler) Check ¶
func (h *BasicAuthHandler) Check(w http.ResponseWriter, r *http.Request) bool
Check HTTP basic auth and reply with Unauthorized if authentication failed. Returns true if authentication passed and then the users can handle the request.
If it returns false auth failed the response has been sent and you can't write more.
If you want to log authentication failures, you can use this call instead of wrapping your handler.
func (*BasicAuthHandler) RemoveUser ¶
func (h *BasicAuthHandler) RemoveUser(username string)
Unauthorize a given username from pages protected by this handler.
func (*BasicAuthHandler) ServeHTTP ¶
func (h *BasicAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
The http.Handler interface function. Only calls the wrapped handler if the request has passed basic auth.
func (*BasicAuthHandler) SetLogin ¶
func (h *BasicAuthHandler) SetLogin(login string) error
Authorizes a user with this handler using a "username:password_hash" string
The password_hash must be a SHA256 base64.URLEncoding encoded string. You can generate this with HashPassword.
func (*BasicAuthHandler) SetUser ¶
func (h *BasicAuthHandler) SetUser(username string, passwordHash string) error
Authorizes the given user to access the pages protected by this handler.
The passwordHash must be a SHA256 base64.URLEncoding encoded string. You can generate this with HashPassword.
type BoolFuncFlag ¶
Use this to define a flag that has a callback like with flag.Func but label it as a boolean flag to the go flag parser. Use flag.Func for non-bool flags.
This means you can invoke the flag by -foobar instead of -foobar=true, and the callback will be called by flag.Parse.
Example ¶
package main import ( "flag" "fmt" "os" "ask.systems/daemon/tools" ) func HandleHello(value string) error { fmt.Println("Hello!") return nil } func main() { flag.Var(tools.BoolFuncFlag(HandleHello), "hello", "If set, print hello") // The handler function is called when flag.Parse sees the flag oldArgs := os.Args os.Args = []string{"bin", "-hello"} flag.Parse() os.Args = oldArgs }
Output: Hello!
func (BoolFuncFlag) IsBoolFlag ¶
func (b BoolFuncFlag) IsBoolFlag() bool
Returns true. This is why you need a helper type and can't just use flag.Var to get this behavior.
func (BoolFuncFlag) Set ¶
func (b BoolFuncFlag) Set(s string) error
func (BoolFuncFlag) String ¶
func (b BoolFuncFlag) String() string
type RedirectToHTTPS ¶
type RedirectToHTTPS struct{}
RedirectToHTTPS is an http.Handler which redirects any requests to the same url but with https instead of http.
Example ¶
package main import ( "net/http" "ask.systems/daemon/tools" ) func main() { // Serve an encrypted greeting httpsServer := &http.Server{ Addr: ":443", Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Hello!")) }), } go httpsServer.ListenAndServeTLS("example.cert", "example.key") // Redirect any unencrypted connections to the encrypted server httpServer := &http.Server{ Addr: ":80", Handler: tools.RedirectToHTTPS{}, } go httpServer.ListenAndServe() }
Output:
func (RedirectToHTTPS) ServeHTTP ¶
func (r RedirectToHTTPS) ServeHTTP(w http.ResponseWriter, req *http.Request)
Unconditionally sets the url to https:// and then serves an HTTP 303 response
type SecureHTTPDir ¶
type SecureHTTPDir struct { http.Dir // If false, do not serve or list files or directories starting with '.' AllowDotfiles bool // If true, serve a page listing all the files in a directory for any // directories that do not have index.html. If false serve 404 instead, and // index.html will still be served for directories containing it. AllowDirectoryListing bool // If you're using [SecureHTTPDir.CheckPasswordsFiles] set this to an // application identifier string e.g. "daemon". The browser will remember the // realm after a successful login so the user won't have to keep typing the // password, and this works across multiple paths as well. BasicAuthRealm string }
SecureHTTPDir is a replacement for http.Dir for use with http.FileServer. It allows you to turn off serving directory listings and hidden dotfiles.
These settings are not thread safe so set them up before serving.
Example ¶
How to use http.FileServer and disallow directory listing
package main import ( "net/http" "ask.systems/daemon/tools" ) func main() { const localDirectory = "/home/www/public/" const servePath = "/foo/" dir := tools.SecureHTTPDir{ Dir: http.Dir(localDirectory), AllowDirectoryListing: false, } http.Handle(servePath, http.StripPrefix(servePath, http.FileServer(dir))) }
Output:
func (SecureHTTPDir) CheckPasswordsFiles ¶
func (s SecureHTTPDir) CheckPasswordsFiles(w http.ResponseWriter, r *http.Request) error
Call this before handling the request with http.FileServer in order to authenticate the user if the directory requested (or parent directories) contains a file named the value of PasswordsFile (default is .passwords). If the returned error is not nil, then authentication failed and an unauthorized http response has been written and sent. Otherwise nothing is written to the http.ResponseWriter.
The passwords file that is checked is the first one found when searching starting with the current directory, then the parent directory, and so on.
This search ordering means that adding a PasswordsFile file somewhere in the directory tree makes access more restrictive than the parent directory. If you want to make a subdirectory allow more users than the parent directory, then you must copy all of the parent directory passwords into the PasswordsFile of the subdirectory, and then add extra users to that list.
You can generate hashes with HashPassword and the format of the files is:
username1:password_hash1 user2:password_hash2
The easiest way to use this is SecureHTTPDir.CheckPasswordsHandler, but you will need to call this directly if you want to log errors for example.
Example ¶
package main import ( "log" "net/http" "ask.systems/daemon/tools" ) func main() { dir := tools.SecureHTTPDir{ Dir: http.Dir("/home/www/public/"), BasicAuthRealm: "daemon", } fileServer := http.FileServer(dir) const servePath = "/filez/" http.Handle(servePath, http.StripPrefix(servePath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := dir.CheckPasswordsFiles(w, r) if err == nil { fileServer.ServeHTTP(w, r) } else { // These headers are added by portal (and other reverse proxies) log.Printf("%v:%v failed authentication: %v", r.Header.Get("X-Forwarded-For"), r.Header.Get("X-Forwarded-For-Port"), err) } }))) // Then start the http server }
Output:
func (SecureHTTPDir) CheckPasswordsHandler ¶
func (s SecureHTTPDir) CheckPasswordsHandler(h http.Handler) http.Handler
Wraps a given handler and only calls it if SecureHTTPDir.CheckPasswordsFiles passes. It probably doesn't make sense to use this with anything other than http.FileServer
Example ¶
package main import ( "net/http" "ask.systems/daemon/tools" ) func main() { dir := tools.SecureHTTPDir{ Dir: http.Dir("/home/www/public/"), BasicAuthRealm: "daemon", } const servePath = "/filez/" http.Handle(servePath, http.StripPrefix(servePath, dir.CheckPasswordsHandler(http.FileServer(dir)))) // Then start the http server }
Output:
func (SecureHTTPDir) FileSize ¶
func (s SecureHTTPDir) FileSize(request string) (int64, error)
Returns the file size in bytes that will be served for a given request path. This means that if it's a directory with index.html we return the size of index.html. Without the index, directories get size 0.
You can safely ignore the error, it's just there in case you want to know why we returned 0
func (SecureHTTPDir) Open ¶
func (s SecureHTTPDir) Open(name string) (http.File, error)
Returns fs.ErrNotExist for files and directories that should not be accessed depending on the settings.
This is the override over http.Dir that allows this class to work
func (SecureHTTPDir) TestOpen ¶
func (s SecureHTTPDir) TestOpen(path string) error
Test if we can open the given file.
It's good to call this when you start up a file server because http.FileServer doesn't log anything on open errors.
type SizeTrackerHTTPResponseWriter ¶ added in v0.6.6
type SizeTrackerHTTPResponseWriter struct { http.ResponseWriter // contains filtered or unexported fields }
func NewSizeTrackerHTTPResponseWriter ¶ added in v0.6.6
func NewSizeTrackerHTTPResponseWriter(w http.ResponseWriter) SizeTrackerHTTPResponseWriter
func (SizeTrackerHTTPResponseWriter) BytesRead ¶ added in v0.6.6
func (w SizeTrackerHTTPResponseWriter) BytesRead() uint64
func (SizeTrackerHTTPResponseWriter) Flush ¶ added in v0.6.6
func (w SizeTrackerHTTPResponseWriter) Flush()
type TimestampWriter ¶
type TimestampWriter struct { io.Writer // Don't forget to include whitespace at the end to separate the message TimeFormat string }
Wraps an io.Writer and prepends a timestamp from time.Now to each TimestampWriter.Write call.
func NewTimestampWriter ¶
func NewTimestampWriter(w io.Writer) *TimestampWriter
Create a TimestampWriter with the default time format (which matches the log package default format)