webserv

package module
v1.1.2 Latest Latest
Warning

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

Go to latest
Published: May 27, 2026 License: MIT Imports: 17 Imported by: 6

README

build coverage goreport Docs

webserv

Thin web service library.

Given a listen address, certificate directory, user name and data directory:

  • If certificate directory is not blank, reads fullchain.pem and privkey.pem from it.
  • If the listen address does not specify a port, default port depends on initial user privileges and if we have a certificate. To specify only a port, use :port.
  • Starts listening on the address and port.
  • If listening succeeds but a later setup step fails, Listen() still returns an error and closes the listener, but cfg.ListenURL may already have been populated.
  • If user name is given, switch to that user.
  • If data directory is given, create it if needed.
  • When serving, listen for SIGINT and SIGTERM and do a controlled shutdown.
  • ServeWith requires non-nil ctx, srv, and listener; panics from srv.Serve are recovered and returned as an error matching ErrServePanic.
  • Path values are treated as trusted config: certificate filenames and data-dir suffixes may use .. and symlinks and can resolve outside their base directories.

Why use this instead of net/http directly?

Wiring up http.Server and net.Listener by hand is easy to get subtly wrong. This package bundles the safe defaults and lifecycle handling you would otherwise have to remember to add yourself.

Security
  • Drops privileges safely (Unix only). Bind to a privileged port (80/443) as root, then switch to an unprivileged User. Supplementary groups, GID and UID are dropped in the correct order (setgroupssetgidsetuid), and HOME/USER/XDG_CONFIG_HOME are fixed up to match.
  • Sane timeouts by default. Serve sets ReadHeaderTimeout and IdleTimeout. A bare http.Server{} has no timeouts at all, leaving it open to Slowloris-style connection exhaustion.
  • TLS 1.3 minimum. When a certificate is loaded, the listener pins MinVersion to TLS 1.3 instead of relying on the standard library default.
  • Quiet TLS handshake errors. Failed handshakes (port scanners, plain HTTP sent to an HTTPS port) no longer flood your logs by default; set LogTLSErrors to keep them.
  • Recovers serve panics. A panic inside srv.Serve is recovered and returned as an error matching ErrServePanic instead of taking down the process.
Convenience
  • One call does the setup. ListenAndServe loads certificates, opens the listener, drops privileges, prepares the data directory and serves — in the right order, with errors propagated.
  • Automatic address defaults. Port and scheme are chosen from privilege level and whether a certificate was loaded (80/443 as root, 8080/8443 otherwise). Override with a full address or just :port.
  • Graceful shutdown. SIGINT/SIGTERM, or canceling the context, triggers srv.Shutdown bounded by Config.ShutdownTimeLimit, so in-flight requests can finish and the port is released cleanly.
  • A connectable URL. cfg.ListenURL is filled in with a printable, reachable URL (resolving wildcard/loopback binds to localhost or the certificate's DNS name) — handy for logs and links.
  • Managed data directory. Resolves DataDir to an absolute path and optionally creates it, defaulting under the user config directory.
  • Bring your own logger. The Logger interface matches log/slog, so structured startup and shutdown logging drops right in.

Usage

go get github.com/linkdata/webserv

package main

import (
	"context"
	"flag"
	"log/slog"
	"net/http"
	"os"

	"github.com/linkdata/webserv"
)

var (
	flagAddress   = flag.String("address", os.Getenv("WEBSERV_ADDRESS"), "serve HTTP requests on given [address][:port]")
	flagCertDir   = flag.String("certdir", os.Getenv("WEBSERV_CERTDIR"), "where to find fullchain.pem and privkey.pem")
	flagUser      = flag.String("user", envOrDefault("WEBSERV_USER", "www-data"), "switch to this user after startup (*nix only)")
	flagDataDir   = flag.String("datadir", envOrDefault("WEBSERV_DATADIR", "$HOME"), "where to store data files after startup")
	flagListenURL = flag.String("listenurl", os.Getenv("WEBSERV_LISTENURL"), "specify the external URL clients can reach us at")
)

func envOrDefault(envvar, defval string) (s string) {
	if s = os.Getenv(envvar); s == "" {
		s = defval
	}
	return
}

func main() {
	flag.Parse()

	cfg := webserv.Config{
		Address:   *flagAddress,
		CertDir:   *flagCertDir,
		User:      *flagUser,
		DataDir:   *flagDataDir,
		ListenURL: *flagListenURL,
		Logger:    slog.Default(),
	}

	http.DefaultServeMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("<html><body>Hello world!</body></html>"))
	})

	l, err := cfg.Listen()
	if err == nil {
		if err = cfg.Serve(context.Background(), l, nil); err == nil {
			return
		}
	}
	slog.Error(err.Error())
}

