tools

package
v0.6.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 9, 2024 License: MIT Imports: 30 Imported by: 4

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

Examples

Constants

This section is empty.

Variables

View Source
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

func CheckPassword(authHash, userPassword string) bool

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

func HashPassword(password string) string

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

func RandomString(bytes int) string

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

func RunHTTPServerTLS(port uint32, config *tls.Config, quit chan struct{})

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

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

type BoolFuncFlag func(string) error

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 (SizeTrackerHTTPResponseWriter) Write added in v0.6.6

func (w SizeTrackerHTTPResponseWriter) Write(input []byte) (n int, err error)

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)

func (*TimestampWriter) Write

func (w *TimestampWriter) Write(in []byte) (int, error)

See io.Writer

Directories

Path Synopsis
Import this package to get useful flags every server should have
Import this package to get useful flags every server should have

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL