checker-http/checker/rules_redirect.go

77 lines
2.5 KiB
Go

// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
package checker
import (
"context"
"fmt"
"net/url"
"strings"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// httpsRedirectRule verifies that plain HTTP either redirects to HTTPS or
// fails (which is acceptable when HTTP is intentionally not served).
type httpsRedirectRule struct{}
func (r *httpsRedirectRule) Name() string { return "http.https_redirect" }
func (r *httpsRedirectRule) Description() string {
return "Plain HTTP responses must redirect to an HTTPS URL on the same host."
}
func (r *httpsRedirectRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
data, errSt := loadHTTPData(ctx, obs)
if errSt != nil {
return []sdk.CheckState{*errSt}
}
require := sdk.GetBoolOption(opts, OptionRequireHTTPS, true)
probes := probesByScheme(data.Probes, "http")
if len(probes) == 0 {
return []sdk.CheckState{unknownState("http.https_redirect.no_probes", "No HTTP probes were attempted.")}
}
var states []sdk.CheckState
for _, p := range probes {
if !p.TCPConnected || p.StatusCode == 0 {
// Reachability rule handles this; an HTTP server that is
// simply not running is fine for redirect-purposes.
continue
}
final := p.FinalURL
// final should be set; fallback to last redirect target.
if final == "" && len(p.RedirectChain) > 0 {
final = p.RedirectChain[len(p.RedirectChain)-1].To
}
isHTTPS := false
if u, err := url.Parse(final); err == nil {
isHTTPS = strings.EqualFold(u.Scheme, "https")
}
switch {
case isHTTPS:
// Good. No state per-probe; we'll emit one summary OK below.
case require:
states = append(states, sdk.CheckState{
Status: sdk.StatusWarn,
Code: "http.no_https_redirect",
Subject: p.Address,
Message: fmt.Sprintf("HTTP response on %s did not redirect to HTTPS (final URL: %s, status %d)", p.Address, final, p.StatusCode),
Meta: map[string]any{"fix": "Configure your web server to redirect every plain-HTTP request to https://."},
})
default:
states = append(states, sdk.CheckState{
Status: sdk.StatusInfo,
Code: "http.plain_http_served",
Subject: p.Address,
Message: fmt.Sprintf("HTTP responded directly without redirect (status %d)", p.StatusCode),
})
}
}
if len(states) == 0 {
return []sdk.CheckState{passState("http.https_redirect.ok", "HTTP redirects to HTTPS on every reachable IP.")}
}
return states
}