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 set-header Strict-Transport-Security "{{$d.HSTSHeader}}" if { hdr(host) -i {{$d.Name}} }
|
||||
{{- 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}}
|
||||
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}} }
|
||||
@@ -176,6 +183,6 @@ backend eg_backend_{{$b.ID}}
|
||||
http-check send meth GET uri {{$b.HealthCheckPath}}
|
||||
{{- end}}
|
||||
{{- 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}}
|
||||
|
||||
@@ -11,6 +11,7 @@ type Backend struct {
|
||||
HealthCheckPath *string `gorm:"column:health_check_path" json:"health_check_path,omitempty"`
|
||||
LBAlgorithm string `gorm:"column:lb_algorithm" json:"lb_algorithm"`
|
||||
WebSocket bool `gorm:"column:websocket" json:"websocket"`
|
||||
ForceHTTP1 bool `gorm:"column:force_http1" json:"force_http1"`
|
||||
Active bool `gorm:"column:active" json:"active"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_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"
|
||||
RateLimitRPS int `gorm:"column:rate_limit_rps" json:"rate_limit_rps"`
|
||||
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"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_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} }
|
||||
|
||||
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
|
||||
FROM backends
|
||||
`
|
||||
@@ -62,11 +62,11 @@ func (r *Repo) Create(ctx context.Context, b models.Backend) (*models.Backend, e
|
||||
b.LBAlgorithm = "roundrobin"
|
||||
}
|
||||
row := r.Pool.QueryRow(ctx, `
|
||||
INSERT INTO backends (name, scheme, health_check_path, lb_algorithm, websocket, active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, 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, $7)
|
||||
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active,
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -81,12 +81,13 @@ UPDATE backends SET
|
||||
health_check_path = $3,
|
||||
lb_algorithm = $4,
|
||||
websocket = $5,
|
||||
active = $6,
|
||||
force_http1 = $6,
|
||||
active = $7,
|
||||
updated_at = NOW()
|
||||
WHERE id = $7
|
||||
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, active,
|
||||
WHERE id = $8
|
||||
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active,
|
||||
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)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
@@ -112,7 +113,7 @@ func scanBackend(row interface{ Scan(...any) error }) (*models.Backend, error) {
|
||||
var b models.Backend
|
||||
if err := row.Scan(
|
||||
&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,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -23,7 +23,7 @@ const baseSelect = `
|
||||
SELECT id, name, active, primary_backend_id, http_to_https,
|
||||
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
||||
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
|
||||
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,
|
||||
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
||||
maintenance_mode, maintenance_message, www_redirect,
|
||||
rate_limit_rps, max_body_kb, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
rate_limit_rps, max_body_kb, disable_h3, notes)
|
||||
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,
|
||||
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
||||
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`,
|
||||
d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS,
|
||||
d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload,
|
||||
d.MaintenanceMode, d.MaintenanceMessage, d.WWWRedirect,
|
||||
d.RateLimitRPS, d.MaxBodyKB, d.Notes)
|
||||
d.RateLimitRPS, d.MaxBodyKB, d.DisableH3, d.Notes)
|
||||
return scanDomain(row)
|
||||
}
|
||||
|
||||
@@ -98,18 +98,19 @@ UPDATE domains SET
|
||||
www_redirect = $11,
|
||||
rate_limit_rps = $12,
|
||||
max_body_kb = $13,
|
||||
notes = $14,
|
||||
disable_h3 = $14,
|
||||
notes = $15,
|
||||
updated_at = NOW()
|
||||
WHERE id = $15
|
||||
WHERE id = $16
|
||||
RETURNING id, name, active, primary_backend_id, http_to_https,
|
||||
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
||||
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`,
|
||||
d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS,
|
||||
d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload,
|
||||
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)
|
||||
if err != nil {
|
||||
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.HSTSEnabled, &d.HSTSMaxAge, &d.HSTSSubdomains, &d.HSTSPreload,
|
||||
&d.MaintenanceMode, &d.MaintenanceMessage, &d.WWWRedirect,
|
||||
&d.RateLimitRPS, &d.MaxBodyKB,
|
||||
&d.RateLimitRPS, &d.MaxBodyKB, &d.DisableH3,
|
||||
&d.Notes, &d.CreatedAt, &d.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user