feat(backends+domains): HTTP-Protokoll-Flags — force_http1 + disable_h3
Backend: force_http1 (bool, default false) — zwingt HAProxy auf der Backend-Verbindung zu HTTP/1.1 statt h2,http/1.1 zu verhandeln. Nötig für Legacy-Apps die kein h2 sprechen. Domain: disable_h3 (bool, default false) — unterdrückt Alt-Svc- Response-Header für diese Domain. Browser erhalten keinen h3/QUIC- Hinweis und bleiben auf h2/http1.1. Migration 0027, Model+Service+HAProxy-Template+UI+i18n. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
22
internal/database/migrations/0027_http_protocol_flags.sql
Normal file
22
internal/database/migrations/0027_http_protocol_flags.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
|
||||||
|
-- backends.force_http1: zwingt HAProxy, die Backend-Verbindung mit
|
||||||
|
-- HTTP/1.1 zu führen (alpn http/1.1) statt H2+H1.1 zu verhandeln.
|
||||||
|
-- Nötig für Backends die kein h2 sprechen (Legacy-Apps, manche nginx-Configs).
|
||||||
|
ALTER TABLE backends
|
||||||
|
ADD COLUMN IF NOT EXISTS force_http1 BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- domains.disable_h3: unterdrückt den Alt-Svc-Response-Header für diese
|
||||||
|
-- Domain. Browser erhalten keinen Hinweis auf h3/QUIC und bleiben auf
|
||||||
|
-- h2/http1.1 — sinnvoll wenn Clients Probleme mit QUIC-Verbindungen melden.
|
||||||
|
ALTER TABLE domains
|
||||||
|
ADD COLUMN IF NOT EXISTS disable_h3 BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE backends DROP COLUMN IF EXISTS force_http1;
|
||||||
|
ALTER TABLE domains DROP COLUMN IF EXISTS disable_h3;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -108,6 +108,13 @@ frontend public_https
|
|||||||
http-response del-header Strict-Transport-Security if { hdr(host) -i {{$d.Name}} }
|
http-response del-header Strict-Transport-Security if { hdr(host) -i {{$d.Name}} }
|
||||||
http-response set-header Strict-Transport-Security "{{$d.HSTSHeader}}" if { hdr(host) -i {{$d.Name}} }
|
http-response set-header Strict-Transport-Security "{{$d.HSTSHeader}}" if { hdr(host) -i {{$d.Name}} }
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
{{- if $d.DisableH3}}
|
||||||
|
# HTTP/3 (QUIC) Werbung für diese Domain unterdrücken: Alt-Svc-Header
|
||||||
|
# entfernen damit Browser nicht auf h3 upgraden. Der globale set-header
|
||||||
|
# oben hat ihn bereits gesetzt; dieses del-header kommt DANACH und
|
||||||
|
# überschreibt ihn für Requests an diese Domain.
|
||||||
|
http-response del-header Alt-Svc if { hdr(host) -i {{$d.Name}} }
|
||||||
|
{{- end}}
|
||||||
{{- range $h := $d.ResponseHeaders}}
|
{{- range $h := $d.ResponseHeaders}}
|
||||||
http-response del-header {{$h.Name}} if { hdr(host) -i {{$d.Name}} }
|
http-response del-header {{$h.Name}} if { hdr(host) -i {{$d.Name}} }
|
||||||
http-response set-header {{$h.Name}} "{{$h.Value}}" if { hdr(host) -i {{$d.Name}} }
|
http-response set-header {{$h.Name}} "{{$h.Value}}" if { hdr(host) -i {{$d.Name}} }
|
||||||
@@ -176,6 +183,6 @@ backend eg_backend_{{$b.ID}}
|
|||||||
http-check send meth GET uri {{$b.HealthCheckPath}}
|
http-check send meth GET uri {{$b.HealthCheckPath}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- range $s := $b.Servers}}
|
{{- range $s := $b.Servers}}
|
||||||
server {{$s.Name | safeID}} {{$s.Address}}:{{$s.Port}}{{if eq $b.Scheme "https"}} ssl verify none alpn h2,http/1.1{{end}}{{if $b.HealthCheckPath}} check inter 5s{{if eq $b.Scheme "https"}} check-alpn http/1.1{{end}}{{end}} weight {{$s.Weight}}{{if $s.Backup}} backup{{end}}
|
server {{$s.Name | safeID}} {{$s.Address}}:{{$s.Port}}{{if eq $b.Scheme "https"}} ssl verify none alpn {{if $b.ForceHTTP1}}http/1.1{{else}}h2,http/1.1{{end}}{{end}}{{if $b.HealthCheckPath}} check inter 5s{{if eq $b.Scheme "https"}} check-alpn http/1.1{{end}}{{end}} weight {{$s.Weight}}{{if $s.Backup}} backup{{end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type Backend struct {
|
|||||||
HealthCheckPath *string `gorm:"column:health_check_path" json:"health_check_path,omitempty"`
|
HealthCheckPath *string `gorm:"column:health_check_path" json:"health_check_path,omitempty"`
|
||||||
LBAlgorithm string `gorm:"column:lb_algorithm" json:"lb_algorithm"`
|
LBAlgorithm string `gorm:"column:lb_algorithm" json:"lb_algorithm"`
|
||||||
WebSocket bool `gorm:"column:websocket" json:"websocket"`
|
WebSocket bool `gorm:"column:websocket" json:"websocket"`
|
||||||
|
ForceHTTP1 bool `gorm:"column:force_http1" json:"force_http1"`
|
||||||
Active bool `gorm:"column:active" json:"active"`
|
Active bool `gorm:"column:active" json:"active"`
|
||||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type Domain struct {
|
|||||||
WWWRedirect string `gorm:"column:www_redirect" json:"www_redirect"` // ""|"to-naked"|"to-www"
|
WWWRedirect string `gorm:"column:www_redirect" json:"www_redirect"` // ""|"to-naked"|"to-www"
|
||||||
RateLimitRPS int `gorm:"column:rate_limit_rps" json:"rate_limit_rps"`
|
RateLimitRPS int `gorm:"column:rate_limit_rps" json:"rate_limit_rps"`
|
||||||
MaxBodyKB int `gorm:"column:max_body_kb" json:"max_body_kb"`
|
MaxBodyKB int `gorm:"column:max_body_kb" json:"max_body_kb"`
|
||||||
|
DisableH3 bool `gorm:"column:disable_h3" json:"disable_h3"`
|
||||||
Notes *string `gorm:"column:notes" json:"notes,omitempty"`
|
Notes *string `gorm:"column:notes" json:"notes,omitempty"`
|
||||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type Repo struct {
|
|||||||
func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
|
func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
|
||||||
|
|
||||||
const baseSelect = `
|
const baseSelect = `
|
||||||
SELECT id, name, scheme, health_check_path, lb_algorithm, websocket, active,
|
SELECT id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active,
|
||||||
created_at, updated_at
|
created_at, updated_at
|
||||||
FROM backends
|
FROM backends
|
||||||
`
|
`
|
||||||
@@ -62,11 +62,11 @@ func (r *Repo) Create(ctx context.Context, b models.Backend) (*models.Backend, e
|
|||||||
b.LBAlgorithm = "roundrobin"
|
b.LBAlgorithm = "roundrobin"
|
||||||
}
|
}
|
||||||
row := r.Pool.QueryRow(ctx, `
|
row := r.Pool.QueryRow(ctx, `
|
||||||
INSERT INTO backends (name, scheme, health_check_path, lb_algorithm, websocket, active)
|
INSERT INTO backends (name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, active,
|
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active,
|
||||||
created_at, updated_at`,
|
created_at, updated_at`,
|
||||||
b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.Active)
|
b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.ForceHTTP1, b.Active)
|
||||||
return scanBackend(row)
|
return scanBackend(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,12 +81,13 @@ UPDATE backends SET
|
|||||||
health_check_path = $3,
|
health_check_path = $3,
|
||||||
lb_algorithm = $4,
|
lb_algorithm = $4,
|
||||||
websocket = $5,
|
websocket = $5,
|
||||||
active = $6,
|
force_http1 = $6,
|
||||||
|
active = $7,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $7
|
WHERE id = $8
|
||||||
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, active,
|
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active,
|
||||||
created_at, updated_at`,
|
created_at, updated_at`,
|
||||||
b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.Active, id)
|
b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.ForceHTTP1, b.Active, id)
|
||||||
out, err := scanBackend(row)
|
out, err := scanBackend(row)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
@@ -112,7 +113,7 @@ func scanBackend(row interface{ Scan(...any) error }) (*models.Backend, error) {
|
|||||||
var b models.Backend
|
var b models.Backend
|
||||||
if err := row.Scan(
|
if err := row.Scan(
|
||||||
&b.ID, &b.Name, &b.Scheme,
|
&b.ID, &b.Name, &b.Scheme,
|
||||||
&b.HealthCheckPath, &b.LBAlgorithm, &b.WebSocket, &b.Active,
|
&b.HealthCheckPath, &b.LBAlgorithm, &b.WebSocket, &b.ForceHTTP1, &b.Active,
|
||||||
&b.CreatedAt, &b.UpdatedAt,
|
&b.CreatedAt, &b.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const baseSelect = `
|
|||||||
SELECT id, name, active, primary_backend_id, http_to_https,
|
SELECT id, name, active, primary_backend_id, http_to_https,
|
||||||
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
||||||
maintenance_mode, maintenance_message, www_redirect,
|
maintenance_mode, maintenance_message, www_redirect,
|
||||||
rate_limit_rps, max_body_kb,
|
rate_limit_rps, max_body_kb, disable_h3,
|
||||||
notes, created_at, updated_at
|
notes, created_at, updated_at
|
||||||
FROM domains
|
FROM domains
|
||||||
`
|
`
|
||||||
@@ -65,17 +65,17 @@ func (r *Repo) Create(ctx context.Context, d models.Domain) (*models.Domain, err
|
|||||||
INSERT INTO domains (name, active, primary_backend_id, http_to_https,
|
INSERT INTO domains (name, active, primary_backend_id, http_to_https,
|
||||||
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
||||||
maintenance_mode, maintenance_message, www_redirect,
|
maintenance_mode, maintenance_message, www_redirect,
|
||||||
rate_limit_rps, max_body_kb, notes)
|
rate_limit_rps, max_body_kb, disable_h3, notes)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
RETURNING id, name, active, primary_backend_id, http_to_https,
|
RETURNING id, name, active, primary_backend_id, http_to_https,
|
||||||
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
||||||
maintenance_mode, maintenance_message, www_redirect,
|
maintenance_mode, maintenance_message, www_redirect,
|
||||||
rate_limit_rps, max_body_kb,
|
rate_limit_rps, max_body_kb, disable_h3,
|
||||||
notes, created_at, updated_at`,
|
notes, created_at, updated_at`,
|
||||||
d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS,
|
d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS,
|
||||||
d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload,
|
d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload,
|
||||||
d.MaintenanceMode, d.MaintenanceMessage, d.WWWRedirect,
|
d.MaintenanceMode, d.MaintenanceMessage, d.WWWRedirect,
|
||||||
d.RateLimitRPS, d.MaxBodyKB, d.Notes)
|
d.RateLimitRPS, d.MaxBodyKB, d.DisableH3, d.Notes)
|
||||||
return scanDomain(row)
|
return scanDomain(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,18 +98,19 @@ UPDATE domains SET
|
|||||||
www_redirect = $11,
|
www_redirect = $11,
|
||||||
rate_limit_rps = $12,
|
rate_limit_rps = $12,
|
||||||
max_body_kb = $13,
|
max_body_kb = $13,
|
||||||
notes = $14,
|
disable_h3 = $14,
|
||||||
|
notes = $15,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $15
|
WHERE id = $16
|
||||||
RETURNING id, name, active, primary_backend_id, http_to_https,
|
RETURNING id, name, active, primary_backend_id, http_to_https,
|
||||||
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
||||||
maintenance_mode, maintenance_message, www_redirect,
|
maintenance_mode, maintenance_message, www_redirect,
|
||||||
rate_limit_rps, max_body_kb,
|
rate_limit_rps, max_body_kb, disable_h3,
|
||||||
notes, created_at, updated_at`,
|
notes, created_at, updated_at`,
|
||||||
d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS,
|
d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS,
|
||||||
d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload,
|
d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload,
|
||||||
d.MaintenanceMode, d.MaintenanceMessage, d.WWWRedirect,
|
d.MaintenanceMode, d.MaintenanceMessage, d.WWWRedirect,
|
||||||
d.RateLimitRPS, d.MaxBodyKB, d.Notes, id)
|
d.RateLimitRPS, d.MaxBodyKB, d.DisableH3, d.Notes, id)
|
||||||
out, err := scanDomain(row)
|
out, err := scanDomain(row)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
@@ -137,7 +138,7 @@ func scanDomain(row interface{ Scan(...any) error }) (*models.Domain, error) {
|
|||||||
&d.ID, &d.Name, &d.Active, &d.PrimaryBackendID, &d.HTTPToHTTPS,
|
&d.ID, &d.Name, &d.Active, &d.PrimaryBackendID, &d.HTTPToHTTPS,
|
||||||
&d.HSTSEnabled, &d.HSTSMaxAge, &d.HSTSSubdomains, &d.HSTSPreload,
|
&d.HSTSEnabled, &d.HSTSMaxAge, &d.HSTSSubdomains, &d.HSTSPreload,
|
||||||
&d.MaintenanceMode, &d.MaintenanceMessage, &d.WWWRedirect,
|
&d.MaintenanceMode, &d.MaintenanceMessage, &d.WWWRedirect,
|
||||||
&d.RateLimitRPS, &d.MaxBodyKB,
|
&d.RateLimitRPS, &d.MaxBodyKB, &d.DisableH3,
|
||||||
&d.Notes, &d.CreatedAt, &d.UpdatedAt,
|
&d.Notes, &d.CreatedAt, &d.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -290,6 +290,8 @@
|
|||||||
"rateLimitHint": "Max. Requests pro Sekunde je Client-IP. HAProxy zählt über ein 10-Sekunden-Fenster pro Stick-Table (max. 100k IPs). 0 = aus.",
|
"rateLimitHint": "Max. Requests pro Sekunde je Client-IP. HAProxy zählt über ein 10-Sekunden-Fenster pro Stick-Table (max. 100k IPs). 0 = aus.",
|
||||||
"maxBody": "Max. Request-Body",
|
"maxBody": "Max. Request-Body",
|
||||||
"maxBodyHint": "Cap auf Content-Length-Header. Größere Requests bekommen 413. Nicht erkannt: Chunked-Bodies (HAProxy bufferd nicht standardmäßig). 0 = aus.",
|
"maxBodyHint": "Cap auf Content-Length-Header. Größere Requests bekommen 413. Nicht erkannt: Chunked-Bodies (HAProxy bufferd nicht standardmäßig). 0 = aus.",
|
||||||
|
"disableH3": "HTTP/3 (QUIC) deaktivieren",
|
||||||
|
"disableH3Hint": "Unterdrückt den Alt-Svc-Response-Header für diese Domain. Browser erhalten keinen Hinweis auf h3/QUIC und bleiben auf h2/http1.1. Sinnvoll bei Clients die Probleme mit QUIC-Verbindungen melden.",
|
||||||
"headersBtn": "Headers",
|
"headersBtn": "Headers",
|
||||||
"headersTitle": "Response-Headers — {{name}}",
|
"headersTitle": "Response-Headers — {{name}}",
|
||||||
"headersHint": "Diese Header werden von HAProxy auf jede Response für diese Domain gesetzt (http-response set-header). Reihenfolge via Position.",
|
"headersHint": "Diese Header werden von HAProxy auf jede Response für diese Domain gesetzt (http-response set-header). Reihenfolge via Position.",
|
||||||
@@ -331,6 +333,8 @@
|
|||||||
"lbAlgoHint": "roundrobin = gleichmäßig, leastconn = an den Server mit wenigsten Verbindungen, source = sticky per Client-IP (für stateful Apps ohne shared session).",
|
"lbAlgoHint": "roundrobin = gleichmäßig, leastconn = an den Server mit wenigsten Verbindungen, source = sticky per Client-IP (für stateful Apps ohne shared session).",
|
||||||
"websocket": "WebSocket-Support",
|
"websocket": "WebSocket-Support",
|
||||||
"websocketHint": "An: erlaubt langlebige WebSocket-/Long-Poll-Verbindungen (z. B. Proxmox-Console, SSH-WS, AsyncAPI) — Tunnel-Idle 1h statt 60s. Aus: strikte HTTP-Timeouts.",
|
"websocketHint": "An: erlaubt langlebige WebSocket-/Long-Poll-Verbindungen (z. B. Proxmox-Console, SSH-WS, AsyncAPI) — Tunnel-Idle 1h statt 60s. Aus: strikte HTTP-Timeouts.",
|
||||||
|
"forceHttp1": "HTTP/1.1 erzwingen",
|
||||||
|
"forceHttp1Hint": "Deaktiviert HTTP/2 (h2) auf der Backend-Verbindung — HAProxy handelt nur noch HTTP/1.1 aus. Nötig für Backends die kein h2 unterstützen (z. B. ältere nginx-Konfigurationen ohne h2-Modul, Legacy-Apps).",
|
||||||
"servers": "Server",
|
"servers": "Server",
|
||||||
"noServers": "kein Server",
|
"noServers": "kein Server",
|
||||||
"nServers": "{{n}} Server",
|
"nServers": "{{n}} Server",
|
||||||
|
|||||||
@@ -290,6 +290,8 @@
|
|||||||
"rateLimitHint": "Max requests per second per client IP. HAProxy counts over a 10-second window per stick table (max. 100k IPs). 0 = off.",
|
"rateLimitHint": "Max requests per second per client IP. HAProxy counts over a 10-second window per stick table (max. 100k IPs). 0 = off.",
|
||||||
"maxBody": "Max request body",
|
"maxBody": "Max request body",
|
||||||
"maxBodyHint": "Cap on the Content-Length header. Larger requests get 413. Chunked bodies are not detected (HAProxy doesn't buffer by default). 0 = off.",
|
"maxBodyHint": "Cap on the Content-Length header. Larger requests get 413. Chunked bodies are not detected (HAProxy doesn't buffer by default). 0 = off.",
|
||||||
|
"disableH3": "Disable HTTP/3 (QUIC)",
|
||||||
|
"disableH3Hint": "Suppresses the Alt-Svc response header for this domain. Browsers won't receive an h3/QUIC upgrade hint and stay on h2/http1.1. Useful when clients report issues with QUIC connections.",
|
||||||
"headersBtn": "Headers",
|
"headersBtn": "Headers",
|
||||||
"headersTitle": "Response headers — {{name}}",
|
"headersTitle": "Response headers — {{name}}",
|
||||||
"headersHint": "These headers are set by HAProxy on every response for this domain (http-response set-header). Ordering via position.",
|
"headersHint": "These headers are set by HAProxy on every response for this domain (http-response set-header). Ordering via position.",
|
||||||
@@ -331,6 +333,8 @@
|
|||||||
"lbAlgoHint": "roundrobin = evenly, leastconn = pick the server with fewest active connections, source = sticky per client-IP hash (for stateful apps without shared session).",
|
"lbAlgoHint": "roundrobin = evenly, leastconn = pick the server with fewest active connections, source = sticky per client-IP hash (for stateful apps without shared session).",
|
||||||
"websocket": "WebSocket support",
|
"websocket": "WebSocket support",
|
||||||
"websocketHint": "On: allow long-lived WebSocket / long-poll connections (Proxmox console, SSH-over-WS, AsyncAPI) — tunnel idle 1h instead of 60s. Off: strict HTTP timeouts.",
|
"websocketHint": "On: allow long-lived WebSocket / long-poll connections (Proxmox console, SSH-over-WS, AsyncAPI) — tunnel idle 1h instead of 60s. Off: strict HTTP timeouts.",
|
||||||
|
"forceHttp1": "Force HTTP/1.1",
|
||||||
|
"forceHttp1Hint": "Disables HTTP/2 (h2) on the backend connection — HAProxy negotiates HTTP/1.1 only. Required for backends that don't support h2 (e.g. older nginx configs without the h2 module, legacy apps).",
|
||||||
"servers": "Servers",
|
"servers": "Servers",
|
||||||
"noServers": "no server",
|
"noServers": "no server",
|
||||||
"nServers": "{{n}} servers",
|
"nServers": "{{n}} servers",
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ interface Backend {
|
|||||||
id: number; name: string; scheme: string
|
id: number; name: string; scheme: string
|
||||||
health_check_path?: string | null
|
health_check_path?: string | null
|
||||||
lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
|
lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
|
||||||
websocket: boolean; active: boolean
|
websocket: boolean; force_http1: boolean; active: boolean
|
||||||
}
|
}
|
||||||
interface BackendFormValues {
|
interface BackendFormValues {
|
||||||
name: string; scheme: 'http' | 'https'
|
name: string; scheme: 'http' | 'https'
|
||||||
health_check_path?: string
|
health_check_path?: string
|
||||||
lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
|
lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
|
||||||
websocket: boolean; active: boolean
|
websocket: boolean; force_http1: boolean; active: boolean
|
||||||
domain_ids?: number[]
|
domain_ids?: number[]
|
||||||
}
|
}
|
||||||
interface BackendServer {
|
interface BackendServer {
|
||||||
@@ -157,6 +157,7 @@ export default function BackendDetailPage() {
|
|||||||
health_check_path: backend.health_check_path ?? undefined,
|
health_check_path: backend.health_check_path ?? undefined,
|
||||||
lb_algorithm: backend.lb_algorithm,
|
lb_algorithm: backend.lb_algorithm,
|
||||||
websocket: backend.websocket,
|
websocket: backend.websocket,
|
||||||
|
force_http1: backend.force_http1,
|
||||||
active: backend.active,
|
active: backend.active,
|
||||||
domain_ids: attached.map(d => d.id),
|
domain_ids: attached.map(d => d.id),
|
||||||
}}
|
}}
|
||||||
@@ -184,6 +185,10 @@ export default function BackendDetailPage() {
|
|||||||
extra={t('backends.websocketHint')}>
|
extra={t('backends.websocketHint')}>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t('backends.forceHttp1')} name="force_http1" valuePropName="checked"
|
||||||
|
extra={t('backends.forceHttp1Hint')}>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label={t('backends.active')} name="active" valuePropName="checked">
|
<Form.Item label={t('backends.active')} name="active" valuePropName="checked">
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ interface Domain {
|
|||||||
maintenance_mode: boolean; maintenance_message?: string | null
|
maintenance_mode: boolean; maintenance_message?: string | null
|
||||||
www_redirect: '' | 'to-naked' | 'to-www'
|
www_redirect: '' | 'to-naked' | 'to-www'
|
||||||
rate_limit_rps: number; max_body_kb: number
|
rate_limit_rps: number; max_body_kb: number
|
||||||
|
disable_h3: boolean
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ interface DomainFormValues {
|
|||||||
maintenance_mode: boolean; maintenance_message?: string
|
maintenance_mode: boolean; maintenance_message?: string
|
||||||
www_redirect: '' | 'to-naked' | 'to-www'
|
www_redirect: '' | 'to-naked' | 'to-www'
|
||||||
rate_limit_rps: number; max_body_kb: number
|
rate_limit_rps: number; max_body_kb: number
|
||||||
|
disable_h3: boolean
|
||||||
notes?: string
|
notes?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +166,7 @@ export default function DomainDetailPage() {
|
|||||||
www_redirect: domain.www_redirect ?? '',
|
www_redirect: domain.www_redirect ?? '',
|
||||||
rate_limit_rps: domain.rate_limit_rps ?? 0,
|
rate_limit_rps: domain.rate_limit_rps ?? 0,
|
||||||
max_body_kb: domain.max_body_kb ?? 0,
|
max_body_kb: domain.max_body_kb ?? 0,
|
||||||
|
disable_h3: domain.disable_h3 ?? false,
|
||||||
notes: domain.notes ?? '',
|
notes: domain.notes ?? '',
|
||||||
}}
|
}}
|
||||||
onFinish={(v) => update.mutate(v)}
|
onFinish={(v) => update.mutate(v)}
|
||||||
@@ -244,6 +247,11 @@ export default function DomainDetailPage() {
|
|||||||
<InputNumber min={0} step={64} style={{ width: '100%' }} addonAfter="KiB" />
|
<InputNumber min={0} step={64} style={{ width: '100%' }} addonAfter="KiB" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label={t('domains.disableH3')} name="disable_h3" valuePropName="checked"
|
||||||
|
extra={t('domains.disableH3Hint')}>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t('domains.notes')} name="notes">
|
<Form.Item label={t('domains.notes')} name="notes">
|
||||||
<Input.TextArea rows={2} />
|
<Input.TextArea rows={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
Reference in New Issue
Block a user