internal/server/proxy_host_form.go
Rama main · réplica de lectura del repositorio original.
// SPDX-License-Identifier: Apache-2.0
package server
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/X4Applegate/caddyui/internal/dns"
"github.com/X4Applegate/caddyui/internal/models"
)
// Proxy-host form parsing.
//
// v2.30.0: moved verbatim out of server.go. parseProxyHostForm is ~660 lines
// on its own — the single largest function in the codebase — and server.go
// was over 16,600 lines. Nothing here changed except the file it lives in.
// clampAtoi parses a form value as an int and constrains it to [min, max].
// Blank, unparseable, or non-positive input yields zeroValue, which callers
// use as their "unset — fall back to the built-in default" sentinel. A value
// that parses but falls outside the range is pulled to the nearest bound
// rather than rejected, so a typo in one numeric field can't fail the whole
// form save.
//
// v2.28.0 (issue #39).
func clampAtoi(raw string, zeroValue, min, max int) int {
v, err := strconv.Atoi(strings.TrimSpace(raw))
if err != nil || v <= 0 {
return zeroValue
}
if v < min {
return min
}
if v > max {
return max
}
return v
}
func parseProxyHostForm(r *http.Request) (*models.ProxyHost, error) {
_ = r.ParseForm()
port, err := strconv.Atoi(r.FormValue("forward_port"))
if err != nil {
return nil, err
}
certID, _ := strconv.ParseInt(r.FormValue("certificate_id"), 10, 64)
// DNS picker is now a two-field combo: dns_provider selects which
// provider (or "" for none) and dns_zone_id is the provider-native
// zone identifier. For human display the zone_name is also captured —
// the form stashes it in a hidden input whenever the picker changes.
provider := strings.ToLower(strings.TrimSpace(r.FormValue("dns_provider")))
profileID := strings.TrimSpace(r.FormValue("dns_profile_id"))
zoneID := ""
zoneName := ""
if provider != "" {
if _, ok := dns.Lookup(provider); !ok {
provider = "" // unknown ID → treat as "no DNS"
} else {
zoneID = strings.TrimSpace(r.FormValue("dns_zone_id"))
zoneName = strings.TrimSpace(r.FormValue("dns_zone_name"))
if zoneID == "" {
// Picker never captured a zone; treat the whole thing as
// unset so we don't try to create a record with no target.
provider = ""
zoneName = ""
}
if zoneName == "" {
// PB/DO/GD/NC use ID == Name; fall back transparently.
zoneName = zoneID
}
}
}
// v2.9.0: validate TLS min version — only accept known values.
tlsMinVersion := strings.TrimSpace(r.FormValue("tls_min_version"))
switch tlsMinVersion {
case "", "1.0", "1.1", "1.2", "1.3":
// valid
default:
tlsMinVersion = ""
}
// v2.64.0 (discussion #137): client certificate authentication (mTLS).
// An unknown mode turns the feature off rather than reaching Caddy and
// having the whole config rejected.
clientAuthMode := strings.TrimSpace(r.FormValue("client_auth_mode"))
if !models.ValidClientAuthMode(clientAuthMode) {
clientAuthMode = ""
}
clientAuthCAFiles := strings.TrimSpace(r.FormValue("client_auth_ca_files"))
if clientAuthMode == "" {
// Keep the stored pair consistent: no mode, no trust pool.
clientAuthCAFiles = ""
}
if models.ClientAuthModeVerifies(clientAuthMode) && clientAuthCAFiles == "" {
// Caddy would accept this and then reject every client, because a
// verifying mode with no trust pool trusts nothing. Fail the save
// with an explanation instead of silently locking the host.
return nil, fmt.Errorf("client certificate authentication: mode %q verifies the client certificate, so it needs at least one trusted CA certificate file — add one, or use \"request\" / \"require\" to ask for a certificate without verifying its issuer", clientAuthMode)
}
// Parse custom request headers: parallel arrays header_req_key[] + header_req_val[]
reqKeys := r.Form["header_req_key"]
reqVals := r.Form["header_req_val"]
reqMap := map[string]string{}
for i, k := range reqKeys {
k = strings.TrimSpace(k)
if k == "" {
continue
}
v := ""
if i < len(reqVals) {
v = strings.TrimSpace(reqVals[i])
}
reqMap[k] = v
}
customReqHeaders := "{}"
if len(reqMap) > 0 {
if b, err := json.Marshal(reqMap); err == nil {
customReqHeaders = string(b)
}
}
// Parse custom response headers: parallel arrays header_resp_key[] + header_resp_val[]
respKeys := r.Form["header_resp_key"]
respVals := r.Form["header_resp_val"]
respMap := map[string]string{}
for i, k := range respKeys {
k = strings.TrimSpace(k)
if k == "" {
continue
}
v := ""
if i < len(respVals) {
v = strings.TrimSpace(respVals[i])
}
respMap[k] = v
}
customRespHeaders := "{}"
if len(respMap) > 0 {
if b, err := json.Marshal(respMap); err == nil {
customRespHeaders = string(b)
}
}
// v2.9.2: URL rewrite rules — submitted as parallel arrays:
// rewrite_type[], rewrite_from[], rewrite_to[]
rewriteTypes := r.Form["rewrite_type[]"]
rewriteFroms := r.Form["rewrite_from[]"]
rewriteTos := r.Form["rewrite_to[]"]
var urlRewrites string
{
type rule struct {
Type string `json:"type"`
From string `json:"from"`
To string `json:"to"`
}
var rules []rule
for i := range rewriteTypes {
from := ""
to := ""
if i < len(rewriteFroms) {
from = strings.TrimSpace(rewriteFroms[i])
}
if i < len(rewriteTos) {
to = strings.TrimSpace(rewriteTos[i])
}
t := strings.TrimSpace(rewriteTypes[i])
if t == "" || from == "" {
continue
}
rules = append(rules, rule{Type: t, From: from, To: to})
}
if len(rules) == 0 {
urlRewrites = "[]"
} else {
b, _ := json.Marshal(rules)
urlRewrites = string(b)
}
}
var maxBodyMB int
if v := strings.TrimSpace(r.FormValue("max_request_body_mb")); v != "" && v != "0" {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
maxBodyMB = n
}
}
var upstreamTimeoutSec int
if v := strings.TrimSpace(r.FormValue("upstream_timeout_sec")); v != "" && v != "0" {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
upstreamTimeoutSec = n
}
}
var healthCheckIntervalSec int = 30
if v := strings.TrimSpace(r.FormValue("health_check_interval_sec")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
healthCheckIntervalSec = n
}
}
var keepaliveConns int
if v := strings.TrimSpace(r.FormValue("keepalive_conns")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
keepaliveConns = n
}
}
ph := &models.ProxyHost{
Domains: strings.TrimSpace(r.FormValue("domains")),
ForwardScheme: r.FormValue("forward_scheme"),
ForwardHost: strings.TrimSpace(r.FormValue("forward_host")),
ForwardPort: port,
WebsocketSupport: r.FormValue("websocket_support") == "on",
BlockCommonExploits: r.FormValue("block_common_exploits") == "on",
SSLEnabled: r.FormValue("ssl_enabled") == "on",
SSLForced: r.FormValue("ssl_forced") == "on",
HTTP2Support: r.FormValue("http2_support") == "on",
AdvancedConfig: normalizeProxyAdvancedConfig(r.FormValue("advanced_config")), // v2.42.1 / v2.42.2
Enabled: r.FormValue("enabled") == "on",
CertificateID: certID,
AccessList: strings.TrimSpace(r.FormValue("access_list")),
DNSProvider: provider,
DNSZoneID: zoneID,
DNSZoneName: zoneName,
DNSProfileID: profileID,
DNSSkipRecord: provider != "" && r.FormValue("dns_create_record") != "on",
CompressionEnabled: r.FormValue("compression_enabled") == "on",
SecurityHeadersEnabled: r.FormValue("security_headers_enabled") == "on",
MaintenanceMode: r.FormValue("maintenance_mode") == "on",
MaintenanceMsg: strings.TrimSpace(r.FormValue("maintenance_msg")),
MaintenanceStatusCode: func() int {
if sc, err := strconv.Atoi(r.FormValue("maintenance_status_code")); err == nil && sc > 0 {
return sc
}
return 503
}(),
StickySessions: r.FormValue("sticky_sessions") == "on",
TLSMinVersion: tlsMinVersion,
ClientAuthMode: clientAuthMode,
ClientAuthCAFiles: clientAuthCAFiles,
CustomReqHeaders: customReqHeaders,
CustomRespHeaders: customRespHeaders,
URLRewrites: urlRewrites,
MaxRequestBodyMB: maxBodyMB,
UpstreamTimeoutSec: upstreamTimeoutSec,
CORSEnabled: r.FormValue("cors_enabled") == "on",
CORSOrigins: strings.TrimSpace(r.FormValue("cors_origins")),
HealthCheckURI: strings.TrimSpace(r.FormValue("health_check_uri")),
HealthCheckIntervalSec: healthCheckIntervalSec,
HealthCheckMethod: func() string {
m := r.FormValue("health_check_method")
if m == "" {
return "GET"
}
return m
}(),
KeepaliveConns: keepaliveConns,
Tags: strings.TrimSpace(r.FormValue("tags")),
Notes: r.FormValue("notes"),
DisableAccessLog: r.FormValue("disable_access_log") == "on",
AddRequestID: r.FormValue("add_request_id") == "on",
StripRespHeaders: strings.TrimSpace(r.FormValue("strip_resp_headers")),
BlockedAgents: strings.TrimSpace(r.FormValue("blocked_agents")),
ResponseCacheControl: strings.TrimSpace(r.FormValue("response_cache_control")),
UpstreamSNI: strings.TrimSpace(r.FormValue("upstream_sni")),
HSTSPreload: r.FormValue("hsts_preload") == "on",
MaxConnsPerHost: func() int {
n, _ := strconv.Atoi(r.FormValue("max_conns_per_host"))
return n
}(),
}
hcTimeout, _ := strconv.Atoi(r.FormValue("health_check_timeout_sec"))
ph.HealthCheckTimeoutSec = hcTimeout
retries, _ := strconv.Atoi(r.FormValue("upstream_retries"))
ph.UpstreamRetries = retries
ph.ForceHTTP1 = r.FormValue("force_http1") == "on"
ph.BasicAuthRealm = strings.TrimSpace(r.FormValue("basicauth_realm"))
if ph.BasicAuthRealm == "" {
ph.BasicAuthRealm = "Restricted"
}
ph.ErrorPageHTML = r.FormValue("error_page_html")
ph.MaintenanceWindowStart = strings.TrimSpace(r.FormValue("maintenance_window_start"))
ph.MaintenanceWindowEnd = strings.TrimSpace(r.FormValue("maintenance_window_end"))
ph.MaintenanceWindowDays = strings.Join(r.Form["maintenance_window_days"], ",")
ph.IPBlocklist = strings.TrimSpace(r.FormValue("ip_blocklist"))
ph.LBPolicy = r.FormValue("lb_policy")
ph.ProxyProtocol = r.FormValue("proxy_protocol")
ph.RobotsTxt = r.FormValue("robots_txt")
ph.PassiveFailDurationSec, _ = strconv.Atoi(r.FormValue("passive_fail_duration_sec"))
ph.PassiveMaxFails, _ = strconv.Atoi(r.FormValue("passive_max_fails"))
ph.HSTSMaxAgeSec, _ = strconv.Atoi(r.FormValue("hsts_max_age_sec"))
ph.CSPHeader = strings.TrimSpace(r.FormValue("csp_header"))
ph.H2CEnabled = r.FormValue("h2c_enabled") == "on"
if hch := strings.TrimSpace(r.FormValue("health_check_headers")); hch != "" {
ph.HealthCheckHeaders = hch
} else {
ph.HealthCheckHeaders = "{}"
}
ph.FlushImmediate = r.FormValue("flush_immediate") == "on"
ph.BufferResponses = r.FormValue("buffer_responses") == "on"
ph.TrustedProxies = strings.TrimSpace(r.FormValue("trusted_proxies"))
ph.UpstreamHostOverride = strings.TrimSpace(r.FormValue("upstream_host_override"))
ph.ReadTimeoutSec, _ = strconv.Atoi(r.FormValue("read_timeout_sec"))
ph.DenyDotfiles = r.FormValue("deny_dotfiles") == "on"
ph.RequestBuffersKB, _ = strconv.Atoi(r.FormValue("request_buffers_kb"))
ph.CORSAllowCredentials = r.FormValue("cors_allow_credentials") == "on"
ph.CORSExposeHeaders = strings.TrimSpace(r.FormValue("cors_expose_headers"))
ph.SSLVerifyUpstream = r.FormValue("ssl_verify_upstream") == "on"
ph.DialTimeoutSec, _ = strconv.Atoi(r.FormValue("dial_timeout_sec"))
ph.APIKeyHeader = strings.TrimSpace(r.FormValue("api_key_header"))
ph.APIKeyValue = strings.TrimSpace(r.FormValue("api_key_value"))
ph.BlockEmptyUserAgent = r.FormValue("block_empty_user_agent") == "on"
ph.ErrorRedirectURL = strings.TrimSpace(r.FormValue("error_redirect_url"))
ph.PermissionsPolicy = strings.TrimSpace(r.FormValue("permissions_policy"))
ph.XFrameOptions = strings.TrimSpace(r.FormValue("x_frame_options"))
ph.ReferrerPolicy = strings.TrimSpace(r.FormValue("referrer_policy"))
ph.HSTSIncludeSubdomains = r.FormValue("hsts_include_subdomains") == "on"
ph.CSPReportOnly = strings.TrimSpace(r.FormValue("csp_report_only"))
ph.KeepaliveIdleTimeoutSec, _ = strconv.Atoi(r.FormValue("keepalive_idle_timeout_sec"))
ph.HealthCheckExpectStatus, _ = strconv.Atoi(r.FormValue("health_check_expect_status"))
ph.HealthCheckExpectBody = strings.TrimSpace(r.FormValue("health_check_expect_body"))
ph.HealthCheckFollowRedirects = r.FormValue("health_check_follow_redirects") == "on"
ph.PathMatcher = strings.TrimSpace(r.FormValue("path_matcher"))
ph.StripPathPrefix = r.FormValue("strip_path_prefix") == "on"
ph.StickyCookieName = strings.TrimSpace(r.FormValue("sticky_cookie_name"))
ph.LBTryDurationSec, _ = strconv.Atoi(r.FormValue("lb_try_duration_sec"))
ph.LBTryIntervalMS, _ = strconv.Atoi(r.FormValue("lb_try_interval_ms"))
ph.CompressionMinSizeKB, _ = strconv.Atoi(r.FormValue("compression_min_size_kb"))
ph.ForwardClientIP = r.FormValue("forward_client_ip") == "on"
ph.CORSMaxAgeSec, _ = strconv.Atoi(r.FormValue("cors_max_age_sec"))
ph.CORSAllowMethods = strings.TrimSpace(r.FormValue("cors_allow_methods"))
ph.CORSAllowHeaders = strings.TrimSpace(r.FormValue("cors_allow_headers"))
ph.RetryStatusCodes = strings.TrimSpace(r.FormValue("retry_status_codes"))
ph.WriteTimeoutSec, _ = strconv.Atoi(r.FormValue("write_timeout_sec"))
ph.UpstreamTLSMinVersion = r.FormValue("upstream_tls_min_version")
ph.ForwardProxyURL = strings.TrimSpace(r.FormValue("forward_proxy_url"))
ph.BlockedMethods = strings.TrimSpace(r.FormValue("blocked_methods"))
ph.ForwardAuthURL = strings.TrimSpace(r.FormValue("forward_auth_url"))
ph.ForwardAuthCopyHeaders = strings.TrimSpace(r.FormValue("forward_auth_copy_headers"))
ph.StripQueryString = r.FormValue("strip_query_string") == "on"
ph.DeleteQueryParams = strings.TrimSpace(r.FormValue("delete_query_params"))
ph.RequestBodyReadTimeoutSec, _ = strconv.Atoi(r.FormValue("request_body_read_timeout_sec"))
ph.ResponseHeaderTimeoutSec, _ = strconv.Atoi(r.FormValue("response_header_timeout_sec"))
ph.MaxConnDurationSec, _ = strconv.Atoi(r.FormValue("max_conn_duration_sec"))
ph.DecompressResponse = r.FormValue("decompress_response") == "on"
ph.Color = strings.TrimSpace(r.FormValue("color"))
ph.WWWRedirect = r.FormValue("www_redirect") // "" | "to_www" | "to_bare"
ph.StripReqHeaders = strings.TrimSpace(r.FormValue("strip_req_headers"))
ph.UpstreamPathPrefix = strings.TrimSpace(r.FormValue("upstream_path_prefix"))
ph.CompressionLevel, _ = strconv.Atoi(r.FormValue("compression_level"))
ph.CompressionPreferGzip = r.FormValue("compression_prefer_gzip") == "on"
ph.SortOrder, _ = strconv.Atoi(r.FormValue("sort_order"))
ph.AllowedMethods = strings.TrimSpace(r.FormValue("allowed_methods"))
ph.UpstreamMaxRespHeaderKB, _ = strconv.Atoi(r.FormValue("upstream_max_resp_header_kb"))
ph.HealthCheckPort, _ = strconv.Atoi(r.FormValue("health_check_port"))
ph.RequestIDHeaderName = strings.TrimSpace(r.FormValue("request_id_header_name"))
ph.LBCookiePath = strings.TrimSpace(r.FormValue("lb_cookie_path"))
ph.PassiveUnhealthyLatencyMS, _ = strconv.Atoi(r.FormValue("passive_unhealthy_latency_ms"))
ph.TLSHandshakeTimeoutSec, _ = strconv.Atoi(r.FormValue("tls_handshake_timeout_sec"))
ph.ExpectContinueTimeoutSec, _ = strconv.Atoi(r.FormValue("expect_continue_timeout_sec"))
ph.ResponseBuffersKB, _ = strconv.Atoi(r.FormValue("response_buffers_kb"))
ph.UpstreamMaxIdleConns, _ = strconv.Atoi(r.FormValue("upstream_max_idle_conns"))
ph.UpstreamKeepAliveProbeIntervalSec, _ = strconv.Atoi(r.FormValue("upstream_keep_alive_probe_sec"))
ph.ForwardAuthMethod = strings.TrimSpace(r.FormValue("forward_auth_method"))
ph.GRPCWebEnabled = r.FormValue("grpc_web_enabled") == "on"
ph.ForwardAuthHeadersPrefix = strings.TrimSpace(r.FormValue("forward_auth_headers_prefix"))
ph.HealthCheckMaxSizeKB, _ = strconv.Atoi(r.FormValue("health_check_max_size_kb"))
ph.StripPathSuffix = strings.TrimSpace(r.FormValue("strip_path_suffix"))
ph.AddReqQueryParams = strings.TrimSpace(r.FormValue("add_req_query_params"))
ph.ErrorPageCodes = strings.TrimSpace(r.FormValue("error_page_codes"))
ph.UpstreamTLSCAPEMFile = strings.TrimSpace(r.FormValue("upstream_tls_ca_pem_file"))
ph.KeepaliveDisabled = r.FormValue("keepalive_disabled") == "on"
ph.TrailingSlashRedirect = r.FormValue("trailing_slash_redirect")
ph.DialFallbackDelayMS, _ = strconv.Atoi(r.FormValue("dial_fallback_delay_ms"))
ph.UpstreamNetwork = strings.TrimSpace(r.FormValue("upstream_network"))
ph.DNSResolver = strings.TrimSpace(r.FormValue("dns_resolver"))
ph.PathMatcherType = r.FormValue("path_matcher_type")
ph.CORSAllowPrivateNetwork = r.FormValue("cors_allow_private_network") == "on"
ph.RobotsTxtDisallowAll = r.FormValue("robots_txt_disallow_all") == "on"
ph.MaintenanceRetryAfterSec, _ = strconv.Atoi(r.FormValue("maintenance_retry_after_sec"))
ph.UpstreamResolveTimeoutSec, _ = strconv.Atoi(r.FormValue("upstream_resolve_timeout_sec"))
ph.UpstreamReadBufferSizeKB, _ = strconv.Atoi(r.FormValue("upstream_read_buffer_size_kb"))
ph.UpstreamWriteBufferSizeKB, _ = strconv.Atoi(r.FormValue("upstream_write_buffer_size_kb"))
ph.ReqHeaderReplace = strings.TrimSpace(r.FormValue("req_header_replace"))
ph.RespHeaderReplace = strings.TrimSpace(r.FormValue("resp_header_replace"))
ph.UpstreamHTTPVersions = strings.TrimSpace(r.FormValue("upstream_http_versions"))
ph.HealthCheckBody = strings.TrimSpace(r.FormValue("health_check_body"))
ph.AddCanonicalLinkHeader = r.FormValue("add_canonical_link_header") == "on"
ph.HTTPBasicAuthUpstream = strings.TrimSpace(r.FormValue("http_basic_auth_upstream"))
ph.BlockUARegexp = strings.TrimSpace(r.FormValue("block_ua_regexp"))
ph.SecurityTxtBody = strings.TrimSpace(r.FormValue("security_txt_body"))
ph.ServerHeaderValue = strings.TrimSpace(r.FormValue("server_header_value"))
ph.XRobotsTag = strings.TrimSpace(r.FormValue("x_robots_tag"))
ph.AddForwardedHeader = r.FormValue("add_forwarded_header") == "on"
ph.LBCookieSecret = strings.TrimSpace(r.FormValue("lb_cookie_secret"))
ph.PassiveUnhealthyStatusCodes = strings.TrimSpace(r.FormValue("passive_unhealthy_status_codes"))
ph.HealthCheckContentType = strings.TrimSpace(r.FormValue("health_check_content_type"))
ph.UpstreamTLSClientCertFile = strings.TrimSpace(r.FormValue("upstream_tls_client_cert_file"))
ph.UpstreamTLSClientKeyFile = strings.TrimSpace(r.FormValue("upstream_tls_client_key_file"))
ph.BlockPrivateIPs = r.FormValue("block_private_ips") == "on"
ph.EnableBrotli = r.FormValue("enable_brotli") == "on"
ph.VaryHeader = strings.TrimSpace(r.FormValue("vary_header"))
ph.StripETag = r.FormValue("strip_etag") == "on"
ph.HTTP2PushPaths = strings.TrimSpace(r.FormValue("http2_push_paths"))
ph.DenyContentTypes = strings.TrimSpace(r.FormValue("deny_content_types"))
ph.UpstreamLocalAddr = strings.TrimSpace(r.FormValue("upstream_local_addr"))
ph.UpstreamTLSRenegotiation = r.FormValue("upstream_tls_renegotiation")
ph.UpstreamTLSCurves = strings.TrimSpace(r.FormValue("upstream_tls_curves"))
ph.UpstreamTLSMaxVersion = r.FormValue("upstream_tls_max_version")
ph.UpstreamTLSPins = strings.TrimSpace(r.FormValue("upstream_tls_pins"))
ph.LBHeaderField = strings.TrimSpace(r.FormValue("lb_header_field"))
ph.MaintenanceCustomHeaders = strings.TrimSpace(r.FormValue("maintenance_custom_headers"))
ph.DenyExtensions = strings.TrimSpace(r.FormValue("deny_extensions"))
ph.InjectRequestTimestamp = r.FormValue("inject_request_timestamp") == "on"
ph.AddRespCookies = strings.TrimSpace(r.FormValue("add_resp_cookies"))
ph.StripAcceptEncoding = r.FormValue("strip_accept_encoding") == "on"
ph.AddUpstreamTimingHeader = r.FormValue("add_upstream_timing_header") == "on"
ph.StripServerHeader = r.FormValue("strip_server_header") == "on"
ph.BlockRefererRegexp = strings.TrimSpace(r.FormValue("block_referer_regexp"))
ph.AddContentTypeNosniff = r.FormValue("add_content_type_nosniff") == "on"
ph.StripAuthorizationHeader = r.FormValue("strip_authorization_header") == "on"
ph.RealIPFromHeader = strings.TrimSpace(r.FormValue("real_ip_from_header"))
ph.HealthCheckHostOverride = strings.TrimSpace(r.FormValue("health_check_host_override"))
ph.AddXForwardedPort = r.FormValue("add_x_forwarded_port") == "on"
ph.LBRetryOn = strings.TrimSpace(r.FormValue("lb_retry_on"))
if v := strings.TrimSpace(r.FormValue("max_buffer_size_kb")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
ph.MaxBufferSizeKB = n
}
}
if v := strings.TrimSpace(r.FormValue("upstream_keepalive_probes")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
ph.UpstreamKeepaliveProbes = n
}
}
if v := strings.TrimSpace(r.FormValue("upstream_flush_interval_ms")); v != "" {
if n, err := strconv.Atoi(v); err == nil {
ph.UpstreamFlushIntervalMS = n
}
}
ph.AddXForwardedHost = r.FormValue("add_x_forwarded_host") == "on"
ph.MaintenanceAllowedIPs = strings.TrimSpace(r.FormValue("maintenance_allowed_ips"))
ph.UpstreamTLSCipherSuites = strings.TrimSpace(r.FormValue("upstream_tls_cipher_suites"))
ph.AddCacheControlNoStore = r.FormValue("add_cache_control_no_store") == "on"
ph.DenyRefererEmpty = r.FormValue("deny_referer_empty") == "on"
ph.LBCookieHTTPOnly = r.FormValue("lb_cookie_httponly") == "on"
ph.LBCookieSecure = r.FormValue("lb_cookie_secure") == "on"
ph.LBCookieSameSite = r.FormValue("lb_cookie_same_site")
ph.UpstreamTLSEarlyData = r.FormValue("upstream_tls_early_data") == "on"
ph.AddViaHeader = r.FormValue("add_via_header") == "on"
ph.ReqHeaderRename = strings.TrimSpace(r.FormValue("req_header_rename"))
ph.AddExpectCTHeader = r.FormValue("add_expect_ct_header") == "on"
ph.ForceUpstreamEncoding = strings.TrimSpace(r.FormValue("force_upstream_encoding"))
if v := strings.TrimSpace(r.FormValue("passive_unhealthy_count")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
ph.PassiveUnhealthyCount = n
}
}
ph.StripXPoweredBy = r.FormValue("strip_x_powered_by") == "on"
ph.AddTimingAllowOrigin = strings.TrimSpace(r.FormValue("add_timing_allow_origin"))
if v := strings.TrimSpace(r.FormValue("lb_cookie_max_age_sec")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
ph.LBCookieMaxAgeSec = n
}
}
ph.CrossOriginOpenerPolicy = strings.TrimSpace(r.FormValue("cross_origin_opener_policy"))
ph.CrossOriginResourcePolicy = strings.TrimSpace(r.FormValue("cross_origin_resource_policy"))
ph.CrossOriginEmbedderPolicy = strings.TrimSpace(r.FormValue("cross_origin_embedder_policy"))
ph.DenyRequestContentType = strings.TrimSpace(r.FormValue("deny_request_content_type"))
ph.CompressionExcludeRegexp = strings.TrimSpace(r.FormValue("compression_exclude_regexp"))
ph.AddCacheControlPublic = r.FormValue("add_cache_control_public") == "on"
// v2.9.140: add_x_request_start — inject X-Request-Start: t=<epoch_ms> for APM timing.
ph.AddXRequestStart = r.FormValue("add_x_request_start") == "on"
// v2.9.141: maintenance_window_timezone — IANA timezone for the scheduled maintenance window.
ph.MaintenanceWindowTimezone = strings.TrimSpace(r.FormValue("maintenance_window_timezone"))
// v2.9.142: lb_random_choose_count — "choose" count for the random_choice lb policy.
if v := strings.TrimSpace(r.FormValue("lb_random_choose_count")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
ph.LBRandomChooseCount = n
}
}
// v2.9.143: add_x_forwarded_scheme — inject X-Forwarded-Scheme with the client-facing scheme.
ph.AddXForwardedScheme = r.FormValue("add_x_forwarded_scheme") == "on"
// v2.9.144: response_cache_ttl_sec — set Cache-Control: max-age=N (0 = disabled).
if v := strings.TrimSpace(r.FormValue("response_cache_ttl_sec")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
ph.ResponseCacheTTLSec = n
}
}
// v2.9.145: add_link_preload — Link response header for HTTP/2 preload hints.
ph.AddLinkPreload = strings.TrimSpace(r.FormValue("add_link_preload"))
// v2.9.146: deny_path_regexp — block requests whose path matches this regex (403).
ph.DenyPathRegexp = strings.TrimSpace(r.FormValue("deny_path_regexp"))
// v2.9.147: add_request_id_to_response — echo request trace ID in response header.
ph.AddRequestIDToResponse = r.FormValue("add_request_id_to_response") == "on"
// v2.9.148: health_check_tls_server_name — TLS SNI override for health check connections.
ph.HealthCheckTLSServerName = strings.TrimSpace(r.FormValue("health_check_tls_server_name"))
// v2.9.149: add_x_real_ip — inject X-Real-IP with the direct client IP.
ph.AddXRealIP = r.FormValue("add_x_real_ip") == "on"
// v2.9.150: strip_incoming_x_forwarded_for — delete incoming X-Forwarded-For.
ph.StripIncomingXForwardedFor = r.FormValue("strip_incoming_x_forwarded_for") == "on"
// v2.9.151: health_check_tls_insecure_skip_verify — skip TLS cert verification for health check probes.
ph.HealthCheckTLSInsecureSkipVerify = r.FormValue("health_check_tls_insecure_skip_verify") == "on"
// v2.9.152: add_cors_vary_header — add Vary: Origin response header for CDN caching of CORS responses.
ph.AddCORSVaryHeader = r.FormValue("add_cors_vary_header") == "on"
// v2.9.153: upstream_tls_alpn — ALPN protocol list for upstream TLS connections.
ph.UpstreamTLSALPN = strings.TrimSpace(r.FormValue("upstream_tls_alpn"))
// v2.9.154: add_x_powered_by — custom X-Powered-By response header value.
ph.AddXPoweredBy = strings.TrimSpace(r.FormValue("add_x_powered_by"))
// v2.9.155: block_query_params — comma-separated query param names to block (403).
ph.BlockQueryParams = strings.TrimSpace(r.FormValue("block_query_params"))
// v2.9.156: add_document_policy — Document-Policy response header value.
ph.AddDocumentPolicy = strings.TrimSpace(r.FormValue("add_document_policy"))
// v2.9.157: maintenance_redirect_url — redirect to this URL during maintenance.
ph.MaintenanceRedirectURL = strings.TrimSpace(r.FormValue("maintenance_redirect_url"))
// v2.9.158: upstream_keepalive_max_lifetime_sec — max keepalive connection lifetime.
if v := strings.TrimSpace(r.FormValue("upstream_keepalive_max_lifetime_sec")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
ph.UpstreamKeepaliveMaxLifetimeSec = n
}
}
// v2.9.159: add_origin_header — inject Origin request header.
ph.AddOriginHeader = strings.TrimSpace(r.FormValue("add_origin_header"))
// v2.9.160: upstream_tls_ca_pem_inline — inline PEM CA certificate for upstream TLS.
ph.UpstreamTLSCAPEMInline = strings.TrimSpace(r.FormValue("upstream_tls_ca_pem_inline"))
// v2.9.161: add_server_timing_header — inject Server-Timing with upstream duration.
ph.AddServerTimingHeader = r.FormValue("add_server_timing_header") == "on"
// v2.9.162: add_clear_site_data — Clear-Site-Data response header value.
ph.AddClearSiteData = strings.TrimSpace(r.FormValue("add_clear_site_data"))
// v2.9.163: add_x_dns_prefetch_control — set X-DNS-Prefetch-Control: off.
ph.AddXDNSPrefetchControl = r.FormValue("add_x_dns_prefetch_control") == "on"
// v2.9.164: add_accept_ranges — signal byte-range support with Accept-Ranges: bytes.
ph.AddAcceptRanges = r.FormValue("add_accept_ranges") == "on"
// v2.9.165: add_content_disposition — set a custom Content-Disposition response header.
ph.AddContentDisposition = strings.TrimSpace(r.FormValue("add_content_disposition"))
// v2.9.166: upstream_tls_server_name_from_host — use Host header value as upstream TLS SNI.
ph.UpstreamTLSServerNameFromHost = r.FormValue("upstream_tls_server_name_from_host") == "on"
// v2.9.167: add_x_permitted_cross_domain_policies — X-Permitted-Cross-Domain-Policies response header value.
ph.AddXPermittedCrossDomainPolicies = strings.TrimSpace(r.FormValue("add_x_permitted_cross_domain_policies"))
// strip_response_headers (v2.9.168) duplicated strip_resp_headers above —
// same semantics, a second column, and only strip_resp_headers was
// understood by the Caddyfile exporter. The duplicate input is gone from the
// form and strip_resp_headers is canonical; the global-strip setting and the
// security-bundle precedence now read the canonical field.
//
// The legacy form value is still ACCEPTED for one release so an old
// bookmarked form or an API client posting the v2.9.168 name does not
// silently lose data; it is merged into the canonical field instead of being
// stored separately. The legacy column is no longer written.
//
// TODO(v2.70): drop this fallback and the StripResponseHeaders field, and
// drop the strip_response_headers column once no supported upgrade path
// still carries an unmigrated value.
if legacy := strings.TrimSpace(r.FormValue("strip_response_headers")); legacy != "" {
ph.StripRespHeaders = models.MergeStripResponseHeaders(ph.StripRespHeaders, legacy)
}
ph.StripResponseHeaders = ""
// v2.9.169: add_report_to — Report-To response header value (JSON endpoint group config).
ph.AddReportTo = strings.TrimSpace(r.FormValue("add_report_to"))
// v2.9.170: add_nel_header — NEL response header JSON config for Network Error Logging.
ph.AddNELHeader = strings.TrimSpace(r.FormValue("add_nel_header"))
// v2.9.171: block_http_methods — comma-separated HTTP methods to reject with 405.
ph.BlockHTTPMethods = strings.TrimSpace(r.FormValue("block_http_methods"))
// v2.9.172: add_service_worker_allowed — Service-Worker-Allowed response header value.
ph.AddServiceWorkerAllowed = strings.TrimSpace(r.FormValue("add_service_worker_allowed"))
// v2.9.173: add_accept_ch — Accept-CH response header to declare accepted client hints.
ph.AddAcceptCH = strings.TrimSpace(r.FormValue("add_accept_ch"))
// v2.9.174: add_alt_svc — Alt-Svc response header for alternate service advertisement.
ph.AddAltSvc = strings.TrimSpace(r.FormValue("add_alt_svc"))
// v2.9.175: add_content_language — Content-Language response header value.
ph.AddContentLanguage = strings.TrimSpace(r.FormValue("add_content_language"))
// v2.9.176: add_critical_ch — Critical-CH response header (marks client hints required before rendering).
ph.AddCriticalCH = strings.TrimSpace(r.FormValue("add_critical_ch"))
// v2.9.177: add_x_download_options — set X-Download-Options: noopen (IE file open prevention).
ph.AddXDownloadOptions = r.FormValue("add_x_download_options") == "on"
// v2.9.178: deny_user_agent_regexp — block requests whose User-Agent matches this regexp with 403.
ph.DenyUserAgentRegexp = strings.TrimSpace(r.FormValue("deny_user_agent_regexp"))
// v2.9.179: add_pragma_no_cache — set Pragma: no-cache response header.
ph.AddPragmaNoCache = r.FormValue("add_pragma_no_cache") == "on"
// v2.9.180: health_check_user_agent — custom User-Agent for active health check probes.
ph.HealthCheckUserAgent = strings.TrimSpace(r.FormValue("health_check_user_agent"))
// v2.9.181: add_x_request_path — inject X-Request-Path request header on upstream calls.
ph.AddXRequestPath = r.FormValue("add_x_request_path") == "on"
// v2.9.182: add_x_clacks_overhead — X-Clacks-Overhead response header value.
ph.AddXClacksOverhead = strings.TrimSpace(r.FormValue("add_x_clacks_overhead"))
// v2.9.183: add_x_ua_compatible — X-UA-Compatible response header value.
ph.AddXUACompatible = strings.TrimSpace(r.FormValue("add_x_ua_compatible"))
// v2.9.184: forward_auth_skip_paths — comma-separated path prefixes that bypass forward_auth.
ph.ForwardAuthSkipPaths = strings.TrimSpace(r.FormValue("forward_auth_skip_paths"))
// v2.9.185: add_age_zero — set Age: 0 response header to signal a fresh response.
ph.AddAgeZero = r.FormValue("add_age_zero") == "on"
// v2.9.186: add_surrogate_control — Surrogate-Control response header value (CDN-only cache directive).
ph.AddSurrogateControl = strings.TrimSpace(r.FormValue("add_surrogate_control"))
// v2.9.187: add_warning_header — Warning response header value.
ph.AddWarningHeader = strings.TrimSpace(r.FormValue("add_warning_header"))
// v2.9.188: add_x_request_method — forward X-Request-Method header (echoes HTTP method) to upstream.
ph.AddXRequestMethod = r.FormValue("add_x_request_method") == "on"
// v2.9.189: add_x_request_query — forward X-Request-Query header (echoes query string) to upstream.
ph.AddXRequestQuery = r.FormValue("add_x_request_query") == "on"
// v2.9.190: add_x_forwarded_user — static X-Forwarded-User request header value.
ph.AddXForwardedUser = strings.TrimSpace(r.FormValue("add_x_forwarded_user"))
// v2.9.191: add_x_real_scheme — forward X-Real-Scheme request header to upstream.
ph.AddXRealScheme = r.FormValue("add_x_real_scheme") == "on"
// v2.9.192: add_origin_agent_cluster — set Origin-Agent-Cluster: ?1 response header.
ph.AddOriginAgentCluster = r.FormValue("add_origin_agent_cluster") == "on"
// v2.9.193: add_x_forwarded_groups — static X-Forwarded-Groups request header value.
ph.AddXForwardedGroups = strings.TrimSpace(r.FormValue("add_x_forwarded_groups"))
// v2.9.194: add_x_forwarded_email — static X-Forwarded-Email request header value.
ph.AddXForwardedEmail = strings.TrimSpace(r.FormValue("add_x_forwarded_email"))
// v2.9.195: add_x_forwarded_roles — static X-Forwarded-Roles request header value.
ph.AddXForwardedRoles = strings.TrimSpace(r.FormValue("add_x_forwarded_roles"))
// v2.9.196: block_query_param_regexp — block requests whose query string matches this regexp with 403.
ph.BlockQueryParamRegexp = strings.TrimSpace(r.FormValue("block_query_param_regexp"))
// v2.9.197: add_x_request_referer — forward X-Request-Referer header to upstream.
ph.AddXRequestReferer = r.FormValue("add_x_request_referer") == "on"
// v2.9.198: add_x_request_origin — forward X-Request-Origin header to upstream.
ph.AddXRequestOrigin = r.FormValue("add_x_request_origin") == "on"
// v2.9.199: add_x_forwarded_uri — forward X-Forwarded-URI header to upstream.
ph.AddXForwardedURI = r.FormValue("add_x_forwarded_uri") == "on"
// v2.9.200: add_x_no_archive — set X-No-Archive: yes response header.
ph.AddXNoArchive = r.FormValue("add_x_no_archive") == "on"
// v2.9.201: add_x_request_hostname — forward X-Request-Hostname header to upstream.
ph.AddXRequestHostname = r.FormValue("add_x_request_hostname") == "on"
// v2.9.202: add_x_xss_protection_disabled — set X-XSS-Protection: 0 response header.
ph.AddXXSSProtectionDisabled = r.FormValue("add_x_xss_protection_disabled") == "on"
// v2.9.212: add_x_request_remote_port — forward X-Request-Remote-Port header to upstream.
ph.AddXRequestRemotePort = r.FormValue("add_x_request_remote_port") == "on"
// v2.9.213: add_x_request_protocol — forward X-Request-Protocol header (HTTP version) to upstream.
ph.AddXRequestProtocol = r.FormValue("add_x_request_protocol") == "on"
// v2.9.214: add_save_data_vary — append Save-Data to Vary response header.
ph.AddSaveDataVary = r.FormValue("add_save_data_vary") == "on"
// v2.9.217: add_x_environment — static X-Environment request header value.
ph.AddXEnvironment = strings.TrimSpace(r.FormValue("add_x_environment"))
// v2.9.218: add_x_trace_id — forward X-Trace-ID header (Caddy UUID per request) to upstream.
ph.AddXTraceID = r.FormValue("add_x_trace_id") == "on"
// v2.9.219: health_check_query_params — query string appended to active health check URL.
ph.HealthCheckQueryParams = strings.TrimSpace(r.FormValue("health_check_query_params"))
// v2.9.220: add_x_session_id — forward X-Session-ID header to upstream.
ph.AddXSessionID = r.FormValue("add_x_session_id") == "on"
// v2.9.221: add_x_response_trace_id — set X-Response-Trace-ID response header.
ph.AddXResponseTraceID = r.FormValue("add_x_response_trace_id") == "on"
// v2.9.222: add_x_request_local_addr — forward X-Local-Addr header to upstream.
ph.AddXRequestLocalAddr = r.FormValue("add_x_request_local_addr") == "on"
// v2.9.223: add_x_request_local_port — forward X-Local-Port header to upstream.
ph.AddXRequestLocalPort = r.FormValue("add_x_request_local_port") == "on"
// v2.9.224: add_x_request_path_info — forward X-PathInfo header to upstream.
ph.AddXRequestPathInfo = r.FormValue("add_x_request_path_info") == "on"
// v2.9.234: add_x_authenticated_user — static X-Authenticated-User request header.
ph.AddXAuthenticatedUser = strings.TrimSpace(r.FormValue("add_x_authenticated_user"))
// v2.9.235: block_path_extensions — comma-separated extensions to 403.
ph.BlockPathExtensions = strings.TrimSpace(r.FormValue("block_path_extensions"))
// v2.9.236: add_link_modulepreload — Link rel=modulepreload value.
ph.AddLinkModulePreload = strings.TrimSpace(r.FormValue("add_link_modulepreload"))
// v2.9.237: add_x_remote_user — static X-Remote-User request header.
ph.AddXRemoteUser = strings.TrimSpace(r.FormValue("add_x_remote_user"))
// v2.9.238: add_x_forwarded_path — forward X-Forwarded-Path header.
ph.AddXForwardedPath = r.FormValue("add_x_forwarded_path") == "on"
// v2.9.239: add_x_geo_country_code — static X-Geo-Country header.
ph.AddXGeoCountryCode = strings.TrimSpace(r.FormValue("add_x_geo_country_code"))
// v2.9.240: add_x_request_priority — X-Request-Priority response header (RFC 9218).
ph.AddXRequestPriority = strings.TrimSpace(r.FormValue("add_x_request_priority"))
// v2.9.241: health_check_basic_auth — "user:pass" credentials for health check probes.
ph.HealthCheckBasicAuth = strings.TrimSpace(r.FormValue("health_check_basic_auth"))
// v2.9.242: add_x_real_ssl_protocol — forward TLS version header.
ph.AddXRealSSLProtocol = r.FormValue("add_x_real_ssl_protocol") == "on"
// v2.9.243: add_x_real_ssl_cipher — forward negotiated cipher header.
ph.AddXRealSSLCipher = r.FormValue("add_x_real_ssl_cipher") == "on"
// v2.9.244: add_x_cache_status — static X-Cache-Status response header.
ph.AddXCacheStatus = strings.TrimSpace(r.FormValue("add_x_cache_status"))
// v2.9.245: deny_referer_regexp — block by Referer regexp with 403.
ph.DenyRefererRegexp = strings.TrimSpace(r.FormValue("deny_referer_regexp"))
// v2.9.246: add_x_request_user_agent — echo UA to upstream (debug).
ph.AddXRequestUserAgent = r.FormValue("add_x_request_user_agent") == "on"
// v2.9.247: add_reporting_endpoints — Reporting-Endpoints response header (RFC 8942).
ph.AddReportingEndpoints = strings.TrimSpace(r.FormValue("add_reporting_endpoints"))
// v2.9.248: add_x_request_byte_count — forward Content-Length as X-Request-Byte-Count.
ph.AddXRequestByteCount = r.FormValue("add_x_request_byte_count") == "on"
// v2.9.249: add_x_request_received_at — forward server-side timestamp.
ph.AddXRequestReceivedAt = r.FormValue("add_x_request_received_at") == "on"
// issue #167: strip_request_headers (v2.9.250) duplicated strip_req_headers
// above — same semantics, different column, and only strip_req_headers was
// understood by the Caddyfile exporter. The duplicate input is gone from the
// form and strip_req_headers is canonical.
//
// The legacy form value is still ACCEPTED for one release so an old
// bookmarked form or an API client posting the v2.9.250 name does not
// silently lose data; it is merged into the canonical field instead of
// being stored separately. The legacy column is no longer written.
//
// TODO(v2.70): drop this fallback and the StripRequestHeaders field, and
// drop the strip_request_headers column once no supported upgrade path
// still carries an unmigrated value.
if legacy := strings.TrimSpace(r.FormValue("strip_request_headers")); legacy != "" {
ph.StripReqHeaders = models.MergeStripRequestHeaders(ph.StripReqHeaders, legacy)
}
ph.StripRequestHeaders = ""
// v2.9.251: add_x_forwarded_method — forward HTTP method header.
ph.AddXForwardedMethod = r.FormValue("add_x_forwarded_method") == "on"
// v2.9.252: add_x_request_original_host — preserve original Host header.
ph.AddXRequestOriginalHost = r.FormValue("add_x_request_original_host") == "on"
// v2.9.253: add_x_request_dnt — forward DNT header.
ph.AddXRequestDNT = r.FormValue("add_x_request_dnt") == "on"
// v2.9.254: add_x_geo_region — static X-Geo-Region request header.
ph.AddXGeoRegion = strings.TrimSpace(r.FormValue("add_x_geo_region"))
// v2.9.255: add_x_request_secure — X-Request-Secure header based on TLS state.
ph.AddXRequestSecure = r.FormValue("add_x_request_secure") == "on"
// v2.9.256: add_x_request_query_count — debug header for query parameters.
ph.AddXRequestQueryCount = r.FormValue("add_x_request_query_count") == "on"
// v2.9.257: add_x_request_id_header_response — echo trace UUID to response header.
ph.AddXRequestIDHeaderResponse = r.FormValue("add_x_request_id_header_response") == "on"
// v2.9.258: force_canonical_host — canonical host for SEO-style consolidation.
ph.ForceCanonicalHost = strings.TrimSpace(r.FormValue("force_canonical_host"))
// v2.9.259: add_x_robots_noindex_quick — X-Robots-Tag: noindex, nofollow.
ph.AddXRobotsNoindexQuick = r.FormValue("add_x_robots_noindex_quick") == "on"
// v2.9.260: block_bot_user_agents — built-in bot blocklist.
ph.BlockBotUserAgents = r.FormValue("block_bot_user_agents") == "on"
// v2.9.261: block_admin_paths — 404 common admin paths.
ph.BlockAdminPaths = r.FormValue("block_admin_paths") == "on"
// v2.9.262: add_link_dns_prefetch — Link rel=dns-prefetch header.
ph.AddLinkDNSPrefetch = strings.TrimSpace(r.FormValue("add_link_dns_prefetch"))
// v2.9.263: add_link_preconnect — Link rel=preconnect header.
ph.AddLinkPreconnect = strings.TrimSpace(r.FormValue("add_link_preconnect"))
// v2.9.264: add_x_csp_disabled — strip Content-Security-Policy from response.
ph.AddXCSPDisabled = r.FormValue("add_x_csp_disabled") == "on"
// v2.9.265: add_x_request_method_override — honor X-HTTP-Method-Override.
ph.AddXRequestMethodOverride = r.FormValue("add_x_request_method_override") == "on"
// v2.12.52: disable upstream compression toggle.
ph.DisableUpstreamCompression = r.FormValue("disable_upstream_compression") == "on"
// v2.28.0 (issue #39): CaddyUI's own monitoring controls. The override
// fields are parsed regardless of mode so switching custom → auto → custom
// doesn't discard what the user typed; the mode alone decides whether they
// take effect (see models.ProxyHost.MonitorSettings).
ph.MonitorMode = models.NormalizeMonitorMode(r.FormValue("monitor_mode"))
ph.MonitorPath = strings.TrimSpace(r.FormValue("monitor_path"))
// v2.36.0 (issue #59): any method in models.MonitorMethods; blank or
// unrecognised input is stored as GET so the row is canonical on disk.
ph.MonitorMethod = models.NormalizeMonitorMethod(r.FormValue("monitor_method"))
ph.MonitorExpectStatus = clampAtoi(r.FormValue("monitor_expect_status"), 0, 100, 599)
// Floor the interval at the poller's own tick: a smaller value can't be
// honoured (the poller only wakes every appHealthInterval) and storing it
// would promise a cadence the UI can't deliver.
ph.MonitorIntervalSec = clampAtoi(r.FormValue("monitor_interval_sec"), 0, 60, 86400)
ph.MonitorTimeoutSec = clampAtoi(r.FormValue("monitor_timeout_sec"), 0, 1, 120)
// v2.33.0: node-local — never sync this host to another Caddy.
ph.NodeLocal = r.FormValue("node_local") == "on"
// v2.46.0 (discussion #91): issue this host's certificate from Caddy's
// internal CA instead of ACME. Only meaningful with Auto TLS (no custom
// certificate selected); buildInternalTLSAutomationPolicies enforces that.
ph.InternalTLS = r.FormValue("internal_tls") == "on"
// v2.51.0 (issue #102): per-host rate limiting (caddy-ratelimit).
ph.RateLimitEnabled = r.FormValue("rate_limit_enabled") == "on"
ph.RateLimitEvents = clampAtoi(r.FormValue("rate_limit_events"), 0, 1, 1000000)
ph.RateLimitWindowSec = clampAtoi(r.FormValue("rate_limit_window_sec"), 0, 1, 86400)
// If enabled with the fields left blank, apply the form's placeholder
// defaults (100 requests / 60s) so a ticked box is never a silent no-op.
if ph.RateLimitEnabled {
if ph.RateLimitEvents == 0 {
ph.RateLimitEvents = 100
}
if ph.RateLimitWindowSec == 0 {
ph.RateLimitWindowSec = 60
}
}
// v2.9.266: proxy_redirect_rules — JSON array of path-based redirects
// fired before the reverse_proxy. Same shape as redirection_hosts.
ph.ProxyRedirectRules = func() string {
v := strings.TrimSpace(r.FormValue("proxy_redirect_rules"))
if v == "" || v == "[]" {
return ""
}
var probe []models.RedirectRule
if err := json.Unmarshal([]byte(v), &probe); err != nil {
return ""
}
return v
}()
// v2.9.267: additional_upstream_rules — JSON array of path-based upstream
// overrides. Probe-parse to drop garbage that UpstreamRuleList would
// silently ignore on read.
ph.AdditionalUpstreamRules = func() string {
v := strings.TrimSpace(r.FormValue("additional_upstream_rules"))
if v == "" || v == "[]" {
return ""
}
var probe []models.UpstreamRule
if err := json.Unmarshal([]byte(v), &probe); err != nil {
return ""
}
return v
}()
// v2.38.0: expectations_json — post-apply checks; stored canonicalised.
ph.Expectations = models.NormalizeHostExpectationsJSON(r.FormValue("expectations_json"))
return ph, nil
}