Documentation

Overview

Example
package main

import (
	"context"
	"flag"
	"log/slog"
	"net/http"
	"os"

	"github.com/linkdata/webserv"
)

var (
	flagAddress   = flag.String("address", os.Getenv("WEBSERV_ADDRESS"), "serve HTTP requests on given [address][:port]")
	flagCertDir   = flag.String("certdir", os.Getenv("WEBSERV_CERTDIR"), "where to find fullchain.pem and privkey.pem")
	flagUser      = flag.String("user", envOrDefault("WEBSERV_USER", "www-data"), "switch to this user after startup (*nix only)")
	flagDataDir   = flag.String("datadir", envOrDefault("WEBSERV_DATADIR", "$HOME"), "where to store data files after startup")
	flagListenURL = flag.String("listenurl", os.Getenv("WEBSERV_LISTENURL"), "specify the external URL clients can reach us at")
)

func envOrDefault(envvar, defval string) (s string) {
	if s = os.Getenv(envvar); s == "" {
		s = defval
	}
	return
}

func main() {
	flag.Parse()

	cfg := webserv.Config{
		Address:   *flagAddress,
		CertDir:   *flagCertDir,
		User:      *flagUser,
		DataDir:   *flagDataDir,
		ListenURL: *flagListenURL,
		Logger:    slog.Default(),
	}

	http.DefaultServeMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("<html><body>Hello world!</body></html>"))
	})

	l, err := cfg.Listen()
	if err == nil {
		if err = cfg.Serve(context.Background(), l, nil); err == nil {
			return
		}
	}
	slog.Error(err.Error())
}

Index

Examples

Constants

View Source
const (
	// FullchainPem is the default certificate chain filename used by LoadCert.
	FullchainPem = "fullchain.pem"
	// PrivkeyPem is the default private key filename used by LoadCert.
	PrivkeyPem = "privkey.pem"
)

Variables

View Source
var ErrBecomeUser = errBecomeUser{}

ErrBecomeUser matches errors returned by BecomeUser on failure.

View Source
var ErrServePanic = errServePanic{}

ErrServePanic matches errors returned by ServeWith when srv.Serve panics.

Functions

func BecomeUser

func BecomeUser(userName string) error

BecomeUser switches to the given userName if not empty.

It sets the GID, UID and changes the USER and HOME environment variables accordingly. It unsets XDG_CONFIG_HOME.

Returns an error matching ErrBecomeUser on failure.

func DefaultDataDir

func DefaultDataDir(dataDir, defaultSuffix string) (result string, err error)

DefaultDataDir returns the absolute path to dataDir if not empty, otherwise if defaultSuffix is not empty it returns the absolute joined path of os.UserConfigDir() and defaultSuffix.

It will expand environment variables in the path before evaluating the absolute path.

dataDir and defaultSuffix may contain paths, ".." segments and symlinks. They are not confined to UserConfigDir, so they may resolve outside of it. Caller is responsible for validating or sandboxing untrusted path input.

func Listener

func Listener(listenAddr, certDir, fullchainPem, privkeyPem, overrideUrl string) (l net.Listener, listenUrl, absCertDir string, err error)

Listener creates a net.Listener given an optional preferred address and an optional directory containing certificate files.

If certDir is not empty, it calls LoadCert to load fullchain.pem and privkey.pem.

The listener will default to all addresses and standard port depending on privileges and if a certificate was loaded or not.

These defaults can be overridden with the listenAddr argument. To specify only a port, use an address like ":8080".

Returns the net.Listener and listenURL if there was no error. If certificates were successfully loaded, absCertDir will be the absolute path to that directory.

func LoadCert

func LoadCert(certDir, fullchainPem, privkeyPem string) (cert *tls.Certificate, absCertDir string, err error)

LoadCert does nothing if certDir is empty, otherwise it expands environment variables and transforms it into an absolute path. It then tries to load a X509 key pair from the files named fullchainPem and privkeyPem from the resulting directory.

The filenames may contain paths, ".." segments and symlinks. They are not confined to certDir, so they may resolve outside of it. Caller is responsible for validating or sandboxing untrusted path input.

If fullchainPem is empty, it defaults to "fullchain.pem". If privkeyPem is empty, it defaults to "privkey.pem".

Return a non-nil cert and absolute path to certDir if there are no errors.

func UseDataDir

func UseDataDir(dataDir string, mode fs.FileMode) (string, error)

UseDataDir transforms dataDir into an absolute path. Then, if mode is not zero, it creates the path if it does not exist. Does nothing if dataDir is empty. Does not expand environment variables in the path.

