Skip to content

Commit 9de3ef9

Browse files
authored
Merge pull request #30 from hypercerts-org/wss-fix
2 parents d8a5df9 + 5f1207e commit 9de3ef9

3 files changed

Lines changed: 133 additions & 3 deletions

File tree

.env.example

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,22 @@ DATABASE_URL=sqlite:data/hypergoat.db
2929
# IMPORTANT: This MUST be persistent across restarts or sessions will be invalidated
3030
SECRET_KEY_BASE=CHANGE_ME_TO_A_RANDOM_64_CHARACTER_STRING_USE_OPENSSL_RAND
3131

32+
# Trust X-User-DID header from reverse proxy for authentication
33+
# DANGEROUS: only enable when running behind a trusted reverse proxy
34+
# TRUST_PROXY_HEADERS=false
35+
36+
# Allowed origins for CORS and WebSocket connections (comma-separated)
37+
# Empty or unset = allow all origins. Set explicit origins in production.
38+
# Examples: https://myapp.com,https://admin.myapp.com
39+
# ALLOWED_ORIGINS=
40+
3241
# Admin DIDs (comma-separated) - users with admin access to the dashboard
3342
# Example: did:plc:qc42fmqqlsmdq7jiypiiigww is daviddao.org
3443
ADMIN_DIDS=did:plc:qc42fmqqlsmdq7jiypiiigww
3544

45+
# Domain DID for server identity (defaults to did:web:{HOST})
46+
# DOMAIN_DID=
47+
3648
# =============================================================================
3749
# OAuth Configuration
3850
# =============================================================================
@@ -52,10 +64,24 @@ ADMIN_DIDS=did:plc:qc42fmqqlsmdq7jiypiiigww
5264
# Set to "true" for local development, leave unset for production
5365
# OAUTH_LOOPBACK_MODE=true
5466

67+
# =============================================================================
68+
# Lexicon Configuration
69+
# =============================================================================
70+
71+
# Directory to load lexicon JSON files from (default: testdata/lexicons)
72+
# LEXICON_DIR=
73+
5574
# =============================================================================
5675
# Jetstream Configuration
5776
# =============================================================================
5877

78+
# Jetstream WebSocket URL (default: wss://jetstream2.us-west.bsky.network/subscribe)
79+
# JETSTREAM_URL=
80+
81+
# Collections to subscribe to via Jetstream (comma-separated NSIDs)
82+
# If not set, uses collections from registered lexicons
83+
# JETSTREAM_COLLECTIONS=
84+
5985
# Disable Jetstream cursor tracking (useful in development to avoid
6086
# backfilling events from previous sessions)
6187
# Set to "true", "1", or "yes" to disable
@@ -65,12 +91,21 @@ ADMIN_DIDS=did:plc:qc42fmqqlsmdq7jiypiiigww
6591
# Backfill Configuration
6692
# =============================================================================
6793

94+
# Run backfill on server start
95+
# BACKFILL_ON_START=false
96+
97+
# Collections to backfill (comma-separated, defaults to JETSTREAM_COLLECTIONS)
98+
# BACKFILL_COLLECTIONS=
99+
68100
# Relay URL for discovering repos (com.atproto.sync.listReposByCollection)
69101
# BACKFILL_RELAY_URL=https://relay1.us-west.bsky.network
70102

71103
# PLC directory URL for resolving DIDs
72104
# BACKFILL_PLC_URL=https://plc.directory
73105

106+
# Concurrent requests per PDS during backfill
107+
# BACKFILL_PDS_CONCURRENCY=4
108+
74109
# Global maximum concurrent HTTP requests (prevents overwhelming network)
75110
# Higher = faster but more resource intensive
76111
# BACKFILL_MAX_HTTP=50
@@ -86,6 +121,16 @@ ADMIN_DIDS=did:plc:qc42fmqqlsmdq7jiypiiigww
86121
# Maximum concurrent DID resolutions during discovery phase
87122
# BACKFILL_MAX_REPOS=50
88123

