Files
smallwebwaf/internal/smallwebwaf/healthcheck.go
T
clawbot bff65f4e2f
check / check (push) Successful in 4m53s
Settings given as files: the _FILE form of every setting (closes #87)
Every setting X may instead be given as a file that X_FILE names, read
once at start: its contents, less one trailing newline, are the value,
checked as X would be. X and X_FILE both set, or a file that cannot be
read, stops the start with a message naming the variable. The logged
settings name the file, and mask a token read from one.
SWWAF_LOG_REMOTE_TLS_CA_FILE, whose value is a file already, has no
_FILE form. The health check reads only SWWAF_LISTEN_ADDR and
SWWAF_UPSTREAM_URL, so no other setting or file can fail it.

Judgement call: an invalid value read from a file is named as X, not X_FILE.
Rule suppressed: gosec G304 on reading the named file, as for the CA file.

Model: opus-5-5
2026-10-07 00:01:19 +02:00

103 lines
2.6 KiB
Go

package smallwebwaf
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/proxy"
)
// healthCheckTimeout bounds the whole health check.
const healthCheckTimeout = 5 * time.Second
var errHealthEndpoint = errors.New("smallwebwaf's health endpoint answered")
// HealthCheck is the container's health check. It returns 0 while
// smallwebwaf answers its health endpoint on 127.0.0.1, at the port in
// SWWAF_LISTEN_ADDR, and the app accepts connections at the address in
// SWWAF_UPSTREAM_URL. Otherwise it writes why to stderr and returns 1.
// It reads no other setting, nor a file that another names, so neither
// can fail it.
// args are the arguments after `healthcheck`; it takes none, and given
// one it names it on stderr and returns 1 without checking anything.
func HealthCheck(
ctx context.Context, args []string, lookupEnv func(string) (string, bool),
stderr io.Writer,
) int {
if len(args) > 0 {
_, _ = fmt.Fprintf(stderr,
"smallwebwaf healthcheck: unexpected argument %q\n", args[0])
return 1
}
err := healthCheck(ctx, lookupEnv)
if err != nil {
_, _ = fmt.Fprintln(stderr, "unhealthy:", err)
return 1
}
return 0
}
func healthCheck(ctx context.Context, lookupEnv func(string) (string, bool)) error {
ctx, cancel := context.WithTimeout(ctx, healthCheckTimeout)
defer cancel()
listenAddr, upstreamURL, err := config.ListenAddrAndUpstreamURL(lookupEnv)
if err != nil {
return fmt.Errorf("invalid setting: %w", err)
}
// The settings have checked that the address has a port.
_, port, _ := net.SplitHostPort(listenAddr)
health := "http://" + net.JoinHostPort("127.0.0.1", port) + proxy.HealthPath
req, err := http.NewRequestWithContext(ctx, http.MethodGet, health, http.NoBody)
if err != nil {
return fmt.Errorf("make the request: %w", err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("ask smallwebwaf: %w", err)
}
_ = res.Body.Close()
if res.StatusCode != http.StatusOK {
return fmt.Errorf("%w %s", errHealthEndpoint, res.Status)
}
conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", appAddress(upstreamURL))
if err != nil {
return fmt.Errorf("connect to the app: %w", err)
}
_ = conn.Close()
return nil
}
// appAddress is the host and port of the app's URL, the port being the
// scheme's own when the URL names none.
func appAddress(app *url.URL) string {
port := app.Port()
if port == "" {
port = "80"
if app.Scheme == "https" {
port = "443"
}
}
return net.JoinHostPort(app.Hostname(), port)
}