radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

connect.rs (17175B)


      1 use std::time::Duration;
      2 
      3 use anyhow::Result;
      4 use jsonrpsee::server::RpcModule;
      5 use serde::{Deserialize, Serialize};
      6 use tokio::sync::broadcast;
      7 use tokio::time::sleep;
      8 use uuid::Uuid;
      9 
     10 use crate::core::nip46::session::{
     11     Nip46Session, Nip46SessionAuthority, filter_perms, session_expires_at,
     12 };
     13 use crate::transport::jsonrpc::nip46::connection::{
     14     Nip46ConnectInfo, Nip46ConnectMode, parse_connect_url,
     15 };
     16 use crate::transport::jsonrpc::params::DEFAULT_TIMEOUT_SECS;
     17 use crate::transport::jsonrpc::{MethodRegistry, RpcContext, RpcError};
     18 use nostr::JsonUtil;
     19 use nostr::nips::{nip44, nip46::NostrConnectMessage, nip46::NostrConnectRequest};
     20 use radroots_nostr::prelude::{
     21     RadrootsNostrClient, RadrootsNostrEventBuilder, RadrootsNostrFilter, RadrootsNostrKeys,
     22     RadrootsNostrKind, RadrootsNostrPublicKey, RadrootsNostrRelayPoolNotification,
     23     RadrootsNostrSecretKey, RadrootsNostrSubscriptionId, RadrootsNostrTimestamp,
     24     radroots_nostr_filter_tag, radroots_nostr_parse_pubkey,
     25 };
     26 
     27 #[derive(Debug, Deserialize)]
     28 struct Nip46ConnectParams {
     29     url: String,
     30     client_secret_key: Option<String>,
     31     #[serde(default)]
     32     signer_authority: Option<Nip46SessionAuthority>,
     33 }
     34 
     35 #[derive(Clone, Debug, Serialize)]
     36 struct Nip46ConnectResponse {
     37     session_id: String,
     38     mode: Nip46ConnectMode,
     39     remote_signer_pubkey: String,
     40     client_pubkey: String,
     41     relays: Vec<String>,
     42 }
     43 
     44 pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
     45     registry.track("nip46.connect");
     46     m.register_async_method("nip46.connect", |params, ctx, _| async move {
     47         let Nip46ConnectParams {
     48             url,
     49             client_secret_key,
     50             signer_authority,
     51         } = params
     52             .parse()
     53             .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
     54         let response = connect_nip46(
     55             ctx.as_ref().clone(),
     56             url,
     57             client_secret_key,
     58             signer_authority,
     59         )
     60         .await?;
     61         Ok::<Nip46ConnectResponse, RpcError>(response)
     62     })?;
     63     Ok(())
     64 }
     65 
     66 async fn connect_nip46(
     67     ctx: RpcContext,
     68     url: String,
     69     client_secret_key: Option<String>,
     70     signer_authority: Option<Nip46SessionAuthority>,
     71 ) -> Result<Nip46ConnectResponse, RpcError> {
     72     let signer_authority =
     73         Nip46Session::normalize_authority(signer_authority).map_err(RpcError::InvalidParams)?;
     74     let info = parse_connect_url(&url)?;
     75     match info.mode {
     76         Nip46ConnectMode::Bunker => connect_bunker(ctx, info, signer_authority).await,
     77         Nip46ConnectMode::Nostrconnect => {
     78             connect_nostrconnect(ctx, info, client_secret_key, signer_authority).await
     79         }
     80     }
     81 }
     82 
     83 async fn connect_bunker(
     84     ctx: RpcContext,
     85     info: Nip46ConnectInfo,
     86     signer_authority: Option<Nip46SessionAuthority>,
     87 ) -> Result<Nip46ConnectResponse, RpcError> {
     88     if info.relays.is_empty() {
     89         return Err(RpcError::InvalidParams("missing relay".to_string()));
     90     }
     91 
     92     let remote_signer_raw = info
     93         .remote_signer_pubkey
     94         .as_ref()
     95         .ok_or_else(|| RpcError::InvalidParams("missing remote signer pubkey".to_string()))?;
     96     let remote_signer_pubkey = radroots_nostr_parse_pubkey(remote_signer_raw)
     97         .map_err(|e| RpcError::InvalidParams(format!("invalid remote signer: {e}")))?;
     98 
     99     let client_keys = RadrootsNostrKeys::generate();
    100     let client_pubkey = client_keys.public_key();
    101     let client = RadrootsNostrClient::new(client_keys.clone());
    102 
    103     add_relays(&client, &info.relays).await?;
    104     client.connect().await;
    105     client
    106         .wait_for_connection(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
    107         .await;
    108 
    109     let request = NostrConnectRequest::Connect {
    110         remote_signer_public_key: remote_signer_pubkey.clone(),
    111         secret: info.secret.clone(),
    112     };
    113     let message = NostrConnectMessage::request(&request);
    114     let request_id = message.id().to_string();
    115     let filter = connect_response_filter(
    116         &remote_signer_pubkey,
    117         &client_pubkey,
    118         RadrootsNostrTimestamp::now(),
    119     )?;
    120     let notifications = client.notifications();
    121     let subscription = client
    122         .subscribe(filter, None)
    123         .await
    124         .map_err(|e| RpcError::Other(format!("nip46 connect failed: {e}")))?;
    125 
    126     if let Err(error) =
    127         send_connect_request(&client, &client_keys, &remote_signer_pubkey, message).await
    128     {
    129         client.unsubscribe(&subscription.val).await;
    130         return Err(error);
    131     }
    132 
    133     let response = wait_for_connect_response(
    134         &client,
    135         &client_keys,
    136         &remote_signer_pubkey,
    137         &request_id,
    138         notifications,
    139         &subscription.val,
    140     )
    141     .await?;
    142 
    143     validate_connect_response(&response, info.secret.as_deref())?;
    144     claim_secret(&ctx, info.secret.as_deref()).await?;
    145 
    146     let perms = filter_perms(&info.perms, &ctx.state.nip46_config.perms);
    147     let expires_at = session_expires_at(ctx.state.nip46_config.session_ttl_secs);
    148 
    149     let session_id = Uuid::new_v4().to_string();
    150     let session = Nip46Session {
    151         id: session_id.clone(),
    152         client,
    153         client_keys,
    154         client_pubkey,
    155         remote_signer_pubkey,
    156         user_pubkey: None,
    157         relays: info.relays.clone(),
    158         perms,
    159         name: info.name.clone(),
    160         url: info.url.clone(),
    161         image: info.image.clone(),
    162         expires_at,
    163         auth_required: false,
    164         authorized: true,
    165         auth_url: None,
    166         pending_request: None,
    167         signer_authority,
    168     };
    169     ctx.state.nip46_sessions.insert(session).await;
    170 
    171     Ok(Nip46ConnectResponse {
    172         session_id,
    173         mode: info.mode,
    174         remote_signer_pubkey: remote_signer_raw.to_string(),
    175         client_pubkey: client_pubkey.to_hex(),
    176         relays: info.relays,
    177     })
    178 }
    179 
    180 async fn connect_nostrconnect(
    181     ctx: RpcContext,
    182     info: Nip46ConnectInfo,
    183     client_secret_key: Option<String>,
    184     signer_authority: Option<Nip46SessionAuthority>,
    185 ) -> Result<Nip46ConnectResponse, RpcError> {
    186     if info.relays.is_empty() {
    187         return Err(RpcError::InvalidParams("missing relay".to_string()));
    188     }
    189     let secret = info
    190         .secret
    191         .as_deref()
    192         .ok_or_else(|| RpcError::InvalidParams("missing secret".to_string()))?;
    193     let client_secret_key = client_secret_key
    194         .map(|value| value.trim().to_string())
    195         .filter(|value| !value.is_empty())
    196         .ok_or_else(|| RpcError::InvalidParams("missing client_secret_key".to_string()))?;
    197     let client_secret_key = RadrootsNostrSecretKey::parse(&client_secret_key)
    198         .map_err(|e| RpcError::InvalidParams(format!("invalid client_secret_key: {e}")))?;
    199     let client_keys = RadrootsNostrKeys::new(client_secret_key);
    200     let client_pubkey = client_keys.public_key();
    201     let client_pubkey_raw = info
    202         .client_pubkey
    203         .as_ref()
    204         .ok_or_else(|| RpcError::InvalidParams("missing client pubkey".to_string()))?;
    205     let expected_pubkey = radroots_nostr_parse_pubkey(client_pubkey_raw)
    206         .map_err(|e| RpcError::InvalidParams(format!("invalid client pubkey: {e}")))?;
    207     if expected_pubkey != client_pubkey {
    208         return Err(RpcError::InvalidParams(
    209             "client_secret_key does not match client pubkey".to_string(),
    210         ));
    211     }
    212 
    213     let client = RadrootsNostrClient::new(client_keys.clone());
    214     add_relays(&client, &info.relays).await?;
    215     client.connect().await;
    216     client
    217         .wait_for_connection(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
    218         .await;
    219 
    220     let (remote_signer_pubkey, response) =
    221         wait_for_nostrconnect_response(&client, &client_keys, &client_pubkey, secret).await?;
    222     validate_nostrconnect_response(&response, secret)?;
    223     claim_secret(&ctx, info.secret.as_deref()).await?;
    224 
    225     let perms = filter_perms(&info.perms, &ctx.state.nip46_config.perms);
    226     let expires_at = session_expires_at(ctx.state.nip46_config.session_ttl_secs);
    227 
    228     let session_id = Uuid::new_v4().to_string();
    229     let session = Nip46Session {
    230         id: session_id.clone(),
    231         client,
    232         client_keys,
    233         client_pubkey,
    234         remote_signer_pubkey,
    235         user_pubkey: None,
    236         relays: info.relays.clone(),
    237         perms,
    238         name: info.name.clone(),
    239         url: info.url.clone(),
    240         image: info.image.clone(),
    241         expires_at,
    242         auth_required: false,
    243         authorized: true,
    244         auth_url: None,
    245         pending_request: None,
    246         signer_authority,
    247     };
    248     ctx.state.nip46_sessions.insert(session).await;
    249 
    250     Ok(Nip46ConnectResponse {
    251         session_id,
    252         mode: info.mode,
    253         remote_signer_pubkey: remote_signer_pubkey.to_hex(),
    254         client_pubkey: client_pubkey.to_hex(),
    255         relays: info.relays,
    256     })
    257 }
    258 
    259 async fn add_relays(client: &RadrootsNostrClient, relays: &[String]) -> Result<(), RpcError> {
    260     for relay in relays.iter() {
    261         client
    262             .add_relay(relay)
    263             .await
    264             .map_err(|e| RpcError::Other(format!("nip46 relay add failed: {e}")))?;
    265     }
    266     Ok(())
    267 }
    268 
    269 async fn claim_secret(ctx: &RpcContext, secret: Option<&str>) -> Result<(), RpcError> {
    270     let Some(secret) = secret else {
    271         return Ok(());
    272     };
    273     let trimmed = secret.trim();
    274     if trimmed.is_empty() {
    275         return Err(RpcError::InvalidParams("secret is empty".to_string()));
    276     }
    277     if ctx.state.nip46_sessions.claim_secret(trimmed).await {
    278         Ok(())
    279     } else {
    280         Err(RpcError::InvalidParams("secret already used".to_string()))
    281     }
    282 }
    283 
    284 async fn send_connect_request(
    285     client: &RadrootsNostrClient,
    286     client_keys: &RadrootsNostrKeys,
    287     remote_signer_pubkey: &RadrootsNostrPublicKey,
    288     message: NostrConnectMessage,
    289 ) -> Result<(), RpcError> {
    290     let event = RadrootsNostrEventBuilder::nostr_connect(
    291         client_keys,
    292         remote_signer_pubkey.clone(),
    293         message,
    294     )
    295     .map_err(|e| RpcError::Other(format!("nip46 connect request failed: {e}")))?;
    296     client
    297         .send_event_builder(event)
    298         .await
    299         .map_err(|e| RpcError::Other(format!("nip46 connect request failed: {e}")))?;
    300     Ok(())
    301 }
    302 
    303 fn connect_response_filter(
    304     remote_signer_pubkey: &RadrootsNostrPublicKey,
    305     client_pubkey: &RadrootsNostrPublicKey,
    306     since: RadrootsNostrTimestamp,
    307 ) -> Result<RadrootsNostrFilter, RpcError> {
    308     let filter = RadrootsNostrFilter::new()
    309         .kind(RadrootsNostrKind::NostrConnect)
    310         .author(remote_signer_pubkey.clone())
    311         .since(since);
    312     radroots_nostr_filter_tag(filter, "p", vec![client_pubkey.to_hex()])
    313         .map_err(|e| RpcError::Other(format!("nip46 connect filter failed: {e}")))
    314 }
    315 
    316 async fn wait_for_connect_response(
    317     client: &RadrootsNostrClient,
    318     client_keys: &RadrootsNostrKeys,
    319     remote_signer_pubkey: &RadrootsNostrPublicKey,
    320     request_id: &str,
    321     mut notifications: broadcast::Receiver<RadrootsNostrRelayPoolNotification>,
    322     subscription_id: &RadrootsNostrSubscriptionId,
    323 ) -> Result<NostrConnectMessage, RpcError> {
    324     let timeout = sleep(Duration::from_secs(DEFAULT_TIMEOUT_SECS));
    325     tokio::pin!(timeout);
    326 
    327     loop {
    328         tokio::select! {
    329             _ = &mut timeout => {
    330                 client.unsubscribe(subscription_id).await;
    331                 return Err(RpcError::Other("nip46 connect response not found".to_string()));
    332             }
    333             msg = notifications.recv() => {
    334                 let notification = match msg {
    335                     Ok(notification) => notification,
    336                     Err(broadcast::error::RecvError::Lagged(_)) => continue,
    337                     Err(broadcast::error::RecvError::Closed) => {
    338                         client.unsubscribe(subscription_id).await;
    339                         return Err(RpcError::Other("nip46 connect notification closed".to_string()));
    340                     }
    341                 };
    342                 let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else {
    343                     continue;
    344                 };
    345                 let event = (*event).clone();
    346                 if event.kind != RadrootsNostrKind::NostrConnect
    347                     || event.pubkey != *remote_signer_pubkey
    348                 {
    349                     continue;
    350                 }
    351                 let decrypted = nip44::decrypt(
    352                     client_keys.secret_key(),
    353                     remote_signer_pubkey,
    354                     &event.content,
    355                 )
    356                 .map_err(|e| RpcError::Other(format!("nip46 connect decrypt failed: {e}")))?;
    357                 let message = NostrConnectMessage::from_json(&decrypted)
    358                     .map_err(|e| RpcError::Other(format!("nip46 connect response parse failed: {e}")))?;
    359                 if message.is_response() && message.id() == request_id {
    360                     client.unsubscribe(subscription_id).await;
    361                     return Ok(message);
    362                 }
    363             }
    364         }
    365     }
    366 }
    367 
    368 fn validate_connect_response(
    369     response: &NostrConnectMessage,
    370     secret: Option<&str>,
    371 ) -> Result<(), RpcError> {
    372     let (result, error) = match response {
    373         NostrConnectMessage::Response { result, error, .. } => (result, error),
    374         _ => {
    375             return Err(RpcError::Other(
    376                 "nip46 connect response invalid".to_string(),
    377             ));
    378         }
    379     };
    380 
    381     if let Some(error) = error {
    382         return Err(RpcError::Other(format!("nip46 connect error: {error}")));
    383     }
    384 
    385     let result = result
    386         .as_deref()
    387         .ok_or_else(|| RpcError::Other("nip46 connect missing result".to_string()))?;
    388 
    389     if result == "ack" {
    390         return Ok(());
    391     }
    392 
    393     if secret.is_some_and(|expected| expected == result) {
    394         return Ok(());
    395     }
    396 
    397     Err(RpcError::Other(format!(
    398         "nip46 connect unexpected result: {result}"
    399     )))
    400 }
    401 
    402 fn validate_nostrconnect_response(
    403     response: &NostrConnectMessage,
    404     secret: &str,
    405 ) -> Result<(), RpcError> {
    406     let (result, error) = match response {
    407         NostrConnectMessage::Response { result, error, .. } => (result, error),
    408         _ => {
    409             return Err(RpcError::Other(
    410                 "nip46 connect response invalid".to_string(),
    411             ));
    412         }
    413     };
    414 
    415     if let Some(error) = error {
    416         return Err(RpcError::Other(format!("nip46 connect error: {error}")));
    417     }
    418 
    419     let Some(value) = result.as_deref() else {
    420         return Err(RpcError::Other("nip46 connect missing result".to_string()));
    421     };
    422 
    423     if value == secret {
    424         return Ok(());
    425     }
    426 
    427     Err(RpcError::Other(format!(
    428         "nip46 connect unexpected result: {value}"
    429     )))
    430 }
    431 
    432 async fn wait_for_nostrconnect_response(
    433     client: &RadrootsNostrClient,
    434     client_keys: &RadrootsNostrKeys,
    435     client_pubkey: &RadrootsNostrPublicKey,
    436     secret: &str,
    437 ) -> Result<(RadrootsNostrPublicKey, NostrConnectMessage), RpcError> {
    438     let filter = RadrootsNostrFilter::new()
    439         .kind(RadrootsNostrKind::NostrConnect)
    440         .since(RadrootsNostrTimestamp::now());
    441     let filter = radroots_nostr_filter_tag(filter, "p", vec![client_pubkey.to_hex()])
    442         .map_err(|e| RpcError::Other(format!("nip46 connect filter failed: {e}")))?;
    443     let mut notifications = client.notifications();
    444     let subscription = client
    445         .subscribe(filter, None)
    446         .await
    447         .map_err(|e| RpcError::Other(format!("nip46 connect failed: {e}")))?;
    448     let timeout = sleep(Duration::from_secs(DEFAULT_TIMEOUT_SECS));
    449     tokio::pin!(timeout);
    450 
    451     loop {
    452         tokio::select! {
    453             _ = &mut timeout => {
    454                 client.unsubscribe(&subscription.val).await;
    455                 return Err(RpcError::Other("nip46 connect response not found".to_string()));
    456             }
    457             msg = notifications.recv() => {
    458                 let notification = match msg {
    459                     Ok(notification) => notification,
    460                     Err(broadcast::error::RecvError::Lagged(_)) => continue,
    461                     Err(broadcast::error::RecvError::Closed) => {
    462                         return Err(RpcError::Other("nip46 connect notification closed".to_string()));
    463                     }
    464                 };
    465                 let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else {
    466                     continue;
    467                 };
    468                 let event = (*event).clone();
    469                 if event.kind != RadrootsNostrKind::NostrConnect {
    470                     continue;
    471                 }
    472                 let decrypted = nip44::decrypt(
    473                     client_keys.secret_key(),
    474                     &event.pubkey,
    475                     &event.content,
    476                 )
    477                 .map_err(|e| RpcError::Other(format!("nip46 connect decrypt failed: {e}")))?;
    478                 let message = NostrConnectMessage::from_json(&decrypted)
    479                     .map_err(|e| RpcError::Other(format!("nip46 connect response parse failed: {e}")))?;
    480                 if !message.is_response() || message.id().is_empty() {
    481                     continue;
    482                 }
    483                 validate_nostrconnect_response(&message, secret)?;
    484                 client.unsubscribe(&subscription.val).await;
    485                 return Ok((event.pubkey, message));
    486             }
    487         }
    488     }
    489 }