lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }