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 }