server.rs (6266B)
1 //! Relay- and persistence-independent NIP-46 server state. 2 3 use crate::error::RadrootsNostrConnectError; 4 use crate::message::{Request, RequestId, RequestMessage, Response, ResponseEnvelope}; 5 use crate::method::Method; 6 use crate::permission::{Permission, Permissions}; 7 use std::collections::BTreeSet; 8 9 pub const SERVER_MESSAGE_MAX_BYTES: usize = 524_288; 10 pub const SERVER_REPLAY_WINDOW_MAX: usize = 4_096; 11 12 /// Protocol-only server state for one bounded replay window. 13 /// 14 /// Approval UI, durable session state, encryption/signing keys, and relay 15 /// execution remain host responsibilities. 16 #[derive(Debug)] 17 pub struct Server { 18 supported_extensions: BTreeSet<Method>, 19 seen_request_ids: BTreeSet<RequestId>, 20 seen_fingerprints: BTreeSet<String>, 21 } 22 23 impl Server { 24 #[must_use] 25 pub fn new() -> Self { 26 Self { 27 supported_extensions: BTreeSet::new(), 28 seen_request_ids: BTreeSet::new(), 29 seen_fingerprints: BTreeSet::new(), 30 } 31 } 32 33 pub fn with_supported_extensions( 34 extensions: impl IntoIterator<Item = Method>, 35 ) -> Result<Self, RadrootsNostrConnectError> { 36 let mut server = Self::new(); 37 for extension in extensions { 38 if !matches!(extension, Method::Custom(_)) { 39 return Err(RadrootsNostrConnectError::InvalidServerState { 40 reason: "server extensions must use custom methods", 41 }); 42 } 43 server.supported_extensions.insert(extension); 44 } 45 Ok(server) 46 } 47 48 /// Parses and admits one decrypted request after the host verifies its event. 49 pub fn parse( 50 &mut self, 51 fingerprint: impl Into<String>, 52 message_json: &str, 53 ) -> Result<ServerRequest, RadrootsNostrConnectError> { 54 if message_json.len() > SERVER_MESSAGE_MAX_BYTES { 55 return Err(RadrootsNostrConnectError::InvalidServerRequest { 56 reason: "request message exceeds its byte limit", 57 }); 58 } 59 let fingerprint = fingerprint.into(); 60 if fingerprint.is_empty() 61 || fingerprint.len() > 128 62 || fingerprint.chars().any(char::is_control) 63 { 64 return Err(RadrootsNostrConnectError::InvalidServerRequest { 65 reason: "request fingerprint must be non-empty, bounded, and control-free", 66 }); 67 } 68 if self.seen_fingerprints.len() >= SERVER_REPLAY_WINDOW_MAX 69 || self.seen_request_ids.len() >= SERVER_REPLAY_WINDOW_MAX 70 { 71 return Err(RadrootsNostrConnectError::InvalidServerState { 72 reason: "server replay window is full", 73 }); 74 } 75 76 let message: RequestMessage = 77 serde_json::from_str(message_json).map_err(RadrootsNostrConnectError::from)?; 78 let request_id = message.request_id()?; 79 let method = message.payload().method(); 80 if matches!(&method, Method::Custom(_)) && !self.supported_extensions.contains(&method) { 81 return Err(RadrootsNostrConnectError::UnsupportedMethod(method)); 82 } 83 if self.seen_fingerprints.contains(&fingerprint) 84 || self.seen_request_ids.contains(&request_id) 85 { 86 return Err(RadrootsNostrConnectError::ReplayedRequest); 87 } 88 self.seen_fingerprints.insert(fingerprint); 89 self.seen_request_ids.insert(request_id.clone()); 90 91 Ok(ServerRequest { 92 request_id, 93 required_permission: required_permission(message.payload()), 94 request: message.request, 95 }) 96 } 97 } 98 99 impl Default for Server { 100 fn default() -> Self { 101 Self::new() 102 } 103 } 104 105 #[derive(Debug, Clone, PartialEq, Eq)] 106 pub struct ServerRequest { 107 request_id: RequestId, 108 request: Request, 109 required_permission: Option<Permission>, 110 } 111 112 impl ServerRequest { 113 #[must_use] 114 pub fn request_id(&self) -> &RequestId { 115 &self.request_id 116 } 117 118 #[must_use] 119 pub fn request(&self) -> &Request { 120 &self.request 121 } 122 123 #[must_use] 124 pub fn required_permission(&self) -> Option<&Permission> { 125 self.required_permission.as_ref() 126 } 127 128 #[must_use] 129 pub fn is_allowed_by(&self, granted: &Permissions) -> bool { 130 self.required_permission.as_ref().is_none_or(|permission| { 131 granted.allows_request(permission.method(), permission.parameter()) 132 }) 133 } 134 135 /// Constructs a correlated plaintext response for host encryption/signing. 136 pub fn respond(self, response: Response) -> Result<ServerResponse, RadrootsNostrConnectError> { 137 let envelope = response.into_envelope(self.request_id.to_string())?; 138 let json = serde_json::to_string(&envelope).map_err(RadrootsNostrConnectError::from)?; 139 Ok(ServerResponse { envelope, json }) 140 } 141 } 142 143 #[derive(Clone, PartialEq, Eq)] 144 pub struct ServerResponse { 145 envelope: ResponseEnvelope, 146 json: String, 147 } 148 149 impl ServerResponse { 150 #[must_use] 151 pub fn envelope(&self) -> &ResponseEnvelope { 152 &self.envelope 153 } 154 155 #[must_use] 156 pub fn as_json(&self) -> &str { 157 &self.json 158 } 159 } 160 161 impl std::fmt::Debug for ServerResponse { 162 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 163 formatter.write_str("ServerResponse(<redacted>)") 164 } 165 } 166 167 #[must_use] 168 pub fn required_permission(request: &Request) -> Option<Permission> { 169 match request { 170 Request::Connect { .. } 171 | Request::GetPublicKey 172 | Request::GetSessionCapability 173 | Request::Ping 174 | Request::Logout => None, 175 Request::SignEvent(event) => Some(Permission::with_parameter( 176 Method::SignEvent, 177 format!("kind:{}", event.kind()), 178 )), 179 Request::Nip04Encrypt { .. } => Some(Permission::new(Method::Nip04Encrypt)), 180 Request::Nip04Decrypt { .. } => Some(Permission::new(Method::Nip04Decrypt)), 181 Request::Nip44Encrypt { .. } => Some(Permission::new(Method::Nip44Encrypt)), 182 Request::Nip44Decrypt { .. } => Some(Permission::new(Method::Nip44Decrypt)), 183 Request::SwitchRelays => Some(Permission::new(Method::SwitchRelays)), 184 Request::Custom { method, .. } => Some(Permission::new(method.clone())), 185 } 186 }