checker-tls/checker/interactive.go
Pierre-Olivier Mercier 79782a49c4 Migrate to checker-sdk-go v1.3.0 with standalone build tag
The SDK split the HTTP server scaffolding into the new
checker-sdk-go/checker/server subpackage. Update main.go to import
server and call server.New, and isolate the interactive form code
behind the standalone build tag so plugin/builtin builds skip
net/http entirely.
2026-04-24 14:04:55 +07:00

130 lines
3.6 KiB
Go

//go:build standalone
package checker
import (
"errors"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
sdk "git.happydns.org/checker-sdk-go/checker"
"git.happydns.org/checker-tls/contract"
)
// starttlsChoices returns the STARTTLS protocols supported by starttlsUpgraders,
// sorted, with an empty first entry meaning "speak TLS immediately".
func starttlsChoices() []string {
protos := make([]string, 0, len(starttlsUpgraders)+1)
protos = append(protos, "")
for k := range starttlsUpgraders {
protos = append(protos, k)
}
sort.Strings(protos)
return protos
}
// RenderForm satisfies server.Interactive. The fields mirror the inputs
// a producer checker would put into a contract.TLSEndpoint; a human fills
// them in directly when running the checker standalone.
func (p *tlsProvider) RenderForm() []sdk.CheckerOptionField {
return []sdk.CheckerOptionField{
{
Id: "host",
Type: "string",
Label: "Host",
Description: "Hostname or IP to probe.",
Placeholder: "example.com",
Required: true,
},
{
Id: "port",
Type: "uint",
Label: "Port",
Description: "TCP port of the TLS (or STARTTLS) endpoint.",
Default: float64(443),
Required: true,
},
{
Id: "sni",
Type: "string",
Label: "SNI",
Description: "Server name for the TLS handshake. Leave empty to reuse Host.",
Placeholder: "(defaults to Host)",
},
{
Id: "starttls",
Type: "string",
Label: "STARTTLS protocol",
Description: "Plaintext protocol to upgrade before the handshake. Leave empty for direct TLS.",
Choices: starttlsChoices(),
},
{
Id: "require",
Type: "bool",
Label: "Require STARTTLS",
Description: "When checked, a server that does not advertise STARTTLS is reported critical instead of a warning.",
},
{
Id: OptionProbeTimeoutMs,
Type: "uint",
Label: "Probe timeout (ms)",
Description: "Maximum time allowed for dial + STARTTLS + TLS handshake on the endpoint.",
Default: float64(DefaultProbeTimeoutMs),
},
}
}
// ParseForm satisfies server.Interactive. It turns the human inputs into
// a single contract.TLSEndpoint, wraps it in a DiscoveryEntry, and returns
// CheckerOptions shaped as if a happyDomain host had auto-filled
// OptionEndpoints via AutoFillDiscoveryEntries.
func (p *tlsProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) {
host := strings.TrimSpace(r.FormValue("host"))
if host == "" {
return nil, errors.New("host is required")
}
portStr := strings.TrimSpace(r.FormValue("port"))
if portStr == "" {
return nil, errors.New("port is required")
}
port64, err := strconv.ParseUint(portStr, 10, 16)
if err != nil || port64 == 0 {
return nil, fmt.Errorf("invalid port %q: must be 1-65535", portStr)
}
starttls := strings.TrimSpace(r.FormValue("starttls"))
if starttls != "" {
if _, ok := starttlsUpgraders[starttls]; !ok {
return nil, fmt.Errorf("unsupported STARTTLS protocol %q", starttls)
}
}
ep := contract.TLSEndpoint{
Host: host,
Port: uint16(port64),
SNI: strings.TrimSpace(r.FormValue("sni")),
STARTTLS: starttls,
RequireSTARTTLS: r.FormValue("require") == "true",
}
entry, err := contract.NewEntry(ep)
if err != nil {
return nil, fmt.Errorf("build discovery entry: %w", err)
}
opts := sdk.CheckerOptions{
OptionEndpoints: []sdk.DiscoveryEntry{entry},
}
if v := strings.TrimSpace(r.FormValue(OptionProbeTimeoutMs)); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
opts[OptionProbeTimeoutMs] = float64(n)
}
}
return opts, nil
}