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