package main import ( "git.smesh.lol/smesh/web/common/helpers" "git.smesh.lol/smesh/web/common/jsbridge/relayproxy" "git.smesh.lol/smesh/web/common/jsbridge/ws" "git.smesh.lol/smesh/web/common/mw" "git.smesh.lol/smesh/web/common/nostr" "git.smesh.lol/smesh/web/common/relay" ) // Relay-proxy worker. // // Owns: WS pool to remote nostr relays, subscription router, MLS event // subscription routing (kinds 443/445/1059 -> MLS worker via supervisor). // // IDB is owned by the Store Worker. This worker sends S_* messages to the // supervisor which routes them to the Store Worker, and receives SR_* responses. // // Wire format: JSON-array MW strings. // page -> worker: // ["REQ", subID, filter] // ["CLOSE", subID] // ["PROXY", subID, filter, [relayURLs]] // ["EVENT", signedEventJSON] -- publish to writeRelays // ["PUBLISH_TO", signedEventJSON, [relayURLs]] -- explicit targets // ["SET_PUBKEY", hex] // ["SET_WRITE_RELAYS", [relayURLs]] // ["ENC_KEY", hex] // ["MLS_SUB", [relayURLs], [groupIDs]] -- open persistent MLS subs // ["MLS_UPDATE_GROUPS", [groupIDs]] -- update kind 445 #h filter // ["MLS_FETCH_KP", peer, [relayURLs]] -- one-shot key package fetch // ["SR_QUERY", reqID, eventsJSON] -- response from Store Worker // worker -> page: // ["READY"] // ["EVENT", subID, event] // ["EOSE", subID] // ["OK", eventID, ok, message] // ["MLS_EVENT", evJSON] -- kind 443/445/1059 -> MLS worker // ["MLS_KP_RESULT", peer, evJSON] -- one-shot KP fetch result -> MLS worker // ["S_PUT_EVENT", evJSON] -- to Store Worker (fire-and-forget) // ["S_ENC_KEY", hexKey] -- to Store Worker (fire-and-forget) // ["S_QUERY", reqID, filterJSON] -- to Store Worker // proxyEOSETimeoutMs: how long to wait for relays to EOSE before emitting // EOSE to the consumer. Short value gets the consumer unblocked quickly so it // can render cached data; the linger window catches stragglers. var proxyEOSETimeoutMs int32 = 3000 // proxyLingerMs: after EOSE is emitted to the consumer, keep the remote subs // open this long to forward late-arriving events. Solves the slow-relay race // where events arrived just after the EOSE timeout fired and were dropped. var proxyLingerMs int32 = 25000 type peerRelayInfo struct { urls []string // relays the peer reads from (or both read+write) ts int64 // created_at of the kind 10002 event } type clientSub struct { filter *nostr.Filter filterRaw string } type proxySub struct { remoteIDs map[string]bool relayCount int32 timer int32 done bool live bool } var ( clientSubs map[string]*clientSub proxySubs map[string]*proxySub remoteToProxy map[string]string rpool *relay.Pool writeRelays []string myPubkey string mlsSubIDs map[string]bool // MLS event subscriptions (kinds 443/445/1059) mlsKPFetchID string // in-flight one-shot KP fetch prefix mlsKPFetchPeer string // peer pubkey for in-flight KP fetch mlsKPFetchSubs []string // per-relay sub IDs for in-flight fetch mlsGroupIDs []string // current #h filter for kind 445 storeCBs map[int32]func(string) // reqID -> callback for SR_QUERY responses nextStoreID int32 // Peer relay list cache (kind 10002). peerRelayCache map[string]*peerRelayInfo // pubkey -> cached relay list rlFetchID string // in-flight relay list fetch sub prefix rlFetchPeer string // peer for in-flight relay list fetch rlFetchSubs []string // per-relay sub IDs rlFetchCB func([]string) // callback when relay list is resolved rlFetchEOSE int32 // count of EOSE received ) func main() { clientSubs = map[string]*clientSub{} proxySubs = map[string]*proxySub{} remoteToProxy = map[string]string{} rpool = relay.NewPool() mlsSubIDs = map[string]bool{} storeCBs = map[int32]func(string){} peerRelayCache = map[string]*peerRelayInfo{} relayproxy.WorkerOnMessage(handleMessage) relayproxy.WorkerPost(`["READY"]`) } // storeQuery sends S_QUERY to the Store Worker and calls fn with the eventsJSON response. func storeQuery(filterRaw string, fn func(string)) { nextStoreID++ reqID := nextStoreID storeCBs[reqID] = fn relayproxy.WorkerPost(`["S_QUERY",` | helpers.Itoa(int64(reqID)) | `,` | filterRaw | `]`) rid := reqID relayproxy.WorkerSetTimeout(30000, func() { if _, ok := storeCBs[rid]; ok { delete(storeCBs, rid) } }) } func handleMessage(msg string) { w := mw.New(msg) msgType := w.Str() switch msgType { case "REQ": subID := dup(w.Str()) filterRaw := dup(w.Raw()) handleReq(subID, filterRaw) case "CLOSE": subID := dup(w.Str()) handleClose(subID) case "PROXY": subID := dup(w.Str()) filterRaw := dup(w.Raw()) relayURLs := dupStrs(w.Strs()) handleProxy(subID, filterRaw, relayURLs, false) case "PROXY_LIVE": subID := dup(w.Str()) filterRaw := dup(w.Raw()) relayURLs := dupStrs(w.Strs()) handleProxy(subID, filterRaw, relayURLs, true) case "EVENT": eventRaw := w.Raw() handleEventPublish(eventRaw) case "PUBLISH_TO": eventRaw := w.Raw() relayURLs := w.Strs() handlePublishTo(eventRaw, relayURLs) case "SET_WRITE_RELAYS": urls := dupStrs(w.Strs()) handleSetWriteRelays(urls) case "SET_PUBKEY": myPubkey = dup(w.Str()) case "ENC_KEY": hexKey := w.Str() relayproxy.WorkerPost(`["S_ENC_KEY",` | jstr(hexKey) | `]`) case "SET_PROXY_EOSE_MS": ms := int32(w.Num()) if ms > 0 { proxyEOSETimeoutMs = ms } case "MLS_SUB": urls := dupStrs(w.Strs()) groupIDs := dupStrs(w.Strs()) handleMLSSub(urls, groupIDs) case "MLS_UPDATE_GROUPS": groupIDs := dupStrs(w.Strs()) handleMLSUpdateGroups(groupIDs) case "MLS_FETCH_KP": peer := dup(w.Str()) urls := dupStrs(w.Strs()) handleMLSFetchKP(peer, urls) // Store Worker response for storeQuery. case "SR_QUERY": reqID := int32(w.Num()) eventsJSON := w.Raw() if fn, ok := storeCBs[reqID]; ok { fn(eventsJSON) delete(storeCBs, reqID) } } } func handleMLSSub(relayURLs, groupIDs []string) { println("[relay-proxy] handleMLSSub: urls=" | helpers.Itoa(int64(len(relayURLs))) | " groupIDs=" | helpers.Itoa(int64(len(groupIDs))) | " pubkey=" | myPubkey[:16] | "...") if myPubkey == "" || len(relayURLs) == 0 { println("[relay-proxy] handleMLSSub: no pubkey or no URLs, aborting") return } for rSubID := range mlsSubIDs { for _, c := range rpool.AllConns() { c.CloseSubscription(rSubID) } } mlsSubIDs = map[string]bool{} mlsGroupIDs = groupIDs for _, url := range relayURLs { url = normalizeRelayURL(url) if !isAllowedRelay(url) { println("[relay-proxy] handleMLSSub: blocked url=" | url) continue } suffix := urlSuffix(url) idP := "mlsp_" | suffix mlsSubIDs[idP] = true c := getConn(url) println("[relay-proxy] handleMLSSub: sub " | idP | " kinds=[443,1059] #p=" | myPubkey[:16] | "... on " | url) c.Subscribe(idP, []*nostr.Filter{{ Kinds: []uint32{443, 1059}, Tags: map[string][]string{"#p": {myPubkey}}, }}) if len(mlsGroupIDs) > 0 { idH := "mlsh_" | suffix mlsSubIDs[idH] = true println("[relay-proxy] handleMLSSub: sub " | idH | " kind=445 #h groups=" | helpers.Itoa(int64(len(mlsGroupIDs)))) c.Subscribe(idH, []*nostr.Filter{{ Kinds: []uint32{445}, Tags: map[string][]string{"#h": mlsGroupIDs}, }}) } } } func handleMLSUpdateGroups(groupIDs []string) { mlsGroupIDs = groupIDs for _, c := range rpool.AllConns() { suffix := urlSuffix(c.URL) idH := "mlsh_" | suffix c.CloseSubscription(idH) delete(mlsSubIDs, idH) if len(mlsGroupIDs) > 0 && mlsSubIDs["mlsp_"|suffix] { mlsSubIDs[idH] = true c.Subscribe(idH, []*nostr.Filter{{ Kinds: []uint32{445}, Tags: map[string][]string{"#h": mlsGroupIDs}, }}) } } } func handleMLSFetchKP(peer string, relayURLs []string) { println("[relay-proxy] handleMLSFetchKP: peer=" | peer[:16] | "... urls=" | helpers.Itoa(int64(len(relayURLs)))) if peer == "" { println("[relay-proxy] handleMLSFetchKP: no peer") relayproxy.WorkerPost(`["MLS_KP_RESULT",` | jstr(peer) | `,null]`) return } // Resolve shared relays: fetch peer's kind 10002, intersect with ours. // Falls back to provided URLs if no intersection found. fallback := relayURLs if len(fallback) == 0 { fallback = writeRelays } resolveSharedRelays(peer, fallback, func(urls []string) { if len(urls) == 0 { println("[relay-proxy] handleMLSFetchKP: no relays after resolution") relayproxy.WorkerPost(`["MLS_KP_RESULT",` | jstr(peer) | `,null]`) return } doKPFetch(peer, urls) }) } func doKPFetch(peer string, relayURLs []string) { if mlsKPFetchID != "" { closeKPFetchSubs() } mlsKPFetchID = "mlskp_" | peer[:8] mlsKPFetchPeer = peer mlsKPFetchSubs = nil println("[relay-proxy] doKPFetch: peer=" | peer[:16] | "... urls=" | helpers.Itoa(int64(len(relayURLs)))) for _, rawURL := range relayURLs { url := normalizeRelayURL(rawURL) if !isAllowedRelay(url) { println("[relay-proxy] doKPFetch: blocked url=" | url) continue } doKPFetchDirect(peer, url) return } println("[relay-proxy] doKPFetch: no valid URLs") mlsKPFetchID = "" relayproxy.WorkerPost(`["MLS_KP_RESULT",` | jstr(peer) | `,null]`) } // doKPFetchDirect opens a dedicated WebSocket to fetch a single KP event. // Bypasses the pool connection to avoid state interference. func doKPFetchDirect(peer, url string) { subID := "kpq" reqMsg := `["REQ","` | subID | `",{"kinds":[443],"authors":["` | peer | `"],"limit":1}]` println("[relay-proxy] doKPFetchDirect: opening dedicated ws to " | url) println("[relay-proxy] doKPFetchDirect: REQ=" | reqMsg) done := false fetchPeer := peer wsConn := ws.Dial(url, func(connID int32, data string) { if done { return } println("[relay-proxy] doKPFetchDirect: recv len=" | helpers.Itoa(int64(len(data)))) label, rSubID, payload := nostr.ParseRelayMessage(data) println("[relay-proxy] doKPFetchDirect: label=" | label | " subID=" | rSubID) if label == "EVENT" && rSubID == subID { ev := nostr.ParseEvent(payload) if ev != nil && ev.Kind == 443 { done = true evJSON := ev.ToJSON() println("[relay-proxy] doKPFetchDirect: FOUND KP for " | fetchPeer[:16] | "...") mlsKPFetchID = "" ws.Close(ws.Conn(connID)) relayproxy.WorkerPost(`["MLS_KP_RESULT",` | jstr(fetchPeer) | `,` | evJSON | `]`) } } if label == "EOSE" && rSubID == subID && !done { done = true println("[relay-proxy] doKPFetchDirect: EOSE with no KP for " | fetchPeer[:16] | "...") mlsKPFetchID = "" ws.Close(ws.Conn(connID)) relayproxy.WorkerPost(`["MLS_KP_RESULT",` | jstr(fetchPeer) | `,null]`) } }, func(connID int32) { println("[relay-proxy] doKPFetchDirect: connected, sending REQ") ws.Send(ws.Conn(connID), reqMsg) }, func(connID int32, code int32, reason string) { if !done { done = true println("[relay-proxy] doKPFetchDirect: ws closed code=" | helpers.Itoa(int64(code))) mlsKPFetchID = "" relayproxy.WorkerPost(`["MLS_KP_RESULT",` | jstr(fetchPeer) | `,null]`) } }, func(connID int32) { if !done { done = true println("[relay-proxy] doKPFetchDirect: ws error") mlsKPFetchID = "" relayproxy.WorkerPost(`["MLS_KP_RESULT",` | jstr(fetchPeer) | `,null]`) } }, ) _ = wsConn relayproxy.WorkerSetTimeout(10000, func() { if !done { done = true println("[relay-proxy] doKPFetchDirect: TIMEOUT for " | fetchPeer[:16] | "...") mlsKPFetchID = "" relayproxy.WorkerPost(`["MLS_KP_RESULT",` | jstr(fetchPeer) | `,null]`) } }) } func closeKPFetchSubs() { for _, sid := range mlsKPFetchSubs { for _, c := range rpool.AllConns() { c.CloseSubscription(sid) } } mlsKPFetchSubs = nil } // parseRelayList extracts relay URLs from a kind 10002 event. // Returns URLs where the peer reads (or has no marker = both). func parseRelayList(ev *nostr.Event) (ss []string) { var urls []string for _, tag := range ev.Tags { if len(tag) < 2 || tag[0] != "r" { continue } url := tag[1] if len(url) < 6 { continue } // If marker present, include only "read" or no marker (= both). if len(tag) >= 3 && tag[2] == "write" { continue } urls = append(urls, normalizeRelayURL(url)) } return urls } // resolveSharedRelays checks the cache for the peer's relay list. // If cached, returns intersection immediately via cb. // If not cached, fetches kind 10002 from connected relays, then calls cb. func resolveSharedRelays(peer string, fallback []string, cb func([]string)) { if info, ok := peerRelayCache[peer]; ok { shared := intersectRelays(writeRelays, info.urls) println("[relay-proxy] resolveSharedRelays: cached peer=" | peer[:16] | "... shared=" | helpers.Itoa(int64(len(shared)))) if len(shared) > 0 { cb(shared) return } cb(fallback) return } fetchPeerRelayList(peer, func(urls []string) { if len(urls) > 0 { peerRelayCache[peer] = &peerRelayInfo{urls: urls} shared := intersectRelays(writeRelays, urls) println("[relay-proxy] resolveSharedRelays: fetched peer=" | peer[:16] | "... peerURLs=" | helpers.Itoa(int64(len(urls))) | " shared=" | helpers.Itoa(int64(len(shared)))) if len(shared) > 0 { cb(shared) return } } else { println("[relay-proxy] resolveSharedRelays: no 10002 for peer=" | peer[:16] | "... using fallback") } cb(fallback) }) } func fetchPeerRelayList(peer string, cb func([]string)) { if rlFetchID != "" { closeRLFetchSubs() } rlFetchID = "rl10k_" | peer[:8] rlFetchPeer = peer rlFetchSubs = nil rlFetchCB = cb rlFetchEOSE = 0 connURLs := rpool.URLs() if len(connURLs) == 0 { connURLs = writeRelays } for _, url := range connURLs { url = normalizeRelayURL(url) if !isAllowedRelay(url) { continue } suffix := urlSuffix(url) subID := rlFetchID | "_" | suffix rlFetchSubs = append(rlFetchSubs, subID) c := getConn(url) println("[relay-proxy] fetchPeerRelayList: sub " | subID | " kind=10002 author=" | peer[:16] | "... on " | url) c.Subscribe(subID, []*nostr.Filter{{ Kinds: []uint32{10002}, Authors: []string{peer}, Limit: 1, }}) } fetchPrefix := rlFetchID relayproxy.WorkerSetTimeout(5000, func() { if rlFetchID == fetchPrefix { println("[relay-proxy] fetchPeerRelayList: TIMEOUT for " | rlFetchPeer[:16] | "...") fn := rlFetchCB closeRLFetchSubs() rlFetchID = "" rlFetchCB = nil if fn != nil { fn(nil) } } }) } func closeRLFetchSubs() { for _, sid := range rlFetchSubs { for _, c := range rpool.AllConns() { c.CloseSubscription(sid) } } rlFetchSubs = nil } func intersectRelays(ours, theirs []string) (ss []string) { var out []string for _, u := range ours { for _, t := range theirs { if normalizeRelayURL(u) == normalizeRelayURL(t) { out = append(out, u) break } } } return out } func isHex(s string) (ok bool) { for i := 0; i < len(s); i++ { c := s[i] if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { return false } } return true } func handleEventPublish(eventRaw string) { ev := nostr.ParseEvent(eventRaw) if ev == nil { return } relayproxy.WorkerPost(`["S_PUT_EVENT",` | eventRaw | `]`) for _, url := range writeRelays { if isAllowedRelay(url) { getConn(url).Publish(ev) } } relayproxy.WorkerPost(`["OK",` | jstr(ev.ID) | `,true,""]`) } func handlePublishTo(eventRaw string, relayURLs []string) { ev := nostr.ParseEvent(eventRaw) if ev == nil { println("[relay-proxy] handlePublishTo: parse failed") return } println("[relay-proxy] handlePublishTo: kind=" | helpers.Itoa(int64(ev.Kind)) | " id=" | ev.ID[:16] | "... to " | helpers.Itoa(int64(len(relayURLs))) | " relays") relayproxy.WorkerPost(`["S_PUT_EVENT",` | eventRaw | `]`) for _, url := range relayURLs { url = normalizeRelayURL(url) if isAllowedRelay(url) { c := getConn(url) oStr := "closed" if c.IsOpen() { oStr = "open" } println("[relay-proxy] handlePublishTo: publishing to " | url | " conn=" | oStr) c.Publish(ev) } } relayproxy.WorkerPost(`["OK",` | jstr(ev.ID) | `,true,""]`) } func handleSetWriteRelays(urls []string) { writeRelays = nil for _, u := range urls { nu := normalizeRelayURL(u) if isAllowedRelay(nu) { writeRelays = append(writeRelays, nu) } } println("[relay-proxy] SET_WRITE_RELAYS: " | helpers.Itoa(int64(len(writeRelays))) | " relays") for _, u := range writeRelays { println("[relay-proxy] relay: " | u) } } func handleReq(subID, filterRaw string) { f := nostr.ParseFilter(filterRaw) if f == nil { return } clientSubs[subID] = &clientSub{filter: f, filterRaw: filterRaw} sid := subID storeQuery(filterRaw, func(eventsJSON string) { events := nostr.ParseEventsJSON(eventsJSON) for _, ev := range events { relayproxy.WorkerPost(`["EVENT",` | jstr(sid) | `,` | ev.ToJSON() | `]`) } relayproxy.WorkerPost(`["EOSE",` | jstr(sid) | `]`) }) } func handleClose(subID string) { delete(clientSubs, subID) cleanupProxy(subID) } func handleProxy(subID, filterRaw string, relayURLs []string, live bool) { cleanupProxy(subID) f := nostr.ParseFilter(filterRaw) if f == nil { return } clientSubs[subID] = &clientSub{filter: f, filterRaw: filterRaw} sid := subID hasSearchField := false for i := 0; i+8 <= len(filterRaw); i++ { if filterRaw[i:i+8] == "\"search\"" { hasSearchField = true break } } if !hasSearchField { storeQuery(filterRaw, func(eventsJSON string) { events := nostr.ParseEventsJSON(eventsJSON) for _, ev := range events { relayproxy.WorkerPost(`["EVENT",` | jstr(sid) | `,` | ev.ToJSON() | `]`) } }) } remoteIDs := map[string]bool{} base := "p_" | subID | "_" seen := map[string]bool{} deduped := []string{:0:len(relayURLs)} for _, url := range relayURLs { url = normalizeRelayURL(url) if seen[url] { continue } seen[url] = true deduped = append(deduped, url) } proxySubs[subID] = &proxySub{ remoteIDs: remoteIDs, relayCount: len(deduped), live: live, } for _, url := range deduped { if !isAllowedRelay(url) { continue } suffix := urlSuffix(url) rSubID := base | suffix remoteIDs[rSubID] = true remoteToProxy[rSubID] = subID c := getConn(url) c.Subscribe(rSubID, []*nostr.Filter{f}) } proxyID := subID proxySubs[subID].timer = relayproxy.WorkerSetTimeout(proxyEOSETimeoutMs, func() { info, ok := proxySubs[proxyID] if !ok || info.done { return } info.done = true if _, ok := clientSubs[proxyID]; ok { relayproxy.WorkerPost(`["EOSE",` | jstr(proxyID) | `]`) } if info.live { return } // Linger: keep the remote subs open for proxyLingerMs after EOSE so // events that arrive late from slow relays still get forwarded. The // consumer already got its EOSE so it can proceed, but stragglers // arriving during the linger window still match findProxySub and // reach the app. Closing immediately on EOSE timer was the source of // inconsistent profile resolution. info.timer = relayproxy.WorkerSetTimeout(proxyLingerMs, func() { info, ok := proxySubs[proxyID] if !ok { return } for rSubID := range info.remoteIDs { delete(remoteToProxy, rSubID) for _, c := range rpool.AllConns() { c.CloseSubscription(rSubID) } } delete(proxySubs, proxyID) delete(clientSubs, proxyID) }) }) } func cleanupProxy(proxyID string) { info, ok := proxySubs[proxyID] if !ok { return } relayproxy.WorkerClearTimeout(info.timer) if !info.done { if _, ok := clientSubs[proxyID]; ok { relayproxy.WorkerPost(`["EOSE",` | jstr(proxyID) | `]`) } } for rSubID := range info.remoteIDs { delete(remoteToProxy, rSubID) for _, c := range rpool.AllConns() { c.CloseSubscription(rSubID) } } delete(proxySubs, proxyID) } func getConn(url string) (p *relay.Conn) { c := rpool.Connect(url) if c.ScheduleReconnect == nil { wireConn(c) } return c } func wireConn(c *relay.Conn) { url := c.URL c.ScheduleReconnect = func(delayMs int32, fn func()) { relayproxy.WorkerSetTimeout(delayMs, fn) } c.SetOnEvent(func(rSubID string, ev *nostr.Event) { if mlsSubIDs[rSubID] { evJSON := ev.ToJSON() println("[relay-proxy] MLS_EVENT: sub=" | rSubID | " kind=" | helpers.Itoa(int64(ev.Kind)) | " from=" | ev.PubKey[:16] | "...") relayproxy.WorkerPost(`["MLS_EVENT",` | evJSON | `]`) return } if mlsKPFetchID != "" && len(rSubID) >= len(mlsKPFetchID) && rSubID[:len(mlsKPFetchID)] == mlsKPFetchID && ev.Kind == 443 { evJSON := ev.ToJSON() peer := mlsKPFetchPeer println("[relay-proxy] MLS_KP_RESULT: found KP for " | peer[:16] | "... on sub=" | rSubID) closeKPFetchSubs() mlsKPFetchID = "" relayproxy.WorkerPost(`["MLS_KP_RESULT",` | jstr(peer) | `,` | evJSON | `]`) return } if rlFetchID != "" && len(rSubID) >= len(rlFetchID) && rSubID[:len(rlFetchID)] == rlFetchID && ev.Kind == 10002 { urls := parseRelayList(ev) peer := rlFetchPeer println("[relay-proxy] RL_RESULT: peer=" | peer[:16] | "... urls=" | helpers.Itoa(int64(len(urls)))) fn := rlFetchCB closeRLFetchSubs() rlFetchID = "" rlFetchCB = nil if fn != nil { fn(urls) } return } // Proxy event path: skip CheckSig here - the Verify Supervisor in // wasm-host.mjs intercepts ["EVENT",...] and verifies before forwarding // to the app worker. Unverified events never reach app business logic. proxyID, info := findProxySub(rSubID) if info == nil { return } cs, ok := clientSubs[proxyID] if !ok { return } if cs.filter != nil && !cs.filter.Matches(ev) { return } evJSON := ev.ToJSON() relayproxy.WorkerPost(`["S_PUT_EVENT",` | evJSON | `]`) relayproxy.WorkerPost(`["EVENT",` | jstr(proxyID) | `,` | evJSON | `]`) relayproxy.WorkerPost(`["SEEN_ON",` | jstr(ev.ID) | `,` | jstr(url) | `]`) }) c.SetOnEOSE(func(rSubID string) { if mlsSubIDs[rSubID] || (mlsKPFetchID != "" && len(rSubID) >= len(mlsKPFetchID) && rSubID[:len(mlsKPFetchID)] == mlsKPFetchID) { println("[relay-proxy] MLS EOSE: sub=" | rSubID | " relay=" | url) } if rlFetchID != "" && len(rSubID) >= len(rlFetchID) && rSubID[:len(rlFetchID)] == rlFetchID { rlFetchEOSE++ println("[relay-proxy] RL EOSE: sub=" | rSubID | " relay=" | url | " eose=" | helpers.Itoa(int64(rlFetchEOSE)) | "/" | helpers.Itoa(int64(len(rlFetchSubs)))) if rlFetchEOSE >= len(rlFetchSubs) { fn := rlFetchCB closeRLFetchSubs() rlFetchID = "" rlFetchCB = nil if fn != nil { fn(nil) } } } }) c.SetOnOK(func(eventID string, ok bool, msg string) { okStr := "true" if !ok { okStr = "false" println("[relay-proxy] OK REJECTED: id=" | eventID[:16] | "... ok=" | okStr | " msg=" | msg | " relay=" | url) } relayproxy.WorkerPost(`["OK",` | jstr(eventID) | `,` | okStr | `,` | jstr(msg) | `]`) if ok { relayproxy.WorkerPost(`["SEEN_ON",` | jstr(eventID) | `,` | jstr(url) | `]`) } }) } func findProxySub(rSubID string) (string, *proxySub) { proxyID, ok := remoteToProxy[rSubID] if !ok { return "", nil } info, ok := proxySubs[proxyID] if !ok { delete(remoteToProxy, rSubID) return "", nil } return proxyID, info } func normalizeRelayURL(url string) (s string) { for len(url) > 0 && url[len(url)-1] == '/' { url = url[:len(url)-1] } return url } func isAllowedRelay(url string) (ok bool) { if len(url) >= 6 && url[:6] == "wss://" { return true } if len(url) >= 16 && url[:16] == "ws://localhost:" { return true } if len(url) >= 15 && url[:15] == "ws://127.0.0.1:" { return true } return false } func urlSuffix(url string) (s string) { // Extract host from wss://host/ or wss://host start := 0 for i := 0; i < len(url); i++ { if url[i] == '/' && i+1 < len(url) && url[i+1] == '/' { start = i + 2 break } } end := len(url) for i := start; i < len(url); i++ { if url[i] == '/' || url[i] == ':' { end = i break } } host := url[start:end] out := []byte{:0:len(host)} for i := 0; i < len(host); i++ { c := host[i] if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') { out = append(out, c) } } if len(out) > 12 { out = out[len(out)-12:] } return string(out) } func jstr(s string) (sv string) { return helpers.JsonString(s) } func dup(s string) (sv string) { if len(s) == 0 { return "" } b := []byte{:len(s)} copy(b, s) return string(b) } func dupStrs(ss []string) (ss2 []string) { if len(ss) == 0 { return nil } out := []string{:0:len(ss)} for _, s := range ss { out = append(out, dup(s)) } return out }