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 }