mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-06-16 22:54:12 +00:00
fix(caddy): disable HTTP/2 to keep WebSocket upgrade auth working (#249)
HTTP/2 forbids the `Connection: Upgrade` and `Upgrade: websocket`
headers per RFC 7540 §8.1.2.2. With h2 advertised at the listener,
ALPN negotiates h2 for TLS-capable clients, the WS-upgrade request
arrives at Caddy with those headers stripped, and Caddy forwards a
plain HTTP/1.1 GET to the gateway. The gateway's `isWebSocketUpgrade(r)`
then returns false, the `?api_key=` / `?jwt=` query-string WS-auth
fallback never runs, and clients see 401.
RFC 8441 ("Bootstrapping WebSockets with HTTP/2") fixes this, but iOS
RN and most other mobile WS libraries don't implement it. Until they
do, h1 is the only protocol that keeps WS auth working.
Trade-off: lose h2 multiplexing on plain HTTP traffic. Acceptable for
an API gateway whose dominant workload is REST + WebSocket — neither
benefits much from h2 streams.
caddy_test.go adds a regression guard so anyone re-enabling h2 in the
listener protocols fails CI loud.
Also (separate, was uncommitted): pkg/cli/build/builder.go now reads
VERSION from the repo-root /VERSION file first, falling back to
parsing the Makefile only if absent. The previous Makefile-only path
broke after VERSION moved to /VERSION (Makefile got `$(shell cat ...)`
which the CLI builder pulled in literally).
VERSION bumped to 0.122.13.
This commit is contained in:
parent
fda47533c3
commit
32a2a62e0d
@ -648,7 +648,23 @@ func (b *Builder) crossEnv() []string {
|
||||
}
|
||||
|
||||
func (b *Builder) readVersion() string {
|
||||
// Try to read from Makefile
|
||||
// Primary: read the repo-root VERSION file (single source of truth).
|
||||
// The Makefile resolves $(shell cat ../VERSION) at make time, but this
|
||||
// CLI builder is a separate Go binary that doesn't go through make, so
|
||||
// we must read VERSION directly. Try ../VERSION first (when projectDir
|
||||
// is core/), then VERSION in projectDir.
|
||||
for _, p := range []string{
|
||||
filepath.Join(b.projectDir, "..", "VERSION"),
|
||||
filepath.Join(b.projectDir, "VERSION"),
|
||||
} {
|
||||
if data, err := os.ReadFile(p); err == nil {
|
||||
if v := strings.TrimSpace(string(data)); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: parse Makefile in case someone runs an older layout where
|
||||
// VERSION is still hard-coded inline.
|
||||
data, err := os.ReadFile(filepath.Join(b.projectDir, "Makefile"))
|
||||
if err != nil {
|
||||
return "dev"
|
||||
@ -658,7 +674,11 @@ func (b *Builder) readVersion() string {
|
||||
if strings.HasPrefix(line, "VERSION") {
|
||||
parts := strings.SplitN(line, ":=", 2)
|
||||
if len(parts) == 2 {
|
||||
return strings.TrimSpace(parts[1])
|
||||
v := strings.TrimSpace(parts[1])
|
||||
// Ignore unevaluated make expressions like $(shell ...)
|
||||
if !strings.Contains(v, "$(") {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,8 +377,28 @@ func (ci *CaddyInstaller) generateCaddyfile(domain, email, acmeEndpoint, baseDom
|
||||
}`, acmeEndpoint)
|
||||
|
||||
var sb strings.Builder
|
||||
// Disable HTTP/3 (QUIC) so Caddy doesn't bind UDP 443, which TURN needs for relay
|
||||
sb.WriteString(fmt.Sprintf("{\n email %s\n servers {\n protocols h1 h2\n }\n}\n", email))
|
||||
// Caddy protocol restrictions:
|
||||
// - HTTP/3 (QUIC) is disabled so Caddy doesn't bind UDP 443, which
|
||||
// TURN needs for relay.
|
||||
// - HTTP/2 is also disabled (bug #249). HTTP/2 forbids the
|
||||
// `Connection: Upgrade` and `Upgrade: websocket` headers per
|
||||
// RFC 7540 §8.1.2.2, so any WebSocket-upgrade request the
|
||||
// client sends over an h2 connection arrives at Caddy with
|
||||
// those headers stripped. Caddy then forwards a plain
|
||||
// HTTP/1.1 GET to the backend gateway, which no longer
|
||||
// recognises the request as a WS upgrade — its
|
||||
// `isWebSocketUpgrade(r)` check fails and the
|
||||
// query-string `?api_key=` / `?jwt=` WS-auth fallback is
|
||||
// ignored, producing 401. RFC 8441 ("Bootstrapping WebSockets
|
||||
// with HTTP/2") would fix this, but iOS RN and many other
|
||||
// mobile WS libraries don't implement it. Until they do, h1
|
||||
// is the only protocol that keeps WS auth working.
|
||||
// - Cost: lose h2 multiplexing on regular HTTP traffic.
|
||||
// Acceptable trade-off for an API gateway whose dominant
|
||||
// workload is REST + WebSocket (neither benefits much from
|
||||
// h2 stream multiplexing — REST is keep-alive over h1, and
|
||||
// WS is single-connection by design).
|
||||
sb.WriteString(fmt.Sprintf("{\n email %s\n servers {\n protocols h1\n }\n}\n", email))
|
||||
|
||||
// Node domain blocks (e.g., node1.dbrs.space, *.node1.dbrs.space)
|
||||
sb.WriteString(fmt.Sprintf("\n*.%s {\n%s\n reverse_proxy localhost:6001\n}\n", domain, tlsBlock))
|
||||
|
||||
99
core/pkg/environments/production/installers/caddy_test.go
Normal file
99
core/pkg/environments/production/installers/caddy_test.go
Normal file
@ -0,0 +1,99 @@
|
||||
package installers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// newTestCaddyInstaller returns a CaddyInstaller suitable for unit tests —
|
||||
// no real filesystem or network dependencies.
|
||||
func newTestCaddyInstaller() *CaddyInstaller {
|
||||
return &CaddyInstaller{
|
||||
BaseInstaller: NewBaseInstaller("amd64", io.Discard),
|
||||
oramaHome: "/nonexistent",
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenerateCaddyfile_DisablesHTTP2 is the regression guard for bug
|
||||
// #249: HTTP/2 forbids the `Connection: Upgrade` and `Upgrade: websocket`
|
||||
// headers per RFC 7540 §8.1.2.2, so a WebSocket-upgrade request sent
|
||||
// over an h2 connection arrives at Caddy with the upgrade headers
|
||||
// stripped. Caddy then forwards a plain HTTP/1.1 GET to the gateway,
|
||||
// the gateway's `isWebSocketUpgrade(r)` returns false, the
|
||||
// query-string `?api_key=` / `?jwt=` WS-auth fallback is ignored, and
|
||||
// the client gets 401.
|
||||
//
|
||||
// Disabling h2 at the listener means ALPN negotiates h1 every time, so
|
||||
// WS upgrades work cleanly. h3 is also disabled (so Caddy doesn't bind
|
||||
// UDP 443, which TURN needs).
|
||||
//
|
||||
// If anyone adds `h2` back to the `protocols` line without a deliberate
|
||||
// migration of every mobile-WS client to RFC 8441 ("Bootstrapping
|
||||
// WebSockets with HTTP/2"), this test fails loud.
|
||||
func TestGenerateCaddyfile_DisablesHTTP2(t *testing.T) {
|
||||
ci := newTestCaddyInstaller()
|
||||
cf := ci.generateCaddyfile("node1.dbrs.space", "admin@dbrs.space",
|
||||
"http://localhost:6001/v1/internal/acme", "dbrs.space")
|
||||
|
||||
if !strings.Contains(cf, "protocols h1\n") {
|
||||
t.Errorf("Caddyfile must declare `protocols h1` (bug #249); got:\n%s", cf)
|
||||
}
|
||||
if strings.Contains(cf, "protocols h1 h2") {
|
||||
t.Errorf("Caddyfile must NOT advertise h2 (bug #249 regression); got:\n%s", cf)
|
||||
}
|
||||
if strings.Contains(cf, "h3") {
|
||||
t.Errorf("Caddyfile must NOT advertise h3 (TURN UDP 443 conflict); got:\n%s", cf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateCaddyfile_ContainsCanonicalReverseProxy(t *testing.T) {
|
||||
ci := newTestCaddyInstaller()
|
||||
cf := ci.generateCaddyfile("node1.dbrs.space", "admin@dbrs.space",
|
||||
"http://localhost:6001/v1/internal/acme", "")
|
||||
|
||||
// Sanity checks on the basics; cheap insurance against fat-finger edits.
|
||||
for _, want := range []string{
|
||||
"*.node1.dbrs.space {",
|
||||
"node1.dbrs.space {",
|
||||
"reverse_proxy localhost:6001",
|
||||
"http://*.node1.dbrs.space",
|
||||
":80 {",
|
||||
} {
|
||||
if !strings.Contains(cf, want) {
|
||||
t.Errorf("Caddyfile missing %q; got:\n%s", want, cf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateCaddyfile_BaseDomainAddsSeparateBlocks(t *testing.T) {
|
||||
ci := newTestCaddyInstaller()
|
||||
cf := ci.generateCaddyfile("node1.dbrs.space", "admin@dbrs.space",
|
||||
"http://localhost:6001/v1/internal/acme", "dbrs.space")
|
||||
|
||||
// Both node-domain and base-domain blocks should be present.
|
||||
for _, want := range []string{
|
||||
"*.node1.dbrs.space",
|
||||
"*.dbrs.space",
|
||||
"dbrs.space {",
|
||||
} {
|
||||
if !strings.Contains(cf, want) {
|
||||
t.Errorf("Caddyfile missing %q (base-domain block); got:\n%s", want, cf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateCaddyfile_BaseDomainSameAsDomainOmitsDuplicates(t *testing.T) {
|
||||
ci := newTestCaddyInstaller()
|
||||
cf := ci.generateCaddyfile("dbrs.space", "admin@dbrs.space",
|
||||
"http://localhost:6001/v1/internal/acme", "dbrs.space")
|
||||
|
||||
// When base == node domain, the duplicate base blocks must be skipped:
|
||||
// one TLS `*.dbrs.space { ... }` block + one HTTP `http://*.dbrs.space {
|
||||
// ... }` block. The substring `*.dbrs.space {` matches both so we
|
||||
// expect a count of exactly 2, not 4 (which would mean the dedupe
|
||||
// guard at `if baseDomain != "" && baseDomain != domain` regressed).
|
||||
if got := strings.Count(cf, "*.dbrs.space {"); got != 2 {
|
||||
t.Errorf("expected exactly 2 `*.dbrs.space {` occurrences (1 TLS + 1 HTTP), got %d in:\n%s", got, cf)
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@debros/orama",
|
||||
"version": "0.122.12",
|
||||
"version": "0.122.13",
|
||||
"description": "TypeScript SDK for Orama Network - Database, PubSub, Cache, Storage, Vault, and more",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user