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 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 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 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 fips_blob_resolver: None,
1260 fetch_from_fips_peers: true,
1261 ws_relay: Arc::new(super::super::auth::WsRelayState::new()),
1262 max_upload_bytes: 5 * 1024 * 1024,
1263 public_writes: true,
1264 public_plaintext_reads: true,
1265 require_random_untrusted_ingest: false,
1266 optimistic_blossom_uploads: false,
1267 optimistic_upload_queue_bytes: 256 * 1024 * 1024,
1268 optimistic_upload_queue: Arc::new(tokio::sync::Semaphore::new(256 * 1024 * 1024)),
1269 allowed_pubkeys: HashSet::new(),
1270 upstream_blossom: Vec::new(),
1271 upstream_http_client: super::super::new_upstream_http_client(),
1272 upstream_blossom_miss_cache: Arc::new(std::sync::Mutex::new(
1273 super::super::auth::new_lookup_cache(),
1274 )),
1275 upstream_blossom_fetch_metrics: Arc::new(
1276 super::super::auth::UpstreamBlossomFetchMetrics::default(),
1277 ),
1278 blossom_upload_replicas: Vec::new(),
1279 blossom_upload_replica_queue_bytes: 256 * 1024 * 1024,
1280 blossom_upload_replica_queue: Arc::new(tokio::sync::Semaphore::new(256 * 1024 * 1024)),
1281 blossom_upload_replica_keys: None,
1282 blossom_upload_replica_scheduler: Arc::new(
1283 super::super::blossom::BlossomUploadReplicaScheduler::new(),
1284 ),
1285 social_graph: None,
1286 social_graph_store: None,
1287 social_graph_root: None,
1288 socialgraph_snapshot_public: false,
1289 nostr_relay: Some(relay),
1290 nostr_provider: None,
1291 nostr_relay_urls: vec![relay_url],
1292 tree_root_cache: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
1293 inflight_blob_fetches: Arc::new(tokio::sync::Mutex::new(
1294 std::collections::HashMap::new(),
1295 )),
1296 inflight_blob_reads: Arc::new(
1297 tokio::sync::Mutex::new(std::collections::HashMap::new()),
1298 ),
1299 blob_cache: Arc::new(crate::blob_cache::BlobCache::for_tests()),
1300 directory_listing_cache: Arc::new(std::sync::Mutex::new(
1301 super::super::auth::new_lookup_cache(),
1302 )),
1303 resolved_path_cache: Arc::new(std::sync::Mutex::new(
1304 super::super::auth::new_lookup_cache(),
1305 )),
1306 thumbnail_path_cache: Arc::new(std::sync::Mutex::new(
1307 super::super::auth::new_lookup_cache(),
1308 )),
1309 cid_size_cache: Arc::new(std::sync::Mutex::new(super::super::auth::new_lookup_cache())),
1310 })
1311 }
1312
1313 async fn ws_integrity_test_state(tmp: &TempDir) -> Result<AppState> {
1314 let graph_store = {
1315 let _guard = crate::socialgraph::test_lock().await;
1316 crate::socialgraph::open_test_social_graph_store_with_mapsize(
1317 tmp.path(),
1318 Some(128 * 1024 * 1024),
1319 )?
1320 };
1321 let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store;
1322 let relay = Arc::new(NostrRelay::new(
1323 backend,
1324 tmp.path().to_path_buf(),
1325 HashSet::new(),
1326 None,
1327 NostrRelayConfig {
1328 spambox_db_max_bytes: 0,
1329 ..Default::default()
1330 },
1331 )?);
1332 test_app_state(tmp, relay, String::new())
1333 }
1334
1335 async fn seed_pending_json_blob_request(
1336 state: &AppState,
1337 peer_id: u64,
1338 origin_id: u64,
1339 request_id: u32,
1340 hash_hex: String,
1341 ) -> mpsc::UnboundedReceiver<Message> {
1342 let (tx, rx) = mpsc::unbounded_channel();
1343 state.ws_relay.clients.lock().await.insert(origin_id, tx);
1344 state.ws_relay.pending.lock().await.insert(
1345 (peer_id, request_id),
1346 PendingRequest {
1347 origin_id,
1348 hash: hash_hex,
1349 found: false,
1350 origin_protocol: WsProtocol::HashtreeJson,
1351 },
1352 );
1353 rx
1354 }
1355
1356 #[tokio::test]
1357 async fn json_found_response_waits_for_valid_binary_payload() -> Result<()> {
1358 let tmp = TempDir::new()?;
1359 let state = ws_integrity_test_state(&tmp).await?;
1360 let expected_hash = hex_encode(sha256(b"expected"));
1361 let peer_id = 7;
1362 let origin_id = 11;
1363 let request_id = 42;
1364 let mut rx = seed_pending_json_blob_request(
1365 &state,
1366 peer_id,
1367 origin_id,
1368 request_id,
1369 expected_hash.clone(),
1370 )
1371 .await;
1372
1373 handle_response(peer_id, request_id, expected_hash, true, &state).await;
1374
1375 assert!(
1376 tokio::time::timeout(std::time::Duration::from_millis(50), rx.recv())
1377 .await
1378 .is_err()
1379 );
1380 Ok(())
1381 }
1382
1383 #[tokio::test]
1384 async fn legacy_binary_response_with_wrong_hash_is_not_forwarded() -> Result<()> {
1385 let tmp = TempDir::new()?;
1386 let state = ws_integrity_test_state(&tmp).await?;
1387 let expected_hash = hex_encode(sha256(b"expected"));
1388 let peer_id = 7;
1389 let origin_id = 11;
1390 let request_id = 42;
1391 let mut rx =
1392 seed_pending_json_blob_request(&state, peer_id, origin_id, request_id, expected_hash)
1393 .await;
1394 let mut wire = request_id.to_le_bytes().to_vec();
1395 wire.extend_from_slice(b"wrong bytes");
1396
1397 handle_binary(peer_id, wire, &state).await;
1398
1399 assert!(
1400 tokio::time::timeout(std::time::Duration::from_millis(50), rx.recv())
1401 .await
1402 .is_err()
1403 );
1404 Ok(())
1405 }
1406
1407 #[tokio::test]
1408 async fn msgpack_response_with_wrong_hash_is_not_forwarded() -> Result<()> {
1409 let tmp = TempDir::new()?;
1410 let state = ws_integrity_test_state(&tmp).await?;
1411 let expected_hash = sha256(b"expected");
1412 let peer_id = 7;
1413 let origin_id = 11;
1414 let request_id = 42;
1415 let mut rx = seed_pending_json_blob_request(
1416 &state,
1417 peer_id,
1418 origin_id,
1419 request_id,
1420 hex_encode(expected_hash),
1421 )
1422 .await;
1423
1424 handle_msgpack_response(
1425 peer_id,
1426 DataResponse {
1427 h: expected_hash.to_vec(),
1428 d: b"wrong bytes".to_vec(),
1429 i: None,
1430 n: None,
1431 },
1432 &state,
1433 )
1434 .await;
1435
1436 assert!(
1437 tokio::time::timeout(std::time::Duration::from_millis(50), rx.recv())
1438 .await
1439 .is_err()
1440 );
1441 Ok(())
1442 }
1443
1444 #[tokio::test]
1445 async fn upstream_proxy_forwards_events_and_caches_them() -> Result<()> {
1446 let tmp = TempDir::new()?;
1447 let graph_store = {
1448 let _guard = crate::socialgraph::test_lock().await;
1449 crate::socialgraph::open_test_social_graph_store_with_mapsize(
1450 tmp.path(),
1451 Some(128 * 1024 * 1024),
1452 )?
1453 };
1454 let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store.clone();
1455 let access = Arc::new(crate::socialgraph::SocialGraphAccessControl::new(
1456 Arc::clone(&backend),
1457 0,
1458 HashSet::new(),
1459 ));
1460
1461 let keys = Keys::generate();
1462 let relay = Arc::new(NostrRelay::new(
1463 Arc::clone(&backend),
1464 tmp.path().to_path_buf(),
1465 HashSet::from([keys.public_key().to_hex()]),
1466 Some(access),
1467 NostrRelayConfig {
1468 spambox_db_max_bytes: 0,
1469 ..Default::default()
1470 },
1471 )?);
1472
1473 let event = EventBuilder::new(Kind::from(30078_u16), "")
1474 .tags([
1475 nostr::Tag::parse(["d", "videos/Test"]).expect("d tag"),
1476 nostr::Tag::parse(["l", "hashtree"]).expect("label tag"),
1477 ])
1478 .sign_with_keys(&keys)?;
1479
1480 let relay_url = spawn_mock_upstream_relay(vec![event.clone()]).await;
1481 let filter = Filter::new()
1482 .authors(vec![event.pubkey])
1483 .kinds(vec![event.kind]);
1484 let state = test_app_state(&tmp, relay.clone(), relay_url)?;
1485 let client_id = 7_u64;
1486 let (tx, mut rx) = mpsc::unbounded_channel();
1487 state.ws_relay.clients.lock().await.insert(client_id, tx);
1488 let subscription_id = SubscriptionId::new("sub-1");
1489
1490 start_upstream_nostr_subscription(
1491 &state,
1492 client_id,
1493 subscription_id.clone(),
1494 vec![filter.clone()],
1495 )
1496 .await;
1497
1498 let forwarded = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
1499 .await?
1500 .expect("forwarded upstream event");
1501 let Message::Text(text) = forwarded else {
1502 panic!("expected text event");
1503 };
1504 match NostrRelayMessage::from_json(text.as_str())? {
1505 NostrRelayMessage::Event {
1506 subscription_id: sid,
1507 event: forwarded_event,
1508 } => {
1509 assert_eq!(sid.as_ref(), &subscription_id);
1510 assert_eq!(forwarded_event.id, event.id);
1511 }
1512 other => panic!("expected forwarded EVENT, got {:?}", other),
1513 }
1514
1515 let eose = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
1516 .await?
1517 .expect("forwarded upstream eose");
1518 let Message::Text(eose_text) = eose else {
1519 panic!("expected text eose");
1520 };
1521 match NostrRelayMessage::from_json(eose_text.as_str())? {
1522 NostrRelayMessage::EndOfStoredEvents(sid) => {
1523 assert_eq!(sid.as_ref(), &subscription_id);
1524 }
1525 other => panic!("expected forwarded EOSE, got {:?}", other),
1526 }
1527
1528 let events = relay.query_events(&filter, 10).await;
1529 assert_eq!(events.len(), 1);
1530 assert_eq!(events[0].id, event.id);
1531
1532 close_upstream_nostr_subscription(&state, client_id, &subscription_id).await;
1533 assert!(state
1534 .ws_relay
1535 .upstream_nostr_subscriptions
1536 .lock()
1537 .await
1538 .is_empty());
1539 Ok(())
1540 }
1541
1542 #[tokio::test]
1543 async fn req_waits_for_upstream_event_before_eose() -> Result<()> {
1544 let tmp = TempDir::new()?;
1545 let graph_store = {
1546 let _guard = crate::socialgraph::test_lock().await;
1547 crate::socialgraph::open_test_social_graph_store_with_mapsize(
1548 tmp.path(),
1549 Some(128 * 1024 * 1024),
1550 )?
1551 };
1552 let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store.clone();
1553 let access = Arc::new(crate::socialgraph::SocialGraphAccessControl::new(
1554 Arc::clone(&backend),
1555 0,
1556 HashSet::new(),
1557 ));
1558
1559 let keys = Keys::generate();
1560 let relay = Arc::new(NostrRelay::new(
1561 Arc::clone(&backend),
1562 tmp.path().to_path_buf(),
1563 HashSet::from([keys.public_key().to_hex()]),
1564 Some(access),
1565 NostrRelayConfig {
1566 spambox_db_max_bytes: 0,
1567 ..Default::default()
1568 },
1569 )?);
1570
1571 let event = EventBuilder::new(Kind::from(30078_u16), "")
1572 .tags([
1573 nostr::Tag::parse(["d", "videos/Test"]).expect("d tag"),
1574 nostr::Tag::parse(["l", "hashtree"]).expect("label tag"),
1575 ])
1576 .sign_with_keys(&keys)?;
1577
1578 let relay_url = spawn_mock_upstream_relay(vec![event.clone()]).await;
1579 let state = test_app_state(&tmp, relay.clone(), relay_url)?;
1580 let client_id = 11_u64;
1581 let (ws_tx, mut ws_rx) = mpsc::unbounded_channel();
1582 let (relay_tx, _relay_rx) = mpsc::unbounded_channel();
1583 state.ws_relay.clients.lock().await.insert(client_id, ws_tx);
1584 relay.register_client(client_id, relay_tx, None).await;
1585
1586 let request = NostrClientMessage::req(
1587 SubscriptionId::new("feed"),
1588 vec![Filter::new()
1589 .authors(vec![event.pubkey])
1590 .kinds(vec![event.kind])],
1591 )
1592 .as_json();
1593
1594 handle_message(client_id, Message::Text(request), &state).await;
1595
1596 let first = tokio::time::timeout(std::time::Duration::from_secs(2), ws_rx.recv())
1597 .await?
1598 .expect("first forwarded message");
1599 let Message::Text(first_text) = first else {
1600 panic!("expected text event");
1601 };
1602 match NostrRelayMessage::from_json(first_text.as_str())? {
1603 NostrRelayMessage::Event {
1604 event: forwarded_event,
1605 ..
1606 } => {
1607 assert_eq!(forwarded_event.id, event.id);
1608 }
1609 other => panic!("expected upstream EVENT before EOSE, got {:?}", other),
1610 }
1611
1612 let second = tokio::time::timeout(std::time::Duration::from_secs(2), ws_rx.recv())
1613 .await?
1614 .expect("second forwarded message");
1615 let Message::Text(second_text) = second else {
1616 panic!("expected text eose");
1617 };
1618 match NostrRelayMessage::from_json(second_text.as_str())? {
1619 NostrRelayMessage::EndOfStoredEvents(sid) => {
1620 assert_eq!(sid.as_ref(), &SubscriptionId::new("feed"));
1621 }
1622 other => panic!("expected aggregated EOSE, got {:?}", other),
1623 }
1624
1625 Ok(())
1626 }
1627
1628 #[tokio::test]
1629 async fn websocket_publish_returns_ok_for_trusted_event() -> Result<()> {
1630 let tmp = TempDir::new()?;
1631 let graph_store = {
1632 let _guard = crate::socialgraph::test_lock().await;
1633 crate::socialgraph::open_test_social_graph_store_with_mapsize(
1634 tmp.path(),
1635 Some(128 * 1024 * 1024),
1636 )?
1637 };
1638 let author_keys = Keys::generate();
1639 let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store.clone();
1640 let access = Arc::new(crate::socialgraph::SocialGraphAccessControl::new(
1641 Arc::clone(&backend),
1642 0,
1643 HashSet::from([author_keys.public_key().to_hex()]),
1644 ));
1645 let relay = Arc::new(NostrRelay::new(
1646 Arc::clone(&backend),
1647 tmp.path().to_path_buf(),
1648 HashSet::from([author_keys.public_key().to_hex()]),
1649 Some(access),
1650 NostrRelayConfig {
1651 spambox_db_max_bytes: 0,
1652 ..Default::default()
1653 },
1654 )?);
1655
1656 let state = test_app_state(&tmp, relay.clone(), String::new())?;
1657 let listener = TcpListener::bind("127.0.0.1:0").await?;
1658 let addr = listener.local_addr()?;
1659 let client_pubkey = author_keys.public_key().to_hex();
1660 let app = axum::Router::new().route(
1661 "/ws",
1662 axum::routing::get({
1663 let state = state.clone();
1664 let client_pubkey = client_pubkey.clone();
1665 move |ws: WebSocketUpgrade| {
1666 let state = state.clone();
1667 let client_pubkey = client_pubkey.clone();
1668 async move { ws_data_with_client_pubkey(state, ws, Some(client_pubkey)) }
1669 }
1670 }),
1671 );
1672 tokio::spawn(async move {
1673 let _ = axum::serve(listener, app).await;
1674 });
1675
1676 let (mut socket, _) = connect_async(format!("ws://{addr}/ws")).await?;
1677 let event = EventBuilder::new(Kind::TextNote, "websocket publish ack")
1678 .sign_with_keys(&author_keys)?;
1679 socket
1680 .send(TungsteniteMessage::Text(
1681 NostrClientMessage::event(event.clone()).as_json(),
1682 ))
1683 .await?;
1684
1685 let reply = tokio::time::timeout(std::time::Duration::from_secs(2), socket.next())
1686 .await?
1687 .ok_or_else(|| anyhow::anyhow!("websocket closed before publish ack"))??;
1688 let TungsteniteMessage::Text(text) = reply else {
1689 anyhow::bail!("expected text publish ack");
1690 };
1691
1692 match NostrRelayMessage::from_json(text.as_str())? {
1693 NostrRelayMessage::Ok {
1694 event_id, status, ..
1695 } => {
1696 assert_eq!(event_id, event.id);
1697 assert!(status);
1698 }
1699 other => anyhow::bail!("expected OK publish ack, got {:?}", other),
1700 }
1701
1702 let stored = relay
1703 .query_events(
1704 &Filter::new()
1705 .authors(vec![event.pubkey])
1706 .kinds(vec![event.kind]),
1707 10,
1708 )
1709 .await;
1710 assert!(stored.iter().any(|candidate| candidate.id == event.id));
1711 Ok(())
1712 }
1713
1714 #[tokio::test]
1715 async fn websocket_req_is_rate_limited_after_configured_quota() -> Result<()> {
1716 let tmp = TempDir::new()?;
1717 let graph_store = {
1718 let _guard = crate::socialgraph::test_lock().await;
1719 crate::socialgraph::open_test_social_graph_store_with_mapsize(
1720 tmp.path(),
1721 Some(128 * 1024 * 1024),
1722 )?
1723 };
1724 let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store.clone();
1725 let access = Arc::new(crate::socialgraph::SocialGraphAccessControl::new(
1726 Arc::clone(&backend),
1727 0,
1728 HashSet::new(),
1729 ));
1730 let relay = Arc::new(NostrRelay::new(
1731 Arc::clone(&backend),
1732 tmp.path().to_path_buf(),
1733 HashSet::new(),
1734 Some(access),
1735 NostrRelayConfig {
1736 spambox_db_max_bytes: 0,
1737 spambox_max_reqs_per_min: 1,
1738 ..Default::default()
1739 },
1740 )?);
1741
1742 let state = test_app_state(&tmp, relay, String::new())?;
1743 let listener = TcpListener::bind("127.0.0.1:0").await?;
1744 let addr = listener.local_addr()?;
1745 let app = axum::Router::new().route(
1746 "/ws",
1747 axum::routing::get({
1748 let state = state.clone();
1749 move |ws: WebSocketUpgrade| {
1750 let state = state.clone();
1751 async move { ws_data_with_client_pubkey(state, ws, None) }
1752 }
1753 }),
1754 );
1755 tokio::spawn(async move {
1756 let _ = axum::serve(listener, app).await;
1757 });
1758
1759 let (mut socket, _) = connect_async(format!("ws://{addr}/ws")).await?;
1760 socket
1761 .send(TungsteniteMessage::Text(
1762 NostrClientMessage::req(SubscriptionId::new("sub-1"), vec![Filter::new()])
1763 .as_json(),
1764 ))
1765 .await?;
1766
1767 let first = tokio::time::timeout(std::time::Duration::from_secs(2), socket.next())
1768 .await?
1769 .ok_or_else(|| anyhow::anyhow!("websocket closed before first relay reply"))??;
1770 let TungsteniteMessage::Text(first_text) = first else {
1771 anyhow::bail!("expected text EOSE reply");
1772 };
1773 match NostrRelayMessage::from_json(first_text.as_str())? {
1774 NostrRelayMessage::EndOfStoredEvents(subscription_id) => {
1775 assert_eq!(subscription_id.as_ref(), &SubscriptionId::new("sub-1"));
1776 }
1777 other => anyhow::bail!("expected EOSE for first request, got {:?}", other),
1778 }
1779
1780 socket
1781 .send(TungsteniteMessage::Text(
1782 NostrClientMessage::req(SubscriptionId::new("sub-2"), vec![Filter::new()])
1783 .as_json(),
1784 ))
1785 .await?;
1786
1787 let second = tokio::time::timeout(std::time::Duration::from_secs(2), socket.next())
1788 .await?
1789 .ok_or_else(|| anyhow::anyhow!("websocket closed before rate-limit reply"))??;
1790 let TungsteniteMessage::Text(second_text) = second else {
1791 anyhow::bail!("expected text CLOSED reply");
1792 };
1793 let second_value: serde_json::Value = serde_json::from_str(second_text.as_str())?;
1794 assert_eq!(
1795 second_value,
1796 serde_json::json!(["CLOSED", "sub-2", "rate limited"])
1797 );
1798
1799 Ok(())
1800 }
1801
1802 #[tokio::test]
1803 async fn websocket_publish_is_rate_limited_for_untrusted_spambox_events() -> Result<()> {
1804 let tmp = TempDir::new()?;
1805 let graph_store = {
1806 let _guard = crate::socialgraph::test_lock().await;
1807 crate::socialgraph::open_test_social_graph_store_with_mapsize(
1808 tmp.path(),
1809 Some(128 * 1024 * 1024),
1810 )?
1811 };
1812 let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store.clone();
1813 let access = Arc::new(crate::socialgraph::SocialGraphAccessControl::new(
1814 Arc::clone(&backend),
1815 0,
1816 HashSet::new(),
1817 ));
1818 let relay = Arc::new(NostrRelay::new(
1819 Arc::clone(&backend),
1820 tmp.path().to_path_buf(),
1821 HashSet::new(),
1822 Some(access),
1823 NostrRelayConfig {
1824 spambox_db_max_bytes: 0,
1825 spambox_max_events_per_min: 1,
1826 ..Default::default()
1827 },
1828 )?);
1829
1830 let state = test_app_state(&tmp, relay, String::new())?;
1831 let listener = TcpListener::bind("127.0.0.1:0").await?;
1832 let addr = listener.local_addr()?;
1833 let app = axum::Router::new().route(
1834 "/ws",
1835 axum::routing::get({
1836 let state = state.clone();
1837 move |ws: WebSocketUpgrade| {
1838 let state = state.clone();
1839 async move { ws_data_with_client_pubkey(state, ws, None) }
1840 }
1841 }),
1842 );
1843 tokio::spawn(async move {
1844 let _ = axum::serve(listener, app).await;
1845 });
1846
1847 let (mut socket, _) = connect_async(format!("ws://{addr}/ws")).await?;
1848 let author_keys = Keys::generate();
1849 let event_a =
1850 EventBuilder::new(Kind::TextNote, "spambox-a").sign_with_keys(&author_keys)?;
1851 let event_b =
1852 EventBuilder::new(Kind::TextNote, "spambox-b").sign_with_keys(&author_keys)?;
1853
1854 socket
1855 .send(TungsteniteMessage::Text(
1856 NostrClientMessage::event(event_a.clone()).as_json(),
1857 ))
1858 .await?;
1859
1860 let first = tokio::time::timeout(std::time::Duration::from_secs(2), socket.next())
1861 .await?
1862 .ok_or_else(|| anyhow::anyhow!("websocket closed before first publish ack"))??;
1863 let TungsteniteMessage::Text(first_text) = first else {
1864 anyhow::bail!("expected text publish ack");
1865 };
1866 match NostrRelayMessage::from_json(first_text.as_str())? {
1867 NostrRelayMessage::Ok {
1868 event_id,
1869 status,
1870 message,
1871 } => {
1872 assert_eq!(event_id, event_a.id);
1873 assert!(status);
1874 assert_eq!(message, "spambox");
1875 }
1876 other => anyhow::bail!("expected OK publish ack, got {:?}", other),
1877 }
1878
1879 socket
1880 .send(TungsteniteMessage::Text(
1881 NostrClientMessage::event(event_b.clone()).as_json(),
1882 ))
1883 .await?;
1884
1885 let second = tokio::time::timeout(std::time::Duration::from_secs(2), socket.next())
1886 .await?
1887 .ok_or_else(|| anyhow::anyhow!("websocket closed before rate-limit publish ack"))??;
1888 let TungsteniteMessage::Text(second_text) = second else {
1889 anyhow::bail!("expected text publish ack");
1890 };
1891 match NostrRelayMessage::from_json(second_text.as_str())? {
1892 NostrRelayMessage::Ok {
1893 event_id,
1894 status,
1895 message,
1896 } => {
1897 assert_eq!(event_id, event_b.id);
1898 assert!(!status);
1899 assert_eq!(message, "rate limited");
1900 }
1901 other => anyhow::bail!("expected OK=false publish ack, got {:?}", other),
1902 }
1903
1904 Ok(())
1905 }
1906}