Returns the final path or an empty string if dataDir was empty.

Types

type Config

type Config struct {
	Address              string        // optional specific address to listen on; use ":port" for port-only
	CertDir              string        // if set, directory to look for fullchain.pem and privkey.pem
	FullchainPem         string        // set to override filename for "fullchain.pem"
	PrivkeyPem           string        // set to override filename for "privkey.pem"
	User                 string        // if set, user to switch to after opening listening port
	DataDir              string        // if set, the data directory to use (created only when DataDirMode is nonzero); if unset, may be filled in after Listen
	DefaultDataDirSuffix string        // if set and DataDir is not set, set DataDir to the user's default data dir plus this suffix
	DataDirMode          fs.FileMode   // if nonzero, create DataDir if it does not exist using this mode
	ListenURL            string        // if set, the external URL clients can reach us at. If unset, Listen may fill this in (e.g. "https://localhost:8443"), even when Listen later returns an error after binding.
	ShutdownTimeLimit    time.Duration // maximum time ServeWith waits for graceful shutdown; zero uses a 1 second default
	LogTLSErrors         bool          // if set, http.Server TLS handshake error messages are not filtered
	Logger               Logger        // logger to use, if nil logs nothing
}

Config contains the startup and serving settings for a simple web service.

The zero value is usable: Listen serves HTTP on the default address and port, Serve uses a default http.Server, no user switch or data directory setup is performed, and no logs are emitted.

func (*Config) Listen

func (cfg *Config) Listen() (l net.Listener, err error)

Listen performs initial setup for a simple web server and returns a net.Listener if successful.

First it loads certificates if cfg.CertDir is set, and then starts a net.Listener (TLS or normal). The listener will default to all addresses and standard port depending on privileges and if a certificate was loaded or not.

If cfg.Address was set, any address or port given there overrides these defaults.

If cfg.User is set it then switches to that user and the users primary group. Note that this is not supported on Windows.

If cfg.DataDir or cfg.DefaultDataDirSuffix is set, calculates the absolute data directory path and sets cfg.DataDir. If cfg.DataDirMode is nonzero, the directory will be created if necessary.

On return, cfg.CertDir and cfg.DataDir will be absolute paths or be empty. If cfg.ListenURL was empty it may be set to a best-guess printable and connectable URL like "http://localhost:80" as soon as the socket is opened. Therefore cfg.ListenURL can be non-empty even if Listen returns an error from a later step, such as user switching or data directory setup.

func (*Config) ListenAndServe added in v0.9.0

func (cfg *Config) ListenAndServe(ctx context.Context, handler http.Handler) (err error)

ListenAndServe calls Listen followed by Serve.

It returns ctx.Err() without opening a listener if ctx is already canceled. Otherwise, it performs the setup documented by Listen and then serves requests with the default server settings documented by Serve.

The returned error is from Listen, Serve, ctx cancellation, or shutdown. A nil return means the server started successfully and then shut down cleanly.

Panics if ctx is nil.

func (*Config) Serve added in v0.9.0

func (cfg *Config) Serve(ctx context.Context, l net.Listener, handler http.Handler) error

Serve creates an http.Server with reasonable defaults and calls ServeWith.

The server uses handler as its Handler; if handler is nil, http.DefaultServeMux is used by net/http. ReadHeaderTimeout is set to 5 seconds and IdleTimeout is set to 1 minute.

Serve takes ownership of l for serving. It returns nil after a clean shutdown, returns ctx.Err() when ctx cancellation starts shutdown, and otherwise returns the error from serving or shutting down.

Panics if ctx or l is nil.

func (*Config) ServeWith added in v0.9.1

func (cfg *Config) ServeWith(ctx context.Context, srv *http.Server, l net.Listener) (err error)

ServeWith sets up a signal handler to catch SIGINT and SIGTERM and then calls srv.Serve(l). If ctx is canceled, the server will be shut down and this function returns with ctx.Err().

Returns nil if the server started successfully and then cleanly shut down. Graceful shutdown waits for cfg.ShutdownTimeLimit, or 1 second when cfg.ShutdownTimeLimit is zero.

Panics if ctx, srv or l is nil. Panics from srv.Serve are recovered and returned as an error matching ErrServePanic.

type Logger added in v0.9.8

type Logger interface {
	Info(msg string, keyValuePairs ...any)
	Warn(msg string, keyValuePairs ...any)
	Error(msg string, keyValuePairs ...any)
}

Logger matches log/slog.Info(), Warn() and Error(), but allows one to use another logger using an adaptor.

Jump to

Keyboard shortcuts

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