136 lines
5 KiB
Go
136 lines
5 KiB
Go
// This file is part of the happyDomain (R) project.
|
|
// Copyright (c) 2020-2026 happyDomain
|
|
// Authors: Pierre-Olivier Mercier, et al.
|
|
//
|
|
// This program is offered under a commercial and under the AGPL license.
|
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
|
|
|
// Package checker implements an HTTP/HTTPS server checker for happyDomain.
|
|
// It probes the abstract.Server it is attached to over HTTP (80) and HTTPS
|
|
// (443), captures response headers, cookies and the (parsed) HTML body, then
|
|
// evaluates a set of independent rules covering reachability, the HTTP→HTTPS
|
|
// upgrade path, modern transport-security headers (HSTS, CSP), application
|
|
// security headers (X-Frame-Options, X-Content-Type-Options, X-XSS-Protection)
|
|
// and Subresource Integrity. Deep TLS / certificate analysis is intentionally
|
|
// delegated to checker-tls.
|
|
package checker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
const ObservationKeyHTTP = "http"
|
|
|
|
const (
|
|
OptionService = "service"
|
|
OptionDomainName = "domain_name"
|
|
OptionProbeTimeoutMs = "probeTimeoutMs"
|
|
OptionMaxRedirects = "maxRedirects"
|
|
OptionUserAgent = "userAgent"
|
|
OptionRequireHTTPS = "requireHTTPS"
|
|
OptionRequireHSTS = "requireHSTS"
|
|
OptionMinHSTSMaxAgeDays = "minHSTSMaxAgeDays"
|
|
OptionRequireCSP = "requireCSP"
|
|
)
|
|
|
|
const (
|
|
DefaultHTTPPort uint16 = 80
|
|
DefaultHTTPSPort uint16 = 443
|
|
DefaultProbeTimeoutMs = 10000
|
|
DefaultMaxRedirects = 5
|
|
DefaultUserAgent = "happyDomain-checker-http/1.0"
|
|
DefaultMinHSTSMaxAge = 180 // days; 180d ≈ 15552000s, the commonly recommended minimum
|
|
MaxConcurrentProbes = 8
|
|
MaxBodyBytes = 1 << 20 // 1 MiB cap on HTML body to keep memory bounded
|
|
)
|
|
|
|
// HTTPData is the full collected payload written under ObservationKeyHTTP.
|
|
//
|
|
// Probes/Domain/CollectedAt come from the root collector and are kept at
|
|
// the top level for backward compatibility with the rules that have
|
|
// always read them directly.
|
|
//
|
|
// Extensions holds the JSON-encoded outputs of every additional Collector
|
|
// registered via RegisterCollector, keyed by Collector.Key(). Rules
|
|
// access them via LoadExtension[T] to get a typed view.
|
|
type HTTPData struct {
|
|
Domain string `json:"domain,omitempty"`
|
|
Probes []HTTPProbe `json:"probes"`
|
|
CollectedAt time.Time `json:"collected_at"`
|
|
|
|
Extensions map[string]json.RawMessage `json:"extensions,omitempty"`
|
|
}
|
|
|
|
// HTTPProbe is the outcome of a single (scheme, ip, port) probe.
|
|
type HTTPProbe struct {
|
|
Scheme string `json:"scheme"` // "http" or "https"
|
|
Host string `json:"host"`
|
|
IP string `json:"ip,omitempty"`
|
|
Port uint16 `json:"port"`
|
|
Address string `json:"address"`
|
|
IsIPv6 bool `json:"ipv6,omitempty"`
|
|
|
|
TCPConnected bool `json:"tcp_connected"`
|
|
ElapsedMS int64 `json:"elapsed_ms,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
|
|
// Final response after following redirects (if any).
|
|
StatusCode int `json:"status_code,omitempty"`
|
|
FinalURL string `json:"final_url,omitempty"`
|
|
Headers map[string]string `json:"headers,omitempty"`
|
|
Cookies []CookieInfo `json:"cookies,omitempty"`
|
|
RedirectChain []RedirectStep `json:"redirect_chain,omitempty"`
|
|
|
|
// Parsed HTML resource references (only populated for the primary
|
|
// HTTPS probe, to keep payloads small).
|
|
Resources []HTMLResource `json:"resources,omitempty"`
|
|
HTMLBytes int `json:"html_bytes,omitempty"`
|
|
BodyTruncated bool `json:"body_truncated,omitempty"`
|
|
}
|
|
|
|
// RedirectStep records one hop of a redirect chain.
|
|
type RedirectStep struct {
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Status int `json:"status"`
|
|
}
|
|
|
|
// CookieInfo summarises one Set-Cookie header.
|
|
type CookieInfo struct {
|
|
Name string `json:"name"`
|
|
Domain string `json:"domain,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
Secure bool `json:"secure"`
|
|
HttpOnly bool `json:"http_only"`
|
|
SameSite string `json:"same_site,omitempty"` // "Strict", "Lax", "None", or ""
|
|
HasExpiry bool `json:"has_expiry,omitempty"`
|
|
// Size is the byte length of the raw Set-Cookie header value
|
|
// (everything after "Set-Cookie: "), used to evaluate the
|
|
// per-cookie 4096-byte budget RFC 6265 §6.1 says browsers SHOULD
|
|
// support.
|
|
Size int `json:"size,omitempty"`
|
|
}
|
|
|
|
// MaxCookieSize is the per-cookie size browsers are required to
|
|
// support per RFC 6265 §6.1. Cookies above this are likely to be
|
|
// silently dropped by some user agents.
|
|
const MaxCookieSize = 4096
|
|
|
|
// HTMLResource is a <script src=...> or <link href=...> reference extracted
|
|
// from the HTML body, used to evaluate Subresource Integrity coverage.
|
|
type HTMLResource struct {
|
|
Tag string `json:"tag"` // "script" or "link"
|
|
URL string `json:"url"`
|
|
CrossOrigin bool `json:"cross_origin"`
|
|
Integrity string `json:"integrity,omitempty"`
|
|
Rel string `json:"rel,omitempty"`
|
|
}
|
|
|
|
// Severity levels used internally by rules.
|
|
const (
|
|
SeverityCrit = "crit"
|
|
SeverityWarn = "warn"
|
|
SeverityInfo = "info"
|
|
SeverityOK = "ok"
|
|
)
|