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:
anonpenguin23 2026-05-14 07:50:47 +03:00
parent fda47533c3
commit 32a2a62e0d
5 changed files with 145 additions and 6 deletions

View File

@ -1 +1 @@
0.122.12
0.122.13

View File

@ -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
}
}
}
}

View File

@ -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))

View 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)
}
}

View File

@ -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",