radrootsd

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

connect.rs (16619B)


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