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) }