Skip to main content

hashtree_cli/server/
ws_relay.rs

1use axum::{
2    extract::{
3        ws::{Message, WebSocket, WebSocketUpgrade},
4        State,
5    },
6    response::IntoResponse,
7};
8use futures::{SinkExt, StreamExt};
9use hashtree_core::{from_hex, sha256};
10use nostr::{
11    ClientMessage as NostrClientMessage, Filter as NostrFilter, JsonUtil as NostrJsonUtil,
12    RelayMessage as NostrRelayMessage, SubscriptionId,
13};
14use serde::{Deserialize, Serialize};
15use std::{collections::HashSet, time::Duration};
16use tokio::sync::{mpsc, watch};
17use tokio_tungstenite::{connect_async, tungstenite::Message as TungsteniteMessage};
18
19use super::auth::{AppState, PendingRequest, UpstreamNostrSubscription, WsProtocol};
20use crate::diagnostics::{
21    nostr_filters_summary, process_memory_snapshot, trim_process_allocations,
22};
23use crate::webrtc::types::{
24    encode_request, encode_response, parse_message, DataMessage, DataRequest, DataResponse, MAX_HTL,
25};
26use hex::encode as hex_encode;
27
28#[derive(Debug, Deserialize)]
29#[serde(tag = "type")]
30enum WsClientMessage {
31    #[serde(rename = "req")]
32    Request { id: u32, hash: String },
33    #[serde(rename = "res")]
34    Response { id: u32, hash: String, found: bool },
35}
36
37#[derive(Debug)]
38enum WsTextMessage<'a> {
39    Hashtree(WsClientMessage),
40    Nostr(NostrClientMessage<'a>),
41}
42
43#[derive(Debug, Deserialize, Serialize)]
44struct WsRequest {
45    #[serde(rename = "type")]
46    kind: String,
47    id: u32,
48    hash: String,
49}
50
51#[derive(Debug, Serialize)]
52struct WsResponse {
53    #[serde(rename = "type")]
54    kind: &'static str,
55    id: u32,
56    hash: String,
57    found: bool,
58}
59
60pub async fn ws_data(State(state): State<AppState>, ws: WebSocketUpgrade) -> impl IntoResponse {
61    ws_data_with_client_pubkey(state, ws, None)
62}
63
64pub fn ws_data_with_client_pubkey(
65    state: AppState,
66    ws: WebSocketUpgrade,
67    client_pubkey: Option<String>,
68) -> impl IntoResponse {
69    ws.on_upgrade(move |socket| handle_socket(socket, state, client_pubkey))
70}
71
72async fn handle_socket(socket: WebSocket, state: AppState, client_pubkey: Option<String>) {
73    // Use the Nostr relay's client-id generator when available so `/ws` IDs
74    // can't collide with WebRTC relay clients.
75    let client_id = state
76        .nostr_relay
77        .as_ref()
78        .map(|relay| relay.next_client_id())
79        .unwrap_or_else(|| state.ws_relay.next_id());
80    let (tx, mut rx) = mpsc::unbounded_channel::<Message>();
81
82    {
83        let mut clients = state.ws_relay.clients.lock().await;
84        clients.insert(client_id, tx);
85    }
86    {
87        let mut protocols = state.ws_relay.client_protocols.lock().await;
88        protocols.insert(client_id, WsProtocol::HashtreeJson);
89    }
90
91    let mut nostr_rx = if let Some(relay) = state.nostr_relay.clone() {
92        let (nostr_tx, nostr_rx) = mpsc::unbounded_channel::<String>();
93        relay
94            .register_client(client_id, nostr_tx, client_pubkey.clone())
95            .await;
96        Some(nostr_rx)
97    } else {
98        None
99    };
100
101    let (mut sender, mut receiver) = socket.split();
102    loop {
103        tokio::select! {
104            maybe_msg = rx.recv() => {
105                let Some(msg) = maybe_msg else {
106                    break;
107                };
108                if sender.send(msg).await.is_err() {
109                    break;
110                }
111            }
112            maybe_text = async {
113                match &mut nostr_rx {
114                    Some(rx) => rx.recv().await,
115                    None => std::future::pending().await,
116                }
117            } => {
118                let Some(text) = maybe_text else {
119                    nostr_rx = None;
120                    continue;
121                };
122                if sender.send(Message::Text(text)).await.is_err() {
123                    break;
124                }
125            }
126            maybe_incoming = receiver.next() => {
127                match maybe_incoming {
128                    Some(Ok(msg)) => handle_message(client_id, msg, &state).await,
129                    Some(Err(_)) | None => break,
130                }
131            }
132        }
133    }
134
135    close_all_upstream_nostr_subscriptions(&state, client_id).await;
136
137    {
138        let mut clients = state.ws_relay.clients.lock().await;
139        clients.remove(&client_id);
140    }
141    {
142        let mut protocols = state.ws_relay.client_protocols.lock().await;
143        protocols.remove(&client_id);
144    }
145    {
146        let mut pending = state.ws_relay.pending.lock().await;
147        pending.retain(|(peer_id, _), _| *peer_id != client_id);
148    }
149
150    if let Some(relay) = &state.nostr_relay {
151        relay.unregister_client(client_id).await;
152    }
153}
154
155fn parse_ws_text_message(text: &str) -> Option<WsTextMessage<'static>> {
156    let trimmed = text.trim_start();
157    if trimmed.starts_with('[') {
158        if let Ok(msg) = NostrClientMessage::from_json(trimmed) {
159            return Some(WsTextMessage::Nostr(msg));
160        }
161    }
162
163    if let Ok(msg) = serde_json::from_str::<WsClientMessage>(text) {
164        return Some(WsTextMessage::Hashtree(msg));
165    }
166
167    None
168}
169async fn close_upstream_nostr_subscription(
170    state: &AppState,
171    client_id: u64,
172    subscription_id: &SubscriptionId,
173) {
174    let key = (client_id, subscription_id.to_string());
175    let subscription = {
176        let mut subscriptions = state.ws_relay.upstream_nostr_subscriptions.lock().await;
177        subscriptions.remove(&key)
178    };
179    if let Some(subscription) = subscription {
180        let _ = subscription.close_tx.send(true);
181        for task in subscription.tasks {
182            task.abort();
183        }
184    }
185    state
186        .ws_relay
187        .upstream_pending_eose
188        .lock()
189        .await
190        .remove(&key);
191    state
192        .ws_relay
193        .upstream_seen_events
194        .lock()
195        .await
196        .remove(&key);
197}
198
199async fn close_all_upstream_nostr_subscriptions(state: &AppState, client_id: u64) {
200    let keys = {
201        let subscriptions = state.ws_relay.upstream_nostr_subscriptions.lock().await;
202        subscriptions
203            .keys()
204            .filter(|(id, _)| *id == client_id)
205            .cloned()
206            .collect::<Vec<_>>()
207    };
208    for (_, sub_id) in keys {
209        close_upstream_nostr_subscription(state, client_id, &SubscriptionId::new(sub_id)).await;
210    }
211}
212
213async fn forward_upstream_nostr_message(
214    state: &AppState,
215    client_id: u64,
216    subscription_id: &SubscriptionId,
217    text: &str,
218) {
219    let Ok(message) = NostrRelayMessage::from_json(text) else {
220        return;
221    };
222
223    match message {
224        NostrRelayMessage::Event {
225            subscription_id: sid,
226            event,
227        } if sid.as_ref() == subscription_id => {
228            let event = event.into_owned();
229            let key = (client_id, subscription_id.to_string());
230            let event_id = event.id.to_hex();
231            let inserted = {
232                let mut seen_events = state.ws_relay.upstream_seen_events.lock().await;
233                seen_events.entry(key).or_default().insert(event_id)
234            };
235            if !inserted {
236                return;
237            }
238            if let Some(relay) = &state.nostr_relay {
239                let _ = relay.ingest_trusted_event_silent(event.clone()).await;
240            }
241            send_nostr(
242                state,
243                client_id,
244                NostrRelayMessage::event(subscription_id.clone(), event),
245            )
246            .await;
247        }
248        NostrRelayMessage::Closed {
249            subscription_id: sid,
250            message,
251        } if sid.as_ref() == subscription_id => {
252            send_nostr(
253                state,
254                client_id,
255                NostrRelayMessage::closed(subscription_id.clone(), message.into_owned()),
256            )
257            .await;
258        }
259        _ => {}
260    }
261}
262
263async fn mark_upstream_nostr_relay_complete(
264    state: &AppState,
265    client_id: u64,
266    subscription_id: &SubscriptionId,
267) {
268    let key = (client_id, subscription_id.to_string());
269    let should_send_eose = {
270        let mut pending = state.ws_relay.upstream_pending_eose.lock().await;
271        let Some(remaining) = pending.get_mut(&key) else {
272            return;
273        };
274        if *remaining > 0 {
275            *remaining -= 1;
276        }
277        if *remaining == 0 {
278            pending.remove(&key);
279            true
280        } else {
281            false
282        }
283    };
284
285    if should_send_eose {
286        send_nostr(
287            state,
288            client_id,
289            NostrRelayMessage::eose(subscription_id.clone()),
290        )
291        .await;
292    }
293}
294
295async fn run_upstream_nostr_subscription(
296    state: AppState,
297    client_id: u64,
298    relay_url: String,
299    subscription_id: SubscriptionId,
300    filters: Vec<NostrFilter>,
301    mut close_rx: watch::Receiver<bool>,
302) {
303    let mut relay_complete = false;
304    let Ok((socket, _)) = connect_async(relay_url.as_str()).await else {
305        tracing::warn!(
306            "upstream nostr relay connect failed: client_id={} subscription_id={} relay={}",
307            client_id,
308            subscription_id,
309            relay_url,
310        );
311        mark_upstream_nostr_relay_complete(&state, client_id, &subscription_id).await;
312        return;
313    };
314    let (mut write, mut read) = socket.split();
315    let request = NostrClientMessage::req(subscription_id.clone(), filters).as_json();
316    state
317        .ws_relay
318        .note_upstream_relay_send(request.as_bytes().len());
319    if write
320        .send(TungsteniteMessage::Text(request.into()))
321        .await
322        .is_err()
323    {
324        tracing::warn!(
325            "upstream nostr relay request send failed: client_id={} subscription_id={} relay={}",
326            client_id,
327            subscription_id,
328            relay_url,
329        );
330        mark_upstream_nostr_relay_complete(&state, client_id, &subscription_id).await;
331        return;
332    }
333
334    loop {
335        tokio::select! {
336            _ = close_rx.changed() => {
337                if *close_rx.borrow() {
338                    let close = NostrClientMessage::close(subscription_id.clone()).as_json();
339                    state
340                        .ws_relay
341                        .note_upstream_relay_send(close.as_bytes().len());
342                    let _ = write.send(TungsteniteMessage::Text(close.into())).await;
343                    let _ = write.close().await;
344                    break;
345                }
346            }
347            message = read.next() => {
348                match message {
349                    Some(Ok(TungsteniteMessage::Text(text))) => {
350                        state
351                            .ws_relay
352                            .note_upstream_relay_receive(text.as_bytes().len());
353                        if matches!(
354                            NostrRelayMessage::from_json(text.as_str()),
355                            Ok(NostrRelayMessage::EndOfStoredEvents(sid)) if sid.as_ref() == &subscription_id
356                        ) {
357                            if !relay_complete {
358                                relay_complete = true;
359                                mark_upstream_nostr_relay_complete(&state, client_id, &subscription_id).await;
360                            }
361                            continue;
362                        }
363                        forward_upstream_nostr_message(&state, client_id, &subscription_id, &text).await;
364                    }
365                    Some(Ok(TungsteniteMessage::Ping(payload))) => {
366                        let _ = write.send(TungsteniteMessage::Pong(payload)).await;
367                    }
368                    Some(Ok(TungsteniteMessage::Close(_))) | None => {
369                        if !relay_complete {
370                            mark_upstream_nostr_relay_complete(&state, client_id, &subscription_id).await;
371                        }
372                        break;
373                    }
374                    Some(Err(_)) => {
375                        if !relay_complete {
376                            mark_upstream_nostr_relay_complete(&state, client_id, &subscription_id).await;
377                        }
378                        break;
379                    }
380                    _ => {}
381                }
382            }
383        }
384    }
385}
386
387async fn start_upstream_nostr_subscription(
388    state: &AppState,
389    client_id: u64,
390    subscription_id: SubscriptionId,
391    filters: Vec<NostrFilter>,
392) -> usize {
393    let memory_before = process_memory_snapshot();
394    close_upstream_nostr_subscription(state, client_id, &subscription_id).await;
395    if state.nostr_relay_urls.is_empty() || filters.is_empty() {
396        tracing::info!(
397            "upstream nostr relay skipped: client_id={} subscription_id={} relays={} filters={}",
398            client_id,
399            subscription_id,
400            state.nostr_relay_urls.len(),
401            filters.len(),
402        );
403        return 0;
404    }
405
406    let mut relay_urls = Vec::new();
407    let mut seen = HashSet::new();
408    for relay in &state.nostr_relay_urls {
409        let relay = relay.trim();
410        if relay.is_empty() || !seen.insert(relay.to_string()) {
411            continue;
412        }
413        relay_urls.push(relay.to_string());
414    }
415    if relay_urls.is_empty() {
416        tracing::info!(
417            "upstream nostr relay skipped after normalization: client_id={} subscription_id={}",
418            client_id,
419            subscription_id,
420        );
421        return 0;
422    }
423
424    tracing::info!(
425        "upstream nostr relay start: client_id={} subscription_id={} relays={}",
426        client_id,
427        subscription_id,
428        relay_urls.len(),
429    );
430
431    let filter_summary = nostr_filters_summary(&filters);
432    let key = (client_id, subscription_id.to_string());
433    state
434        .ws_relay
435        .upstream_seen_events
436        .lock()
437        .await
438        .insert(key.clone(), HashSet::new());
439    state
440        .ws_relay
441        .upstream_pending_eose
442        .lock()
443        .await
444        .insert(key.clone(), relay_urls.len());
445
446    let (close_tx, close_rx) = watch::channel(false);
447    let mut tasks = Vec::new();
448    for relay_url in &relay_urls {
449        tasks.push(tokio::spawn(run_upstream_nostr_subscription(
450            state.clone(),
451            client_id,
452            relay_url.clone(),
453            subscription_id.clone(),
454            filters.clone(),
455            close_rx.clone(),
456        )));
457    }
458
459    tracing::info!(
460        target: "hashtree_cli::server::ws_relay::upstream",
461        client_id,
462        subscription_id = %subscription_id,
463        relays = relay_urls.len(),
464        tasks = tasks.len(),
465        filters = filters.len(),
466        filter = %filter_summary,
467        memory_before = ?memory_before,
468        memory_after = ?process_memory_snapshot(),
469        "upstream nostr relay tasks spawned",
470    );
471
472    state
473        .ws_relay
474        .upstream_nostr_subscriptions
475        .lock()
476        .await
477        .insert(key, UpstreamNostrSubscription { close_tx, tasks });
478    relay_urls.len()
479}
480
481async fn handle_message(client_id: u64, msg: Message, state: &AppState) {
482    match msg {
483        Message::Text(text) => {
484            if let Some(msg) = parse_ws_text_message(&text) {
485                match msg {
486                    WsTextMessage::Hashtree(msg) => {
487                        set_client_protocol(state, client_id, WsProtocol::HashtreeJson).await;
488                        match msg {
489                            WsClientMessage::Request { id, hash } => {
490                                handle_request(
491                                    client_id,
492                                    id,
493                                    hash,
494                                    WsProtocol::HashtreeJson,
495                                    state,
496                                )
497                                .await;
498                            }
499                            WsClientMessage::Response { id, hash, found } => {
500                                handle_response(client_id, id, hash, found, state).await;
501                            }
502                        }
503                    }
504                    WsTextMessage::Nostr(msg) => {
505                        if let Some(relay) = &state.nostr_relay {
506                            match msg {
507                                NostrClientMessage::Req {
508                                    subscription_id,
509                                    filters,
510                                } => {
511                                    let subscription_id = subscription_id.into_owned();
512                                    let filters = filters
513                                        .into_iter()
514                                        .map(|filter| filter.into_owned())
515                                        .collect::<Vec<_>>();
516                                    let local_events = match relay
517                                        .register_subscription_query(
518                                            client_id,
519                                            subscription_id.clone(),
520                                            filters.clone(),
521                                        )
522                                        .await
523                                    {
524                                        Ok(events) => events,
525                                        Err(message) => {
526                                            send_nostr(
527                                                state,
528                                                client_id,
529                                                NostrRelayMessage::closed(subscription_id, message),
530                                            )
531                                            .await;
532                                            return;
533                                        }
534                                    };
535
536                                    let upstream_relays = start_upstream_nostr_subscription(
537                                        state,
538                                        client_id,
539                                        subscription_id.clone(),
540                                        filters,
541                                    )
542                                    .await;
543                                    if upstream_relays > 0 {
544                                        let key = (client_id, subscription_id.to_string());
545                                        let mut seen_events =
546                                            state.ws_relay.upstream_seen_events.lock().await;
547                                        seen_events.entry(key).or_default().extend(
548                                            local_events.iter().map(|event| event.id.to_hex()),
549                                        );
550                                    }
551                                    for event in local_events {
552                                        send_nostr(
553                                            state,
554                                            client_id,
555                                            NostrRelayMessage::event(
556                                                subscription_id.clone(),
557                                                event,
558                                            ),
559                                        )
560                                        .await;
561                                    }
562                                    trim_process_allocations();
563                                    if upstream_relays == 0 {
564                                        send_nostr(
565                                            state,
566                                            client_id,
567                                            NostrRelayMessage::eose(subscription_id),
568                                        )
569                                        .await;
570                                    }
571                                }
572                                NostrClientMessage::Close(subscription_id) => {
573                                    let subscription_id = subscription_id.into_owned();
574                                    close_upstream_nostr_subscription(
575                                        state,
576                                        client_id,
577                                        &subscription_id,
578                                    )
579                                    .await;
580                                    relay
581                                        .handle_client_message(
582                                            client_id,
583                                            NostrClientMessage::close(subscription_id.clone()),
584                                        )
585                                        .await;
586                                }
587                                other => {
588                                    relay.handle_client_message(client_id, other).await;
589                                }
590                            }
591                        } else {
592                            handle_nostr_message(client_id, msg, state).await;
593                        }
594                    }
595                }
596            }
597        }
598        Message::Binary(data) => {
599            handle_binary(client_id, data, state).await;
600        }
601        Message::Close(_) => {}
602        _ => {}
603    }
604}
605
606async fn handle_request(
607    client_id: u64,
608    request_id: u32,
609    hash: String,
610    origin_protocol: WsProtocol,
611    state: &AppState,
612) {
613    let hash_hex = hash.to_lowercase();
614    let hash_bytes = match from_hex(&hash_hex) {
615        Ok(bytes) => bytes,
616        Err(_) => {
617            if origin_protocol == WsProtocol::HashtreeJson {
618                send_json(
619                    state,
620                    client_id,
621                    WsResponse {
622                        kind: "res",
623                        id: request_id,
624                        hash,
625                        found: false,
626                    },
627                )
628                .await;
629            }
630            return;
631        }
632    };
633
634    if let Ok(Some(data)) = state.store.get_blob(&hash_bytes) {
635        match origin_protocol {
636            WsProtocol::HashtreeJson => {
637                send_json(
638                    state,
639                    client_id,
640                    WsResponse {
641                        kind: "res",
642                        id: request_id,
643                        hash: hash.clone(),
644                        found: true,
645                    },
646                )
647                .await;
648                send_binary(state, client_id, request_id, data).await;
649            }
650            WsProtocol::HashtreeMsgpack => {
651                send_msgpack_response(state, client_id, &hash_bytes, &data).await;
652            }
653            WsProtocol::Unknown => {}
654        }
655        return;
656    }
657
658    let peers: Vec<(u64, mpsc::UnboundedSender<Message>, WsProtocol)> = {
659        let clients = state.ws_relay.clients.lock().await;
660        let protocols = state.ws_relay.client_protocols.lock().await;
661        clients
662            .iter()
663            .filter(|(id, _)| **id != client_id)
664            .filter_map(|(id, tx)| {
665                let protocol = protocols.get(id).copied().unwrap_or(WsProtocol::Unknown);
666                match protocol {
667                    WsProtocol::HashtreeJson | WsProtocol::HashtreeMsgpack => {
668                        Some((*id, tx.clone(), protocol))
669                    }
670                    WsProtocol::Unknown => None,
671                }
672            })
673            .collect()
674    };
675
676    if peers.is_empty() {
677        if origin_protocol == WsProtocol::HashtreeJson {
678            send_json(
679                state,
680                client_id,
681                WsResponse {
682                    kind: "res",
683                    id: request_id,
684                    hash,
685                    found: false,
686                },
687            )
688            .await;
689        }
690        return;
691    }
692
693    {
694        let mut pending = state.ws_relay.pending.lock().await;
695        for (peer_id, _, _) in &peers {
696            pending.insert(
697                (*peer_id, request_id),
698                PendingRequest {
699                    origin_id: client_id,
700                    hash: hash.clone(),
701                    found: false,
702                    origin_protocol,
703                },
704            );
705        }
706    }
707
708    let request_text = serde_json::to_string(&WsRequest {
709        kind: "req".to_string(),
710        id: request_id,
711        hash: hash.clone(),
712    })
713    .unwrap_or_else(|_| String::new());
714    for (peer_id, tx, protocol) in peers {
715        match protocol {
716            WsProtocol::HashtreeMsgpack => {
717                let _ = send_msgpack_request(state, peer_id, &hash_bytes).await;
718            }
719            WsProtocol::HashtreeJson => {
720                let _ = tx.send(Message::Text(request_text.clone()));
721            }
722            WsProtocol::Unknown => {}
723        }
724    }
725
726    let timeout_state = state.clone();
727    let timeout_hash = hash.clone();
728    tokio::spawn(async move {
729        tokio::time::sleep(Duration::from_millis(1500)).await;
730        let mut pending = timeout_state.ws_relay.pending.lock().await;
731        let still_pending = pending
732            .iter()
733            .any(|((_, id), p)| *id == request_id && p.origin_id == client_id);
734        let already_found = pending
735            .iter()
736            .any(|((_, id), p)| *id == request_id && p.origin_id == client_id && p.found);
737        if !still_pending || already_found {
738            return;
739        }
740        let origin_protocol = pending
741            .iter()
742            .find(|((_, id), p)| *id == request_id && p.origin_id == client_id)
743            .map(|(_, p)| p.origin_protocol)
744            .unwrap_or(WsProtocol::HashtreeJson);
745        pending.retain(|(_, id), p| !(*id == request_id && p.origin_id == client_id));
746        drop(pending);
747        if origin_protocol == WsProtocol::HashtreeJson {
748            send_json(
749                &timeout_state,
750                client_id,
751                WsResponse {
752                    kind: "res",
753                    id: request_id,
754                    hash: timeout_hash,
755                    found: false,
756                },
757            )
758            .await;
759        }
760    });
761}
762
763async fn handle_response(
764    client_id: u64,
765    request_id: u32,
766    _hash: String,
767    found: bool,
768    state: &AppState,
769) {
770    let pending_entry = {
771        let pending = state.ws_relay.pending.lock().await;
772        pending
773            .get(&(client_id, request_id))
774            .map(|p| (p.origin_id, p.hash.clone(), p.origin_protocol))
775    };
776
777    let Some((origin_id, pending_hash, origin_protocol)) = pending_entry else {
778        return;
779    };
780
781    if found {
782        // Legacy JSON peers send a `found` control message before sending the
783        // binary body. Do not forward success until the body hashes correctly.
784        return;
785    }
786
787    let mut pending = state.ws_relay.pending.lock().await;
788    pending.remove(&(client_id, request_id));
789    let has_remaining = pending
790        .iter()
791        .any(|((_, id), p)| *id == request_id && p.origin_id == origin_id);
792    drop(pending);
793
794    if !has_remaining && origin_protocol == WsProtocol::HashtreeJson {
795        send_json(
796            state,
797            origin_id,
798            WsResponse {
799                kind: "res",
800                id: request_id,
801                hash: pending_hash,
802                found: false,
803            },
804        )
805        .await;
806    }
807}
808
809fn payload_matches_hash(hash: &[u8], data: &[u8]) -> bool {
810    hash.len() == 32 && sha256(data).as_slice() == hash
811}
812
813async fn handle_binary(client_id: u64, data: Vec<u8>, state: &AppState) {
814    if let Some(msg) = parse_msgpack_message(&data) {
815        set_client_protocol(state, client_id, WsProtocol::HashtreeMsgpack).await;
816        match msg {
817            DataMessage::Request(req) => {
818                let hash_hex = hex_encode(&req.h);
819                let request_id = state.ws_relay.next_request_id();
820                handle_request(
821                    client_id,
822                    request_id,
823                    hash_hex,
824                    WsProtocol::HashtreeMsgpack,
825                    state,
826                )
827                .await;
828            }
829            DataMessage::Response(res) => {
830                handle_msgpack_response(client_id, res, state).await;
831            }
832            DataMessage::QuoteRequest(_)
833            | DataMessage::QuoteResponse(_)
834            | DataMessage::Payment(_)
835            | DataMessage::PaymentAck(_)
836            | DataMessage::Chunk(_)
837            | DataMessage::PeerHints(_)
838            | DataMessage::PubsubInterest(_)
839            | DataMessage::PubsubFrame(_)
840            | DataMessage::PubsubInventory(_)
841            | DataMessage::PubsubWant(_) => {}
842        }
843        return;
844    }
845
846    // Legacy binary: [4-byte LE request_id][data]
847    if data.len() < 4 {
848        return;
849    }
850    let request_id = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
851    let pending_entry = {
852        let pending = state.ws_relay.pending.lock().await;
853        pending
854            .get(&(client_id, request_id))
855            .map(|p| (p.origin_id, p.hash.clone(), p.origin_protocol))
856    };
857    let Some((origin_id, hash_hex, origin_protocol)) = pending_entry else {
858        return;
859    };
860
861    let Ok(hash_bytes) = from_hex(&hash_hex) else {
862        return;
863    };
864    let payload = &data[4..];
865    if !payload_matches_hash(&hash_bytes, payload) {
866        return;
867    }
868
869    match origin_protocol {
870        WsProtocol::HashtreeJson => {
871            send_json(
872                state,
873                origin_id,
874                WsResponse {
875                    kind: "res",
876                    id: request_id,
877                    hash: hash_hex.clone(),
878                    found: true,
879                },
880            )
881            .await;
882            send_binary(state, origin_id, request_id, payload.to_vec()).await;
883        }
884        WsProtocol::HashtreeMsgpack => {
885            send_msgpack_response(state, origin_id, &hash_bytes, payload).await;
886        }
887        WsProtocol::Unknown => {}
888    }
889
890    let mut pending = state.ws_relay.pending.lock().await;
891    pending.retain(|(_, id), p| !(*id == request_id && p.origin_id == origin_id));
892}
893
894async fn handle_nostr_message(client_id: u64, msg: NostrClientMessage<'_>, state: &AppState) {
895    let replies = nostr_responses_for(&msg);
896    for reply in replies {
897        send_nostr(state, client_id, reply).await;
898    }
899}
900
901fn nostr_responses_for(msg: &NostrClientMessage<'_>) -> Vec<NostrRelayMessage<'static>> {
902    match msg {
903        NostrClientMessage::Event(event) => {
904            let ok = event.verify().is_ok();
905            let message = if ok { "" } else { "invalid: signature" };
906            vec![NostrRelayMessage::ok(event.id, ok, message)]
907        }
908        NostrClientMessage::Req {
909            subscription_id, ..
910        } => {
911            vec![NostrRelayMessage::eose(
912                subscription_id.clone().into_owned(),
913            )]
914        }
915        NostrClientMessage::Count {
916            subscription_id, ..
917        } => {
918            vec![NostrRelayMessage::count(
919                subscription_id.clone().into_owned(),
920                0,
921            )]
922        }
923        NostrClientMessage::Close(_) => Vec::new(),
924        NostrClientMessage::Auth(event) => {
925            let ok = event.verify().is_ok();
926            let message = if ok { "" } else { "invalid auth" };
927            vec![NostrRelayMessage::ok(event.id, ok, message)]
928        }
929        NostrClientMessage::NegOpen { .. }
930        | NostrClientMessage::NegMsg { .. }
931        | NostrClientMessage::NegClose { .. } => {
932            vec![NostrRelayMessage::notice("negentropy not supported")]
933        }
934    }
935}
936
937async fn send_nostr(state: &AppState, client_id: u64, response: NostrRelayMessage<'_>) {
938    let text = response.as_json();
939    send_to_client(state, client_id, Message::Text(text)).await;
940}
941
942fn parse_msgpack_message(data: &[u8]) -> Option<DataMessage> {
943    let msg = parse_message(data).ok()?;
944    match msg {
945        DataMessage::Request(req) => {
946            if req.h.len() == 32 {
947                Some(DataMessage::Request(req))
948            } else {
949                None
950            }
951        }
952        DataMessage::Response(res) => {
953            if res.h.len() == 32 {
954                Some(DataMessage::Response(res))
955            } else {
956                None
957            }
958        }
959        DataMessage::QuoteRequest(req) => {
960            if req.h.len() == 32 {
961                Some(DataMessage::QuoteRequest(req))
962            } else {
963                None
964            }
965        }
966        DataMessage::QuoteResponse(res) => {
967            if res.h.len() == 32 {
968                Some(DataMessage::QuoteResponse(res))
969            } else {
970                None
971            }
972        }
973        DataMessage::Payment(req) => {
974            if req.h.len() == 32 {
975                Some(DataMessage::Payment(req))
976            } else {
977                None
978            }
979        }
980        DataMessage::PaymentAck(res) => {
981            if res.h.len() == 32 {
982                Some(DataMessage::PaymentAck(res))
983            } else {
984                None
985            }
986        }
987        DataMessage::Chunk(chunk) => {
988            if chunk.h.len() == 32 {
989                Some(DataMessage::Chunk(chunk))
990            } else {
991                None
992            }
993        }
994        DataMessage::PeerHints(_)
995        | DataMessage::PubsubInterest(_)
996        | DataMessage::PubsubFrame(_)
997        | DataMessage::PubsubInventory(_)
998        | DataMessage::PubsubWant(_) => Some(msg),
999    }
1000}
1001
1002async fn handle_msgpack_response(client_id: u64, res: DataResponse, state: &AppState) {
1003    let hash_hex = hex_encode(&res.h);
1004    let data = res.d.clone();
1005    let hash_bytes = res.h.clone();
1006
1007    if !payload_matches_hash(&hash_bytes, &data) {
1008        return;
1009    }
1010
1011    let mut responses: Vec<(u64, u32, WsProtocol)> = Vec::new();
1012    let mut seen = HashSet::new();
1013    {
1014        let pending = state.ws_relay.pending.lock().await;
1015        for ((peer_id, request_id), p) in pending.iter() {
1016            if *peer_id != client_id {
1017                continue;
1018            }
1019            if p.hash != hash_hex {
1020                continue;
1021            }
1022            if seen.insert((p.origin_id, *request_id)) {
1023                responses.push((p.origin_id, *request_id, p.origin_protocol));
1024            }
1025        }
1026    }
1027
1028    if responses.is_empty() {
1029        return;
1030    }
1031
1032    for (origin_id, request_id, protocol) in &responses {
1033        match protocol {
1034            WsProtocol::HashtreeJson => {
1035                send_json(
1036                    state,
1037                    *origin_id,
1038                    WsResponse {
1039                        kind: "res",
1040                        id: *request_id,
1041                        hash: hash_hex.clone(),
1042                        found: true,
1043                    },
1044                )
1045                .await;
1046                send_binary(state, *origin_id, *request_id, data.clone()).await;
1047            }
1048            WsProtocol::HashtreeMsgpack => {
1049                send_msgpack_response(state, *origin_id, &hash_bytes, &data).await;
1050            }
1051            WsProtocol::Unknown => {}
1052        }
1053    }
1054
1055    let completed: HashSet<(u64, u32)> = responses
1056        .into_iter()
1057        .map(|(origin_id, request_id, _)| (origin_id, request_id))
1058        .collect();
1059    let mut pending = state.ws_relay.pending.lock().await;
1060    pending.retain(|(_, id), p| !completed.contains(&(p.origin_id, *id)));
1061}
1062
1063async fn send_json(state: &AppState, client_id: u64, response: WsResponse) {
1064    if let Ok(text) = serde_json::to_string(&response) {
1065        send_to_client(state, client_id, Message::Text(text)).await;
1066    }
1067}
1068
1069async fn send_msgpack_request(
1070    state: &AppState,
1071    client_id: u64,
1072    hash: &[u8],
1073) -> Result<(), rmp_serde::encode::Error> {
1074    let req = DataRequest {
1075        h: hash.to_vec(),
1076        htl: MAX_HTL,
1077        q: None,
1078    };
1079    let wire = encode_request(&req)?;
1080    send_to_client(state, client_id, Message::Binary(wire)).await;
1081    Ok(())
1082}
1083
1084async fn send_msgpack_response(state: &AppState, client_id: u64, hash: &[u8], data: &[u8]) {
1085    let res = DataResponse {
1086        h: hash.to_vec(),
1087        d: data.to_vec(),
1088        i: None,
1089        n: None,
1090    };
1091    if let Ok(wire) = encode_response(&res) {
1092        send_to_client(state, client_id, Message::Binary(wire)).await;
1093    }
1094}
1095
1096async fn send_binary(state: &AppState, client_id: u64, request_id: u32, payload: Vec<u8>) {
1097    let mut packet = Vec::with_capacity(4 + payload.len());
1098    packet.extend_from_slice(&request_id.to_le_bytes());
1099    packet.extend_from_slice(&payload);
1100    send_to_client(state, client_id, Message::Binary(packet)).await;
1101}
1102
1103async fn send_to_client(state: &AppState, client_id: u64, msg: Message) {
1104    let sender = {
1105        let clients = state.ws_relay.clients.lock().await;
1106        clients.get(&client_id).cloned()
1107    };
1108    if let Some(tx) = sender {
1109        let _ = tx.send(msg);
1110    }
1111}
1112
1113async fn set_client_protocol(state: &AppState, client_id: u64, protocol: WsProtocol) {
1114    let mut protocols = state.ws_relay.client_protocols.lock().await;
1115    protocols.insert(client_id, protocol);
1116}
1117
1118#[cfg(test)]
1119mod tests {
1120    use super::*;
1121    use crate::nostr_relay::{NostrRelay, NostrRelayConfig};
1122    use anyhow::Result;
1123    use futures::{SinkExt, StreamExt};
1124    use nostr::secp256k1::schnorr::Signature;
1125    use nostr::{EventBuilder, Filter, Keys, Kind, SubscriptionId};
1126    use std::collections::HashSet;
1127    use std::sync::Arc;
1128    use tempfile::TempDir;
1129    use tokio::net::TcpListener;
1130    use tokio_tungstenite::{accept_async, tungstenite::Message as TungsteniteMessage};
1131
1132    #[test]
1133    fn parse_ws_text_message_detects_nostr_req() {
1134        let msg = r#"["REQ","sub-1",{"kinds":[1]}]"#;
1135        match parse_ws_text_message(msg) {
1136            Some(WsTextMessage::Nostr(_)) => {}
1137            other => panic!("expected Nostr message, got {:?}", other),
1138        }
1139    }
1140
1141    #[test]
1142    fn parse_ws_text_message_detects_hashtree_request() {
1143        let msg = r#"{"type":"req","id":1,"hash":"abcd"}"#;
1144        match parse_ws_text_message(msg) {
1145            Some(WsTextMessage::Hashtree(_)) => {}
1146            other => panic!("expected Hashtree message, got {:?}", other),
1147        }
1148    }
1149
1150    #[test]
1151    fn nostr_replies_for_req_is_eose() {
1152        let sub = SubscriptionId::new("sub-1");
1153        let msg = NostrClientMessage::req(sub.clone(), vec![]);
1154        let replies = nostr_responses_for(&msg);
1155        assert_eq!(replies.len(), 1);
1156        match &replies[0] {
1157            NostrRelayMessage::EndOfStoredEvents(id) => assert_eq!(id.as_ref(), &sub),
1158            other => panic!("expected EOSE, got {:?}", other),
1159        }
1160    }
1161
1162    #[test]
1163    fn nostr_replies_for_event_ok() {
1164        let keys = Keys::generate();
1165        let event = EventBuilder::new(Kind::TextNote, "hello")
1166            .sign_with_keys(&keys)
1167            .unwrap();
1168        let msg = NostrClientMessage::event(event.clone());
1169        let replies = nostr_responses_for(&msg);
1170        assert_eq!(replies.len(), 1);
1171        match &replies[0] {
1172            NostrRelayMessage::Ok {
1173                event_id, status, ..
1174            } => {
1175                assert_eq!(event_id, &event.id);
1176                assert!(*status);
1177            }
1178            other => panic!("expected OK, got {:?}", other),
1179        }
1180    }
1181
1182    #[test]
1183    fn nostr_replies_for_invalid_event_is_not_ok() {
1184        let keys = Keys::generate();
1185        let mut event = EventBuilder::new(Kind::TextNote, "hello")
1186            .sign_with_keys(&keys)
1187            .unwrap();
1188        event.sig = Signature::from_slice(&[0u8; 64]).unwrap();
1189        let msg = NostrClientMessage::event(event);
1190        let replies = nostr_responses_for(&msg);
1191        assert_eq!(replies.len(), 1);
1192        match &replies[0] {
1193            NostrRelayMessage::Ok { status, .. } => assert!(!*status),
1194            other => panic!("expected OK=false, got {:?}", other),
1195        }
1196    }
1197
1198    async fn spawn_mock_upstream_relay(events: Vec<nostr::Event>) -> String {
1199        let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind relay");
1200        let addr = listener.local_addr().expect("relay addr");
1201        tokio::spawn(async move {
1202            let (stream, _) = listener.accept().await.expect("accept relay");
1203            let ws = accept_async(stream).await.expect("accept websocket");
1204            let (mut write, mut read) = ws.split();
1205
1206            while let Some(Ok(message)) = read.next().await {
1207                let TungsteniteMessage::Text(text) = message else {
1208                    continue;
1209                };
1210                let Ok(parsed) = NostrClientMessage::from_json(text.as_bytes()) else {
1211                    continue;
1212                };
1213                if let NostrClientMessage::Req {
1214                    subscription_id,
1215                    filters,
1216                } = parsed
1217                {
1218                    let subscription_id = subscription_id.into_owned();
1219                    let filters = filters
1220                        .into_iter()
1221                        .map(|filter| filter.into_owned())
1222                        .collect::<Vec<_>>();
1223                    for event in events.iter().filter(|event| {
1224                        filters
1225                            .iter()
1226                            .any(|filter| filter.match_event(event, Default::default()))
1227                    }) {
1228                        let _ = write
1229                            .send(TungsteniteMessage::Text(
1230                                NostrRelayMessage::event(subscription_id.clone(), event.clone())
1231                                    .as_json()
1232                                    .into(),
1233                            ))
1234                            .await;
1235                    }
1236                    let _ = write
1237                        .send(TungsteniteMessage::Text(
1238                            NostrRelayMessage::eose(subscription_id).as_json().into(),
1239                        ))
1240                        .await;
1241                }
1242            }
1243        });
1244        format!("ws://{}", addr)
1245    }
1246
1247    fn test_app_state(
1248        tmp: &TempDir,
1249        relay: Arc<NostrRelay>,
1250        relay_url: String,
1251    ) -> Result<AppState> {
1252        let store = Arc::new(crate::storage::HashtreeStore::with_options(
1253            tmp.path(),
1254            None,
1255            128 * 1024 * 1024,
1256        )?);
1257        Ok(AppState {
1258            store,
1259            auth: None,
1260            daemon_started_at: 1_700_000_000,
1261            peer_mode: crate::config::ServerMode::Normal,
1262            hash_get_enabled: true,
1263            http_webrtc_fetch: true,
1264            webrtc_peers: None,
1265            fips_transport: None,
1266            fetch_from_fips_peers: true,
1267            ws_relay: Arc::new(super::super::auth::WsRelayState::new()),
1268            max_upload_bytes: 5 * 1024 * 1024,
1269            public_writes: true,
1270            public_plaintext_reads: true,
1271            require_random_untrusted_ingest: false,
1272            optimistic_blossom_uploads: false,
1273            optimistic_upload_queue_bytes: 256 * 1024 * 1024,
1274            optimistic_upload_queue: Arc::new(tokio::sync::Semaphore::new(256 * 1024 * 1024)),
1275            allowed_pubkeys: HashSet::new(),
1276            upstream_blossom: Vec::new(),
1277            upstream_http_client: super::super::new_upstream_http_client(),
1278            upstream_blossom_miss_cache: Arc::new(std::sync::Mutex::new(
1279                super::super::auth::new_lookup_cache(),
1280            )),
1281            upstream_blossom_fetch_metrics: Arc::new(
1282                super::super::auth::UpstreamBlossomFetchMetrics::default(),
1283            ),
1284            blossom_upload_replicas: Vec::new(),
1285            blossom_upload_replica_queue_bytes: 256 * 1024 * 1024,
1286            blossom_upload_replica_queue: Arc::new(tokio::sync::Semaphore::new(256 * 1024 * 1024)),
1287            blossom_upload_replica_keys: None,
1288            blossom_upload_replica_scheduler: Arc::new(
1289                super::super::blossom::BlossomUploadReplicaScheduler::new(),
1290            ),
1291            social_graph: None,
1292            social_graph_store: None,
1293            social_graph_root: None,
1294            socialgraph_snapshot_public: false,
1295            nostr_relay: Some(relay),
1296            nostr_provider: None,
1297            nostr_relay_urls: vec![relay_url],
1298            tree_root_cache: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
1299            inflight_blob_fetches: Arc::new(tokio::sync::Mutex::new(
1300                std::collections::HashMap::new(),
1301            )),
1302            inflight_blob_reads: Arc::new(
1303                tokio::sync::Mutex::new(std::collections::HashMap::new()),
1304            ),
1305            blob_cache: Arc::new(crate::blob_cache::BlobCache::for_tests()),
1306            directory_listing_cache: Arc::new(std::sync::Mutex::new(
1307                super::super::auth::new_lookup_cache(),
1308            )),
1309            resolved_path_cache: Arc::new(std::sync::Mutex::new(
1310                super::super::auth::new_lookup_cache(),
1311            )),
1312            thumbnail_path_cache: Arc::new(std::sync::Mutex::new(
1313                super::super::auth::new_lookup_cache(),
1314            )),
1315            cid_size_cache: Arc::new(std::sync::Mutex::new(super::super::auth::new_lookup_cache())),
1316        })
1317    }
1318
1319    fn ws_integrity_test_state(tmp: &TempDir) -> Result<AppState> {
1320        let graph_store = {
1321            let _guard = crate::socialgraph::test_lock();
1322            crate::socialgraph::open_test_social_graph_store_with_mapsize(
1323                tmp.path(),
1324                Some(128 * 1024 * 1024),
1325            )?
1326        };
1327        let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store;
1328        let relay = Arc::new(NostrRelay::new(
1329            backend,
1330            tmp.path().to_path_buf(),
1331            HashSet::new(),
1332            None,
1333            NostrRelayConfig {
1334                spambox_db_max_bytes: 0,
1335                ..Default::default()
1336            },
1337        )?);
1338        test_app_state(tmp, relay, String::new())
1339    }
1340
1341    async fn seed_pending_json_blob_request(
1342        state: &AppState,
1343        peer_id: u64,
1344        origin_id: u64,
1345        request_id: u32,
1346        hash_hex: String,
1347    ) -> mpsc::UnboundedReceiver<Message> {
1348        let (tx, rx) = mpsc::unbounded_channel();
1349        state.ws_relay.clients.lock().await.insert(origin_id, tx);
1350        state.ws_relay.pending.lock().await.insert(
1351            (peer_id, request_id),
1352            PendingRequest {
1353                origin_id,
1354                hash: hash_hex,
1355                found: false,
1356                origin_protocol: WsProtocol::HashtreeJson,
1357            },
1358        );
1359        rx
1360    }
1361
1362    #[tokio::test]
1363    async fn json_found_response_waits_for_valid_binary_payload() -> Result<()> {
1364        let tmp = TempDir::new()?;
1365        let state = ws_integrity_test_state(&tmp)?;
1366        let expected_hash = hex_encode(sha256(b"expected"));
1367        let peer_id = 7;
1368        let origin_id = 11;
1369        let request_id = 42;
1370        let mut rx = seed_pending_json_blob_request(
1371            &state,
1372            peer_id,
1373            origin_id,
1374            request_id,
1375            expected_hash.clone(),
1376        )
1377        .await;
1378
1379        handle_response(peer_id, request_id, expected_hash, true, &state).await;
1380
1381        assert!(
1382            tokio::time::timeout(std::time::Duration::from_millis(50), rx.recv())
1383                .await
1384                .is_err()
1385        );
1386        Ok(())
1387    }
1388
1389    #[tokio::test]
1390    async fn legacy_binary_response_with_wrong_hash_is_not_forwarded() -> Result<()> {
1391        let tmp = TempDir::new()?;
1392        let state = ws_integrity_test_state(&tmp)?;
1393        let expected_hash = hex_encode(sha256(b"expected"));
1394        let peer_id = 7;
1395        let origin_id = 11;
1396        let request_id = 42;
1397        let mut rx =
1398            seed_pending_json_blob_request(&state, peer_id, origin_id, request_id, expected_hash)
1399                .await;
1400        let mut wire = request_id.to_le_bytes().to_vec();
1401        wire.extend_from_slice(b"wrong bytes");
1402
1403        handle_binary(peer_id, wire, &state).await;
1404
1405        assert!(
1406            tokio::time::timeout(std::time::Duration::from_millis(50), rx.recv())
1407                .await
1408                .is_err()
1409        );
1410        Ok(())
1411    }
1412
1413    #[tokio::test]
1414    async fn msgpack_response_with_wrong_hash_is_not_forwarded() -> Result<()> {
1415        let tmp = TempDir::new()?;
1416        let state = ws_integrity_test_state(&tmp)?;
1417        let expected_hash = sha256(b"expected");
1418        let peer_id = 7;
1419        let origin_id = 11;
1420        let request_id = 42;
1421        let mut rx = seed_pending_json_blob_request(
1422            &state,
1423            peer_id,
1424            origin_id,
1425            request_id,
1426            hex_encode(expected_hash),
1427        )
1428        .await;
1429
1430        handle_msgpack_response(
1431            peer_id,
1432            DataResponse {
1433                h: expected_hash.to_vec(),
1434                d: b"wrong bytes".to_vec(),
1435                i: None,
1436                n: None,
1437            },
1438            &state,
1439        )
1440        .await;
1441
1442        assert!(
1443            tokio::time::timeout(std::time::Duration::from_millis(50), rx.recv())
1444                .await
1445                .is_err()
1446        );
1447        Ok(())
1448    }
1449
1450    #[tokio::test]
1451    async fn upstream_proxy_forwards_events_and_caches_them() -> Result<()> {
1452        let tmp = TempDir::new()?;
1453        let graph_store = {
1454            let _guard = crate::socialgraph::test_lock();
1455            crate::socialgraph::open_test_social_graph_store_with_mapsize(
1456                tmp.path(),
1457                Some(128 * 1024 * 1024),
1458            )?
1459        };
1460        let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store.clone();
1461        let access = Arc::new(crate::socialgraph::SocialGraphAccessControl::new(
1462            Arc::clone(&backend),
1463            0,
1464            HashSet::new(),
1465        ));
1466
1467        let keys = Keys::generate();
1468        let relay = Arc::new(NostrRelay::new(
1469            Arc::clone(&backend),
1470            tmp.path().to_path_buf(),
1471            HashSet::from([keys.public_key().to_hex()]),
1472            Some(access),
1473            NostrRelayConfig {
1474                spambox_db_max_bytes: 0,
1475                ..Default::default()
1476            },
1477        )?);
1478
1479        let event = EventBuilder::new(Kind::from(30078_u16), "")
1480            .tags([
1481                nostr::Tag::parse(["d", "videos/Test"]).expect("d tag"),
1482                nostr::Tag::parse(["l", "hashtree"]).expect("label tag"),
1483            ])
1484            .sign_with_keys(&keys)?;
1485
1486        let relay_url = spawn_mock_upstream_relay(vec![event.clone()]).await;
1487        let filter = Filter::new()
1488            .authors(vec![event.pubkey])
1489            .kinds(vec![event.kind]);
1490        let state = test_app_state(&tmp, relay.clone(), relay_url)?;
1491        let client_id = 7_u64;
1492        let (tx, mut rx) = mpsc::unbounded_channel();
1493        state.ws_relay.clients.lock().await.insert(client_id, tx);
1494        let subscription_id = SubscriptionId::new("sub-1");
1495
1496        start_upstream_nostr_subscription(
1497            &state,
1498            client_id,
1499            subscription_id.clone(),
1500            vec![filter.clone()],
1501        )
1502        .await;
1503
1504        let forwarded = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
1505            .await?
1506            .expect("forwarded upstream event");
1507        let Message::Text(text) = forwarded else {
1508            panic!("expected text event");
1509        };
1510        match NostrRelayMessage::from_json(text.as_str())? {
1511            NostrRelayMessage::Event {
1512                subscription_id: sid,
1513                event: forwarded_event,
1514            } => {
1515                assert_eq!(sid.as_ref(), &subscription_id);
1516                assert_eq!(forwarded_event.id, event.id);
1517            }
1518            other => panic!("expected forwarded EVENT, got {:?}", other),
1519        }
1520
1521        let eose = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
1522            .await?
1523            .expect("forwarded upstream eose");
1524        let Message::Text(eose_text) = eose else {
1525            panic!("expected text eose");
1526        };
1527        match NostrRelayMessage::from_json(eose_text.as_str())? {
1528            NostrRelayMessage::EndOfStoredEvents(sid) => {
1529                assert_eq!(sid.as_ref(), &subscription_id);
1530            }
1531            other => panic!("expected forwarded EOSE, got {:?}", other),
1532        }
1533
1534        let events = relay.query_events(&filter, 10).await;
1535        assert_eq!(events.len(), 1);
1536        assert_eq!(events[0].id, event.id);
1537
1538        close_upstream_nostr_subscription(&state, client_id, &subscription_id).await;
1539        assert!(state
1540            .ws_relay
1541            .upstream_nostr_subscriptions
1542            .lock()
1543            .await
1544            .is_empty());
1545        Ok(())
1546    }
1547
1548    #[tokio::test]
1549    async fn req_waits_for_upstream_event_before_eose() -> Result<()> {
1550        let tmp = TempDir::new()?;
1551        let graph_store = {
1552            let _guard = crate::socialgraph::test_lock();
1553            crate::socialgraph::open_test_social_graph_store_with_mapsize(
1554                tmp.path(),
1555                Some(128 * 1024 * 1024),
1556            )?
1557        };
1558        let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store.clone();
1559        let access = Arc::new(crate::socialgraph::SocialGraphAccessControl::new(
1560            Arc::clone(&backend),
1561            0,
1562            HashSet::new(),
1563        ));
1564
1565        let keys = Keys::generate();
1566        let relay = Arc::new(NostrRelay::new(
1567            Arc::clone(&backend),
1568            tmp.path().to_path_buf(),
1569            HashSet::from([keys.public_key().to_hex()]),
1570            Some(access),
1571            NostrRelayConfig {
1572                spambox_db_max_bytes: 0,
1573                ..Default::default()
1574            },
1575        )?);
1576
1577        let event = EventBuilder::new(Kind::from(30078_u16), "")
1578            .tags([
1579                nostr::Tag::parse(["d", "videos/Test"]).expect("d tag"),
1580                nostr::Tag::parse(["l", "hashtree"]).expect("label tag"),
1581            ])
1582            .sign_with_keys(&keys)?;
1583
1584        let relay_url = spawn_mock_upstream_relay(vec![event.clone()]).await;
1585        let state = test_app_state(&tmp, relay.clone(), relay_url)?;
1586        let client_id = 11_u64;
1587        let (ws_tx, mut ws_rx) = mpsc::unbounded_channel();
1588        let (relay_tx, _relay_rx) = mpsc::unbounded_channel();
1589        state.ws_relay.clients.lock().await.insert(client_id, ws_tx);
1590        relay.register_client(client_id, relay_tx, None).await;
1591
1592        let request = NostrClientMessage::req(
1593            SubscriptionId::new("feed"),
1594            vec![Filter::new()
1595                .authors(vec![event.pubkey])
1596                .kinds(vec![event.kind])],
1597        )
1598        .as_json();
1599
1600        handle_message(client_id, Message::Text(request.into()), &state).await;
1601
1602        let first = tokio::time::timeout(std::time::Duration::from_secs(2), ws_rx.recv())
1603            .await?
1604            .expect("first forwarded message");
1605        let Message::Text(first_text) = first else {
1606            panic!("expected text event");
1607        };
1608        match NostrRelayMessage::from_json(first_text.as_str())? {
1609            NostrRelayMessage::Event {
1610                event: forwarded_event,
1611                ..
1612            } => {
1613                assert_eq!(forwarded_event.id, event.id);
1614            }
1615            other => panic!("expected upstream EVENT before EOSE, got {:?}", other),
1616        }
1617
1618        let second = tokio::time::timeout(std::time::Duration::from_secs(2), ws_rx.recv())
1619            .await?
1620            .expect("second forwarded message");
1621        let Message::Text(second_text) = second else {
1622            panic!("expected text eose");
1623        };
1624        match NostrRelayMessage::from_json(second_text.as_str())? {
1625            NostrRelayMessage::EndOfStoredEvents(sid) => {
1626                assert_eq!(sid.as_ref(), &SubscriptionId::new("feed"));
1627            }
1628            other => panic!("expected aggregated EOSE, got {:?}", other),
1629        }
1630
1631        Ok(())
1632    }
1633
1634    #[tokio::test]
1635    async fn websocket_publish_returns_ok_for_trusted_event() -> Result<()> {
1636        let tmp = TempDir::new()?;
1637        let graph_store = {
1638            let _guard = crate::socialgraph::test_lock();
1639            crate::socialgraph::open_test_social_graph_store_with_mapsize(
1640                tmp.path(),
1641                Some(128 * 1024 * 1024),
1642            )?
1643        };
1644        let author_keys = Keys::generate();
1645        let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store.clone();
1646        let access = Arc::new(crate::socialgraph::SocialGraphAccessControl::new(
1647            Arc::clone(&backend),
1648            0,
1649            HashSet::from([author_keys.public_key().to_hex()]),
1650        ));
1651        let relay = Arc::new(NostrRelay::new(
1652            Arc::clone(&backend),
1653            tmp.path().to_path_buf(),
1654            HashSet::from([author_keys.public_key().to_hex()]),
1655            Some(access),
1656            NostrRelayConfig {
1657                spambox_db_max_bytes: 0,
1658                ..Default::default()
1659            },
1660        )?);
1661
1662        let state = test_app_state(&tmp, relay.clone(), String::new())?;
1663        let listener = TcpListener::bind("127.0.0.1:0").await?;
1664        let addr = listener.local_addr()?;
1665        let client_pubkey = author_keys.public_key().to_hex();
1666        let app = axum::Router::new().route(
1667            "/ws",
1668            axum::routing::get({
1669                let state = state.clone();
1670                let client_pubkey = client_pubkey.clone();
1671                move |ws: WebSocketUpgrade| {
1672                    let state = state.clone();
1673                    let client_pubkey = client_pubkey.clone();
1674                    async move { ws_data_with_client_pubkey(state, ws, Some(client_pubkey)) }
1675                }
1676            }),
1677        );
1678        tokio::spawn(async move {
1679            let _ = axum::serve(listener, app).await;
1680        });
1681
1682        let (mut socket, _) = connect_async(format!("ws://{addr}/ws")).await?;
1683        let event = EventBuilder::new(Kind::TextNote, "websocket publish ack")
1684            .sign_with_keys(&author_keys)?;
1685        socket
1686            .send(TungsteniteMessage::Text(
1687                NostrClientMessage::event(event.clone()).as_json().into(),
1688            ))
1689            .await?;
1690
1691        let reply = tokio::time::timeout(std::time::Duration::from_secs(2), socket.next())
1692            .await?
1693            .ok_or_else(|| anyhow::anyhow!("websocket closed before publish ack"))??;
1694        let TungsteniteMessage::Text(text) = reply else {
1695            anyhow::bail!("expected text publish ack");
1696        };
1697
1698        match NostrRelayMessage::from_json(text.as_str())? {
1699            NostrRelayMessage::Ok {
1700                event_id, status, ..
1701            } => {
1702                assert_eq!(event_id, event.id);
1703                assert!(status);
1704            }
1705            other => anyhow::bail!("expected OK publish ack, got {:?}", other),
1706        }
1707
1708        let stored = relay
1709            .query_events(
1710                &Filter::new()
1711                    .authors(vec![event.pubkey])
1712                    .kinds(vec![event.kind]),
1713                10,
1714            )
1715            .await;
1716        assert!(stored.iter().any(|candidate| candidate.id == event.id));
1717        Ok(())
1718    }
1719
1720    #[tokio::test]
1721    async fn websocket_req_is_rate_limited_after_configured_quota() -> Result<()> {
1722        let tmp = TempDir::new()?;
1723        let graph_store = {
1724            let _guard = crate::socialgraph::test_lock();
1725            crate::socialgraph::open_test_social_graph_store_with_mapsize(
1726                tmp.path(),
1727                Some(128 * 1024 * 1024),
1728            )?
1729        };
1730        let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store.clone();
1731        let access = Arc::new(crate::socialgraph::SocialGraphAccessControl::new(
1732            Arc::clone(&backend),
1733            0,
1734            HashSet::new(),
1735        ));
1736        let relay = Arc::new(NostrRelay::new(
1737            Arc::clone(&backend),
1738            tmp.path().to_path_buf(),
1739            HashSet::new(),
1740            Some(access),
1741            NostrRelayConfig {
1742                spambox_db_max_bytes: 0,
1743                spambox_max_reqs_per_min: 1,
1744                ..Default::default()
1745            },
1746        )?);
1747
1748        let state = test_app_state(&tmp, relay, String::new())?;
1749        let listener = TcpListener::bind("127.0.0.1:0").await?;
1750        let addr = listener.local_addr()?;
1751        let app = axum::Router::new().route(
1752            "/ws",
1753            axum::routing::get({
1754                let state = state.clone();
1755                move |ws: WebSocketUpgrade| {
1756                    let state = state.clone();
1757                    async move { ws_data_with_client_pubkey(state, ws, None) }
1758                }
1759            }),
1760        );
1761        tokio::spawn(async move {
1762            let _ = axum::serve(listener, app).await;
1763        });
1764
1765        let (mut socket, _) = connect_async(format!("ws://{addr}/ws")).await?;
1766        socket
1767            .send(TungsteniteMessage::Text(
1768                NostrClientMessage::req(SubscriptionId::new("sub-1"), vec![Filter::new()])
1769                    .as_json()
1770                    .into(),
1771            ))
1772            .await?;
1773
1774        let first = tokio::time::timeout(std::time::Duration::from_secs(2), socket.next())
1775            .await?
1776            .ok_or_else(|| anyhow::anyhow!("websocket closed before first relay reply"))??;
1777        let TungsteniteMessage::Text(first_text) = first else {
1778            anyhow::bail!("expected text EOSE reply");
1779        };
1780        match NostrRelayMessage::from_json(first_text.as_str())? {
1781            NostrRelayMessage::EndOfStoredEvents(subscription_id) => {
1782                assert_eq!(subscription_id.as_ref(), &SubscriptionId::new("sub-1"));
1783            }
1784            other => anyhow::bail!("expected EOSE for first request, got {:?}", other),
1785        }
1786
1787        socket
1788            .send(TungsteniteMessage::Text(
1789                NostrClientMessage::req(SubscriptionId::new("sub-2"), vec![Filter::new()])
1790                    .as_json()
1791                    .into(),
1792            ))
1793            .await?;
1794
1795        let second = tokio::time::timeout(std::time::Duration::from_secs(2), socket.next())
1796            .await?
1797            .ok_or_else(|| anyhow::anyhow!("websocket closed before rate-limit reply"))??;
1798        let TungsteniteMessage::Text(second_text) = second else {
1799            anyhow::bail!("expected text CLOSED reply");
1800        };
1801        let second_value: serde_json::Value = serde_json::from_str(second_text.as_str())?;
1802        assert_eq!(
1803            second_value,
1804            serde_json::json!(["CLOSED", "sub-2", "rate limited"])
1805        );
1806
1807        Ok(())
1808    }
1809
1810    #[tokio::test]
1811    async fn websocket_publish_is_rate_limited_for_untrusted_spambox_events() -> Result<()> {
1812        let tmp = TempDir::new()?;
1813        let graph_store = {
1814            let _guard = crate::socialgraph::test_lock();
1815            crate::socialgraph::open_test_social_graph_store_with_mapsize(
1816                tmp.path(),
1817                Some(128 * 1024 * 1024),
1818            )?
1819        };
1820        let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store.clone();
1821        let access = Arc::new(crate::socialgraph::SocialGraphAccessControl::new(
1822            Arc::clone(&backend),
1823            0,
1824            HashSet::new(),
1825        ));
1826        let relay = Arc::new(NostrRelay::new(
1827            Arc::clone(&backend),
1828            tmp.path().to_path_buf(),
1829            HashSet::new(),
1830            Some(access),
1831            NostrRelayConfig {
1832                spambox_db_max_bytes: 0,
1833                spambox_max_events_per_min: 1,
1834                ..Default::default()
1835            },
1836        )?);
1837
1838        let state = test_app_state(&tmp, relay, String::new())?;
1839        let listener = TcpListener::bind("127.0.0.1:0").await?;
1840        let addr = listener.local_addr()?;
1841        let app = axum::Router::new().route(
1842            "/ws",
1843            axum::routing::get({
1844                let state = state.clone();
1845                move |ws: WebSocketUpgrade| {
1846                    let state = state.clone();
1847                    async move { ws_data_with_client_pubkey(state, ws, None) }
1848                }
1849            }),
1850        );
1851        tokio::spawn(async move {
1852            let _ = axum::serve(listener, app).await;
1853        });
1854
1855        let (mut socket, _) = connect_async(format!("ws://{addr}/ws")).await?;
1856        let author_keys = Keys::generate();
1857        let event_a =
1858            EventBuilder::new(Kind::TextNote, "spambox-a").sign_with_keys(&author_keys)?;
1859        let event_b =
1860            EventBuilder::new(Kind::TextNote, "spambox-b").sign_with_keys(&author_keys)?;
1861
1862        socket
1863            .send(TungsteniteMessage::Text(
1864                NostrClientMessage::event(event_a.clone()).as_json().into(),
1865            ))
1866            .await?;
1867
1868        let first = tokio::time::timeout(std::time::Duration::from_secs(2), socket.next())
1869            .await?
1870            .ok_or_else(|| anyhow::anyhow!("websocket closed before first publish ack"))??;
1871        let TungsteniteMessage::Text(first_text) = first else {
1872            anyhow::bail!("expected text publish ack");
1873        };
1874        match NostrRelayMessage::from_json(first_text.as_str())? {
1875            NostrRelayMessage::Ok {
1876                event_id,
1877                status,
1878                message,
1879            } => {
1880                assert_eq!(event_id, event_a.id);
1881                assert!(status);
1882                assert_eq!(message, "spambox");
1883            }
1884            other => anyhow::bail!("expected OK publish ack, got {:?}", other),
1885        }
1886
1887        socket
1888            .send(TungsteniteMessage::Text(
1889                NostrClientMessage::event(event_b.clone()).as_json().into(),
1890            ))
1891            .await?;
1892
1893        let second = tokio::time::timeout(std::time::Duration::from_secs(2), socket.next())
1894            .await?
1895            .ok_or_else(|| anyhow::anyhow!("websocket closed before rate-limit publish ack"))??;
1896        let TungsteniteMessage::Text(second_text) = second else {
1897            anyhow::bail!("expected text publish ack");
1898        };
1899        match NostrRelayMessage::from_json(second_text.as_str())? {
1900            NostrRelayMessage::Ok {
1901                event_id,
1902                status,
1903                message,
1904            } => {
1905                assert_eq!(event_id, event_b.id);
1906                assert!(!status);
1907                assert_eq!(message, "rate limited");
1908            }
1909            other => anyhow::bail!("expected OK=false publish ack, got {:?}", other),
1910        }
1911
1912        Ok(())
1913    }
1914}