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