124+
# Timeout per repo in milliseconds
125+
# BACKFILL_REPO_TIMEOUT=60000
126+
127+
# =============================================================================
128+
# PLC Directory
129+
# =============================================================================
130+
131+
# PLC directory URL for DID resolution (default: https://plc.directory)
132+
# PLC_DIRECTORY_URL=
133+
89134
# =============================================================================
90135
# External Services (Defaults configured via Admin UI)
91136
# =============================================================================

internal/graphql/subscription/handler.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,15 @@ func NewHandler(schema *graphql.Schema, pubsub *PubSub, allowedOrigins []string)
6666

6767
// makeOriginChecker returns a CheckOrigin function based on the allowed origins list.
6868
func makeOriginChecker(allowedOrigins []string) func(r *http.Request) bool {
69-
// If explicitly set to "*", allow all origins (development mode)
70-
if len(allowedOrigins) == 1 && allowedOrigins[0] == "*" {
71-
slog.Warn("WebSocket CheckOrigin allows all origins (development mode)")
69+
// No origins configured or explicitly set to "*": allow all origins.
70+
// This matches the CORS middleware default behavior. To restrict origins,
71+
// set ALLOWED_ORIGINS to a comma-separated list of specific origins.
72+
if len(allowedOrigins) == 0 || (len(allowedOrigins) == 1 && allowedOrigins[0] == "*") {
73+
if len(allowedOrigins) == 0 {
74+
slog.Warn("WebSocket CheckOrigin allows all origins (ALLOWED_ORIGINS not configured)")
75+
} else {
76+
slog.Warn("WebSocket CheckOrigin allows all origins (ALLOWED_ORIGINS=\"*\")")
77+
}
7278
return func(r *http.Request) bool {
7379
return true
7480
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package subscription
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
)
7+
8+
func TestMakeOriginChecker(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
allowedOrigins []string
12+
requestOrigin string
13+
want bool
14+
}{
15+
{
16+
name: "nil origins allows all",
17+
allowedOrigins: nil,
18+
requestOrigin: "https://example.com",
19+
want: true,
20+
},
21+
{
22+
name: "empty origins allows all",
23+
allowedOrigins: []string{},
24+
requestOrigin: "https://example.com",
25+
want: true,
26+
},
27+
{
28+
name: "wildcard allows all",
29+
allowedOrigins: []string{"*"},
30+
requestOrigin: "https://example.com",
31+
want: true,
32+
},
33+
{
34+
name: "no origin header always allowed",
35+
allowedOrigins: []string{"https://allowed.com"},
36+
requestOrigin: "",
37+
want: true,
38+
},
39+
{
40+
name: "matching origin allowed",
41+
allowedOrigins: []string{"https://allowed.com"},
42+
requestOrigin: "https://allowed.com",
43+
want: true,
44+
},
45+
{
46+
name: "non-matching origin rejected",
47+
allowedOrigins: []string{"https://allowed.com"},
48+
requestOrigin: "https://evil.com",
49+
want: false,
50+
},
51+
{
52+
name: "multiple origins one matches",
53+
allowedOrigins: []string{"https://a.com", "https://b.com"},
54+
requestOrigin: "https://b.com",
55+
want: true,
56+
},
57+
{
58+
name: "multiple origins none match",
59+
allowedOrigins: []string{"https://a.com", "https://b.com"},
60+
requestOrigin: "https://c.com",
61+
want: false,
62+
},
63+
}
64+
65+
for _, tt := range tests {
66+
t.Run(tt.name, func(t *testing.T) {
67+
checker := makeOriginChecker(tt.allowedOrigins)
68+
req, _ := http.NewRequest("GET", "/graphql/ws", nil)
69+
if tt.requestOrigin != "" {
70+
req.Header.Set("Origin", tt.requestOrigin)
71+
}
72+
got := checker(req)
73+
if got != tt.want {
74+
t.Errorf("makeOriginChecker(%v) with origin %q = %v, want %v",
75+
tt.allowedOrigins, tt.requestOrigin, got, tt.want)
76+
}
77+
})
78+
}
79+
}

0 commit comments

Comments
 (0)