mirror of
https://github.com/scsibug/nostr-rs-relay.git
synced 2024-11-12 14:29:06 -05:00
feat(NIP-42): pubkey authentication
Configurable in `config.toml`. Limited functionality, but this does send metadata to gRPC for event authorization. fixes: https://todo.sr.ht/~gheartsfield/nostr-rs-relay/66
This commit is contained in:
parent
d0f57aea21
commit
5cecfba319
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1592,6 +1592,7 @@ dependencies = [
|
|||
"tracing",
|
||||
"tracing-subscriber 0.2.25",
|
||||
"tungstenite",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ chrono = "0.4.23"
|
|||
prometheus = "0.13.3"
|
||||
indicatif = "0.17.3"
|
||||
bech32 = "0.9.1"
|
||||
url = "2.3.1"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1"
|
||||
|
|
|
@ -35,6 +35,7 @@ mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay).
|
|||
- [ ] NIP-26: [Event Delegation](https://github.com/nostr-protocol/nips/blob/master/26.md) (_implemented, but currently disabled_)
|
||||
- [x] NIP-28: [Public Chat](https://github.com/nostr-protocol/nips/blob/master/28.md)
|
||||
- [x] NIP-33: [Parameterized Replaceable Events](https://github.com/nostr-protocol/nips/blob/master/33.md)
|
||||
- [x] NIP-42: [Authentication of clients to relays](https://github.com/nostr-protocol/nips/blob/master/42.md)
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
|
@ -136,6 +136,8 @@ reject_future_seconds = 1800
|
|||
# "35d26e4690cbe1a898af61cc3515661eb5fa763b57bd0b42e45099c8b32fd50f",
|
||||
# "887645fef0ce0c3c1218d2f5d8e6132a19304cdc57cd20281d082f38cfea0072",
|
||||
#]
|
||||
# Enable NIP-42 authentication
|
||||
#nip42_auth = false
|
||||
|
||||
[verified_users]
|
||||
# NIP-05 verification of users. Can be "enabled" to require NIP-05
|
||||
|
|
|
@ -75,6 +75,7 @@ pub struct Limits {
|
|||
#[allow(unused)]
|
||||
pub struct Authorization {
|
||||
pub pubkey_whitelist: Option<Vec<String>>, // If present, only allow these pubkeys to publish events
|
||||
pub nip42_auth: bool, // if true enables NIP-42 authentication
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
@ -250,6 +251,7 @@ impl Default for Settings {
|
|||
},
|
||||
authorization: Authorization {
|
||||
pubkey_whitelist: None, // Allow any address to publish
|
||||
nip42_auth: false, // Disable NIP-42 authentication
|
||||
},
|
||||
verified_users: VerifiedUsers {
|
||||
mode: VerifiedUsersMode::Disabled,
|
||||
|
|
124
src/conn.rs
124
src/conn.rs
|
@ -1,16 +1,30 @@
|
|||
//! Client connection state
|
||||
use crate::close::Close;
|
||||
use crate::error::Error;
|
||||
use crate::error::Result;
|
||||
|
||||
use crate::subscription::Subscription;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use tracing::{debug, trace};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::close::Close;
|
||||
use crate::conn::Nip42AuthState::{AuthPubkey, Challenge, NoAuth};
|
||||
use crate::error::Error;
|
||||
use crate::error::Result;
|
||||
use crate::event::Event;
|
||||
use crate::subscription::Subscription;
|
||||
use crate::utils::{host_str, unix_time};
|
||||
|
||||
/// A subscription identifier has a maximum length
|
||||
const MAX_SUBSCRIPTION_ID_LEN: usize = 256;
|
||||
|
||||
/// NIP-42 authentication state
|
||||
pub enum Nip42AuthState {
|
||||
/// The client is not authenticated yet
|
||||
NoAuth,
|
||||
/// The AUTH challenge sent
|
||||
Challenge(String),
|
||||
/// The client is authenticated
|
||||
AuthPubkey(String),
|
||||
}
|
||||
|
||||
/// State for a client connection
|
||||
pub struct ClientConn {
|
||||
/// Client IP (either from socket, or configured proxy header
|
||||
|
@ -21,6 +35,8 @@ pub struct ClientConn {
|
|||
subscriptions: HashMap<String, Subscription>,
|
||||
/// Per-connection maximum concurrent subscriptions
|
||||
max_subs: usize,
|
||||
/// NIP-42 AUTH
|
||||
auth: Nip42AuthState,
|
||||
}
|
||||
|
||||
impl Default for ClientConn {
|
||||
|
@ -39,15 +55,18 @@ impl ClientConn {
|
|||
client_id,
|
||||
subscriptions: HashMap::new(),
|
||||
max_subs: 32,
|
||||
auth: NoAuth,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use] pub fn subscriptions(&self) -> &HashMap<String, Subscription> {
|
||||
#[must_use]
|
||||
pub fn subscriptions(&self) -> &HashMap<String, Subscription> {
|
||||
&self.subscriptions
|
||||
}
|
||||
|
||||
/// Check if the given subscription already exists
|
||||
#[must_use] pub fn has_subscription(&self, sub: &Subscription) -> bool {
|
||||
#[must_use]
|
||||
pub fn has_subscription(&self, sub: &Subscription) -> bool {
|
||||
self.subscriptions.values().any(|x| x == sub)
|
||||
}
|
||||
|
||||
|
@ -63,6 +82,22 @@ impl ClientConn {
|
|||
&self.client_ip_addr
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn auth_pubkey(&self) -> Option<&String> {
|
||||
match &self.auth {
|
||||
AuthPubkey(pubkey) => Some(pubkey),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn auth_challenge(&self) -> Option<&String> {
|
||||
match &self.auth {
|
||||
Challenge(pubkey) => Some(pubkey),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new subscription for this connection.
|
||||
/// # Errors
|
||||
///
|
||||
|
@ -116,4 +151,79 @@ impl ClientConn {
|
|||
self.get_client_prefix(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn generate_auth_challenge(&mut self) {
|
||||
self.auth = Challenge(Uuid::new_v4().to_string());
|
||||
}
|
||||
|
||||
pub fn authenticate(&mut self, event: &Event, relay_url: &String) -> Result<()> {
|
||||
match &self.auth {
|
||||
Challenge(_) => (),
|
||||
AuthPubkey(_) => {
|
||||
// already authenticated
|
||||
return Ok(())
|
||||
},
|
||||
NoAuth => {
|
||||
// unexpected AUTH request
|
||||
return Err(Error::AuthFailure);
|
||||
},
|
||||
}
|
||||
match event.validate() {
|
||||
Ok(_) => {
|
||||
if event.kind != 22242 {
|
||||
return Err(Error::AuthFailure);
|
||||
}
|
||||
|
||||
let curr_time = unix_time();
|
||||
let past_cutoff = curr_time - 600; // 10 minutes
|
||||
let future_cutoff = curr_time + 600; // 10 minutes
|
||||
if event.created_at < past_cutoff || event.created_at > future_cutoff {
|
||||
return Err(Error::AuthFailure);
|
||||
}
|
||||
|
||||
let mut challenge: Option<&String> = None;
|
||||
let mut relay: Option<&String> = None;
|
||||
|
||||
for tag in &event.tags {
|
||||
if tag.len() == 2 && tag.get(0) == Some(&"challenge".into()) {
|
||||
challenge = tag.get(1);
|
||||
}
|
||||
if tag.len() == 2 && tag.get(0) == Some(&"relay".into()) {
|
||||
relay = tag.get(1);
|
||||
}
|
||||
}
|
||||
|
||||
match (challenge, &self.auth) {
|
||||
(Some(received_challenge), Challenge(sent_challenge)) => {
|
||||
if received_challenge != sent_challenge {
|
||||
return Err(Error::AuthFailure);
|
||||
}
|
||||
}
|
||||
(_, _) => {
|
||||
return Err(Error::AuthFailure);
|
||||
}
|
||||
}
|
||||
|
||||
match (relay.and_then(|url| host_str(url)), host_str(relay_url)) {
|
||||
(Some(received_relay), Some(our_relay)) => {
|
||||
if received_relay != our_relay {
|
||||
return Err(Error::AuthFailure);
|
||||
}
|
||||
}
|
||||
(_, _) => {
|
||||
return Err(Error::AuthFailure);
|
||||
}
|
||||
}
|
||||
|
||||
self.auth = AuthPubkey(event.pubkey.clone());
|
||||
trace!(
|
||||
"authenticated pubkey {} (cid: {})",
|
||||
event.pubkey.chars().take(8).collect::<String>(),
|
||||
self.get_client_prefix()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(Error::AuthFailure),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ pub struct SubmittedEvent {
|
|||
pub source_ip: String,
|
||||
pub origin: Option<String>,
|
||||
pub user_agent: Option<String>,
|
||||
pub auth_pubkey: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// Database file
|
||||
|
@ -240,7 +241,7 @@ pub async fn db_writer(
|
|||
if let Some(ref mut c) = grpc_client {
|
||||
trace!("checking if grpc permits");
|
||||
let grpc_start = Instant::now();
|
||||
let decision_res = c.admit_event(&event, &subm_event.source_ip, subm_event.origin, subm_event.user_agent, nip05_address).await;
|
||||
let decision_res = c.admit_event(&event, &subm_event.source_ip, subm_event.origin, subm_event.user_agent, nip05_address, subm_event.auth_pubkey).await;
|
||||
match decision_res {
|
||||
Ok(decision) => {
|
||||
if !decision.permitted() {
|
||||
|
|
|
@ -68,6 +68,8 @@ pub enum Error {
|
|||
AuthzError,
|
||||
#[error("Tonic GRPC error")]
|
||||
TonicError(tonic::Status),
|
||||
#[error("Invalid AUTH message")]
|
||||
AuthFailure,
|
||||
#[error("Unknown/Undocumented")]
|
||||
UnknownError,
|
||||
}
|
||||
|
|
20
src/event.rs
20
src/event.rs
|
@ -14,6 +14,8 @@ use std::collections::HashMap;
|
|||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
use tracing::{debug, info};
|
||||
use crate::event::EventWrapper::WrappedEvent;
|
||||
use crate::event::EventWrapper::WrappedAuth;
|
||||
|
||||
lazy_static! {
|
||||
/// Secp256k1 verification instance.
|
||||
|
@ -83,17 +85,27 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
pub enum EventWrapper {
|
||||
WrappedEvent(Event),
|
||||
WrappedAuth(Event)
|
||||
}
|
||||
|
||||
/// Convert network event to parsed/validated event.
|
||||
impl From<EventCmd> for Result<Event> {
|
||||
fn from(ec: EventCmd) -> Result<Event> {
|
||||
impl From<EventCmd> for Result<EventWrapper> {
|
||||
fn from(ec: EventCmd) -> Result<EventWrapper> {
|
||||
// ensure command is correct
|
||||
if ec.cmd == "EVENT" {
|
||||
ec.event.validate().map(|_| {
|
||||
let mut e = ec.event;
|
||||
e.build_index();
|
||||
e.update_delegation();
|
||||
e
|
||||
WrappedEvent(e)
|
||||
})
|
||||
} else if ec.cmd == "AUTH" {
|
||||
// we don't want to validate the event here, because NIP-42 can be disabled
|
||||
// it will be validated later during the authentication process
|
||||
Ok(WrappedAuth(ec.event))
|
||||
} else {
|
||||
Err(CommandUnknownError)
|
||||
}
|
||||
|
@ -326,7 +338,7 @@ impl Event {
|
|||
}
|
||||
|
||||
/// Convert event to canonical representation for signing.
|
||||
fn to_canonical(&self) -> Option<String> {
|
||||
pub fn to_canonical(&self) -> Option<String> {
|
||||
// create a JsonValue for each event element
|
||||
let mut c: Vec<Value> = vec![];
|
||||
// id must be set to 0
|
||||
|
|
17
src/info.rs
17
src/info.rs
|
@ -1,6 +1,6 @@
|
|||
//! Relay metadata using NIP-11
|
||||
/// Relay Info
|
||||
use crate::config;
|
||||
use crate::config::Settings;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
|
||||
|
@ -27,15 +27,24 @@ pub struct RelayInfo {
|
|||
}
|
||||
|
||||
/// Convert an Info configuration into public Relay Info
|
||||
impl From<config::Info> for RelayInfo {
|
||||
fn from(i: config::Info) -> Self {
|
||||
impl From<Settings> for RelayInfo {
|
||||
fn from(c: Settings) -> Self {
|
||||
let mut supported_nips = vec![1, 2, 9, 11, 12, 15, 16, 20, 22, 33];
|
||||
|
||||
if c.authorization.nip42_auth {
|
||||
supported_nips.push(42);
|
||||
supported_nips.sort();
|
||||
}
|
||||
|
||||
let i = c.info;
|
||||
|
||||
RelayInfo {
|
||||
id: i.relay_url,
|
||||
name: i.name,
|
||||
description: i.description,
|
||||
pubkey: i.pubkey,
|
||||
contact: i.contact,
|
||||
supported_nips: Some(vec![1, 2, 9, 11, 12, 15, 16, 20, 22, 33]),
|
||||
supported_nips: Some(supported_nips),
|
||||
software: Some("https://git.sr.ht/~gheartsfield/nostr-rs-relay".to_owned()),
|
||||
version: CARGO_PKG_VERSION.map(std::borrow::ToOwned::to_owned),
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@ impl EventAuthzService {
|
|||
origin: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
nip05: Option<Nip05Name>,
|
||||
auth_pubkey: Option<Vec<u8>>
|
||||
) -> Result<Box<dyn AuthzDecision>> {
|
||||
self.ready_connection().await;
|
||||
let id_blob = hex::decode(&event.id)?;
|
||||
|
@ -97,7 +98,7 @@ impl EventAuthzService {
|
|||
ip_addr: Some(ip.to_string()),
|
||||
origin,
|
||||
user_agent,
|
||||
auth_pubkey: None,
|
||||
auth_pubkey,
|
||||
nip05: nip05.map(|x| nauthz_grpc::event_request::Nip05Name::from(x)),
|
||||
})
|
||||
.await?;
|
||||
|
|
|
@ -16,6 +16,7 @@ pub struct EventResult {
|
|||
pub enum Notice {
|
||||
Message(String),
|
||||
EventResult(EventResult),
|
||||
AuthChallenge(String)
|
||||
}
|
||||
|
||||
impl EventResultStatus {
|
||||
|
|
|
@ -7,6 +7,8 @@ use crate::repo::NostrRepo;
|
|||
use crate::db;
|
||||
use crate::db::SubmittedEvent;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::event::EventWrapper;
|
||||
use crate::server::EventWrapper::{WrappedAuth, WrappedEvent};
|
||||
use crate::event::Event;
|
||||
use crate::event::EventCmd;
|
||||
use crate::info::RelayInfo;
|
||||
|
@ -48,6 +50,7 @@ use tungstenite::error::Error as WsError;
|
|||
use tungstenite::handshake;
|
||||
use tungstenite::protocol::Message;
|
||||
use tungstenite::protocol::WebSocketConfig;
|
||||
use crate::server::Error::CommandUnknownError;
|
||||
|
||||
/// Handle arbitrary HTTP requests, including for `WebSocket` upgrades.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -157,7 +160,7 @@ async fn handle_web_request(
|
|||
if mt_str.contains("application/nostr+json") {
|
||||
// build a relay info response
|
||||
debug!("Responding to server info request");
|
||||
let rinfo = RelayInfo::from(settings.info);
|
||||
let rinfo = RelayInfo::from(settings);
|
||||
let b = Body::from(serde_json::to_string_pretty(&rinfo).unwrap());
|
||||
return Ok(Response::builder()
|
||||
.status(200)
|
||||
|
@ -268,6 +271,10 @@ fn create_metrics() -> (Registry, NostrMetrics) {
|
|||
"nostr_cmd_close_total",
|
||||
"CLOSE commands",
|
||||
)).unwrap();
|
||||
let cmd_auth = IntCounter::with_opts(Opts::new(
|
||||
"nostr_cmd_auth_total",
|
||||
"AUTH commands",
|
||||
)).unwrap();
|
||||
let disconnects = IntCounterVec::new(
|
||||
Opts::new("nostr_disconnects_total", "Client disconnects"),
|
||||
vec!["reason"].as_slice(),
|
||||
|
@ -282,6 +289,7 @@ fn create_metrics() -> (Registry, NostrMetrics) {
|
|||
registry.register(Box::new(cmd_req.clone())).unwrap();
|
||||
registry.register(Box::new(cmd_event.clone())).unwrap();
|
||||
registry.register(Box::new(cmd_close.clone())).unwrap();
|
||||
registry.register(Box::new(cmd_auth.clone())).unwrap();
|
||||
registry.register(Box::new(disconnects.clone())).unwrap();
|
||||
let metrics = NostrMetrics {
|
||||
query_sub,
|
||||
|
@ -295,6 +303,7 @@ fn create_metrics() -> (Registry, NostrMetrics) {
|
|||
cmd_req,
|
||||
cmd_event,
|
||||
cmd_close,
|
||||
cmd_auth,
|
||||
};
|
||||
(registry,metrics)
|
||||
}
|
||||
|
@ -488,7 +497,7 @@ pub fn start_server(settings: &Settings, shutdown_rx: MpscReceiver<()>) -> Resul
|
|||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum NostrMessage {
|
||||
/// An `EVENT` message
|
||||
/// `EVENT` and `AUTH` messages
|
||||
EventMsg(EventCmd),
|
||||
/// A `REQ` message
|
||||
SubMsg(Subscription),
|
||||
|
@ -528,6 +537,7 @@ fn make_notice_message(notice: &Notice) -> Message {
|
|||
let json = match notice {
|
||||
Notice::Message(ref msg) => json!(["NOTICE", msg]),
|
||||
Notice::EventResult(ref res) => json!(["OK", res.id, res.status.to_bool(), res.msg]),
|
||||
Notice::AuthChallenge(ref challenge) => json!(["AUTH", challenge]),
|
||||
};
|
||||
|
||||
Message::text(json.to_string())
|
||||
|
@ -616,6 +626,14 @@ async fn nostr_server(
|
|||
// Measure connections
|
||||
metrics.connections.inc();
|
||||
|
||||
if settings.authorization.nip42_auth {
|
||||
conn.generate_auth_challenge();
|
||||
if let Some(challenge) = conn.auth_challenge() {
|
||||
ws_stream.send(
|
||||
make_notice_message(&Notice::AuthChallenge(challenge.to_string()))).await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown.recv() => {
|
||||
|
@ -729,16 +747,17 @@ async fn nostr_server(
|
|||
// An EventCmd needs to be validated to be converted into an Event
|
||||
// handle each type of message
|
||||
let evid = ec.event_id().to_owned();
|
||||
let parsed : Result<Event> = Result::<Event>::from(ec);
|
||||
metrics.cmd_event.inc();
|
||||
let parsed : Result<EventWrapper> = Result::<EventWrapper>::from(ec);
|
||||
match parsed {
|
||||
Ok(e) => {
|
||||
Ok(WrappedEvent(e)) => {
|
||||
metrics.cmd_event.inc();
|
||||
let id_prefix:String = e.id.chars().take(8).collect();
|
||||
debug!("successfully parsed/validated event: {:?} (cid: {}, kind: {})", id_prefix, cid, e.kind);
|
||||
// check if the event is too far in the future.
|
||||
if e.is_valid_timestamp(settings.options.reject_future_seconds) {
|
||||
// Write this to the database.
|
||||
let submit_event = SubmittedEvent { event: e.clone(), notice_tx: notice_tx.clone(), source_ip: conn.ip().to_string(), origin: client_info.origin.clone(), user_agent: client_info.user_agent.clone()};
|
||||
let auth_pubkey = conn.auth_pubkey().and_then(|pubkey| hex::decode(&pubkey).ok());
|
||||
let submit_event = SubmittedEvent { event: e.clone(), notice_tx: notice_tx.clone(), source_ip: conn.ip().to_string(), origin: client_info.origin.clone(), user_agent: client_info.user_agent.clone(), auth_pubkey };
|
||||
event_tx.send(submit_event).await.ok();
|
||||
client_published_event_count += 1;
|
||||
} else {
|
||||
|
@ -750,7 +769,39 @@ async fn nostr_server(
|
|||
}
|
||||
}
|
||||
},
|
||||
Ok(WrappedAuth(event)) => {
|
||||
metrics.cmd_auth.inc();
|
||||
if settings.authorization.nip42_auth {
|
||||
let id_prefix:String = event.id.chars().take(8).collect();
|
||||
debug!("successfully parsed auth: {:?} (cid: {})", id_prefix, cid);
|
||||
match &settings.info.relay_url {
|
||||
None => {
|
||||
error!("AUTH command received, but relay_url is not set in the config file (cid: {})", cid);
|
||||
},
|
||||
Some(relay) => {
|
||||
match conn.authenticate(&event, &relay) {
|
||||
Ok(_) => {
|
||||
let pubkey = match conn.auth_pubkey() {
|
||||
Some(k) => k.chars().take(8).collect(),
|
||||
None => "<unspecified>".to_string(),
|
||||
};
|
||||
info!("client is authenticated: (cid: {}, pubkey: {:?})", cid, pubkey);
|
||||
},
|
||||
Err(e) => {
|
||||
info!("authentication error: {} (cid: {})", e, cid);
|
||||
ws_stream.send(make_notice_message(&Notice::message(format!("Authentication error: {e}")))).await.ok();
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let e = CommandUnknownError;
|
||||
info!("client sent an invalid event (cid: {})", cid);
|
||||
ws_stream.send(make_notice_message(&Notice::invalid(evid, &format!("{e}")))).await.ok();
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
metrics.cmd_event.inc();
|
||||
info!("client sent an invalid event (cid: {})", cid);
|
||||
ws_stream.send(make_notice_message(&Notice::invalid(evid, &format!("{e}")))).await.ok();
|
||||
}
|
||||
|
@ -855,5 +906,6 @@ pub struct NostrMetrics {
|
|||
pub cmd_req: IntCounter, // count of REQ commands received
|
||||
pub cmd_event: IntCounter, // count of EVENT commands received
|
||||
pub cmd_close: IntCounter, // count of CLOSE commands received
|
||||
pub cmd_auth: IntCounter, // count of AUTH commands received
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//! Common utility functions
|
||||
use bech32::FromBase32;
|
||||
use std::time::SystemTime;
|
||||
use url::Url;
|
||||
|
||||
/// Seconds since 1970.
|
||||
#[must_use] pub fn unix_time() -> u64 {
|
||||
|
@ -33,6 +34,10 @@ pub fn nip19_to_hex(s: &str) -> Result<String, bech32::Error> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn host_str(url: &String) -> Option<String> {
|
||||
Url::parse(url).ok().and_then(|u| u.host_str().map(|s| s.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
356
tests/conn.rs
Normal file
356
tests/conn.rs
Normal file
|
@ -0,0 +1,356 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bitcoin_hashes::hex::ToHex;
|
||||
use bitcoin_hashes::sha256;
|
||||
use bitcoin_hashes::Hash;
|
||||
use secp256k1::rand;
|
||||
use secp256k1::{KeyPair, Secp256k1, XOnlyPublicKey};
|
||||
|
||||
use nostr_rs_relay::conn::ClientConn;
|
||||
use nostr_rs_relay::error::Error;
|
||||
use nostr_rs_relay::event::Event;
|
||||
use nostr_rs_relay::utils::unix_time;
|
||||
|
||||
const RELAY: &str = "wss://nostr.example.com/";
|
||||
|
||||
#[test]
|
||||
fn test_generate_auth_challenge() {
|
||||
let mut client_conn = ClientConn::new("127.0.0.1".into());
|
||||
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
client_conn.generate_auth_challenge();
|
||||
|
||||
assert_ne!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
let last_auth_challenge = client_conn.auth_challenge().cloned();
|
||||
|
||||
client_conn.generate_auth_challenge();
|
||||
|
||||
assert_ne!(client_conn.auth_challenge(), None);
|
||||
assert_ne!(
|
||||
client_conn.auth_challenge().unwrap(),
|
||||
&last_auth_challenge.unwrap()
|
||||
);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authenticate_with_valid_event() {
|
||||
let mut client_conn = ClientConn::new("127.0.0.1".into());
|
||||
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
client_conn.generate_auth_challenge();
|
||||
|
||||
assert_ne!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
let challenge = client_conn.auth_challenge().unwrap();
|
||||
let event = auth_event(challenge);
|
||||
|
||||
let result = client_conn.authenticate(&event, &RELAY.into());
|
||||
|
||||
assert!(matches!(result, Ok(())));
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), Some(&event.pubkey));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fail_to_authenticate_in_invalid_state() {
|
||||
let mut client_conn = ClientConn::new("127.0.0.1".into());
|
||||
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
let event = auth_event(&"challenge".into());
|
||||
let result = client_conn.authenticate(&event, &RELAY.into());
|
||||
|
||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authenticate_when_already_authenticated() {
|
||||
let mut client_conn = ClientConn::new("127.0.0.1".into());
|
||||
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
client_conn.generate_auth_challenge();
|
||||
|
||||
assert_ne!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
let challenge = client_conn.auth_challenge().unwrap().clone();
|
||||
|
||||
let event = auth_event(&challenge);
|
||||
let result = client_conn.authenticate(&event, &RELAY.into());
|
||||
|
||||
assert!(matches!(result, Ok(())));
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), Some(&event.pubkey));
|
||||
|
||||
let event1 = auth_event(&challenge);
|
||||
let result1 = client_conn.authenticate(&event1, &RELAY.into());
|
||||
|
||||
assert!(matches!(result1, Ok(())));
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), Some(&event.pubkey));
|
||||
assert_ne!(client_conn.auth_pubkey(), Some(&event1.pubkey));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fail_to_authenticate_with_invalid_event() {
|
||||
let mut client_conn = ClientConn::new("127.0.0.1".into());
|
||||
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
client_conn.generate_auth_challenge();
|
||||
|
||||
assert_ne!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
let challenge = client_conn.auth_challenge().unwrap();
|
||||
let mut event = auth_event(challenge);
|
||||
event.sig = event.sig.chars().rev().collect::<String>();
|
||||
|
||||
let result = client_conn.authenticate(&event, &RELAY.into());
|
||||
|
||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fail_to_authenticate_with_invalid_event_kind() {
|
||||
let mut client_conn = ClientConn::new("127.0.0.1".into());
|
||||
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
client_conn.generate_auth_challenge();
|
||||
|
||||
assert_ne!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
let challenge = client_conn.auth_challenge().unwrap();
|
||||
let event = auth_event_with_kind(challenge, 9999999999999999);
|
||||
|
||||
let result = client_conn.authenticate(&event, &RELAY.into());
|
||||
|
||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fail_to_authenticate_with_expired_timestamp() {
|
||||
let mut client_conn = ClientConn::new("127.0.0.1".into());
|
||||
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
client_conn.generate_auth_challenge();
|
||||
|
||||
assert_ne!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
let challenge = client_conn.auth_challenge().unwrap();
|
||||
let event = auth_event_with_created_at(challenge, unix_time() - 1200); // 20 minutes
|
||||
|
||||
let result = client_conn.authenticate(&event, &RELAY.into());
|
||||
|
||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fail_to_authenticate_with_future_timestamp() {
|
||||
let mut client_conn = ClientConn::new("127.0.0.1".into());
|
||||
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
client_conn.generate_auth_challenge();
|
||||
|
||||
assert_ne!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
let challenge = client_conn.auth_challenge().unwrap();
|
||||
let event = auth_event_with_created_at(challenge, unix_time() + 1200); // 20 minutes
|
||||
|
||||
let result = client_conn.authenticate(&event, &RELAY.into());
|
||||
|
||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fail_to_authenticate_without_tags() {
|
||||
let mut client_conn = ClientConn::new("127.0.0.1".into());
|
||||
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
client_conn.generate_auth_challenge();
|
||||
|
||||
assert_ne!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
let event = auth_event_without_tags();
|
||||
|
||||
let result = client_conn.authenticate(&event, &RELAY.into());
|
||||
|
||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fail_to_authenticate_without_challenge() {
|
||||
let mut client_conn = ClientConn::new("127.0.0.1".into());
|
||||
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
client_conn.generate_auth_challenge();
|
||||
|
||||
assert_ne!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
let event = auth_event_without_challenge();
|
||||
|
||||
let result = client_conn.authenticate(&event, &RELAY.into());
|
||||
|
||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fail_to_authenticate_without_relay() {
|
||||
let mut client_conn = ClientConn::new("127.0.0.1".into());
|
||||
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
client_conn.generate_auth_challenge();
|
||||
|
||||
assert_ne!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
let challenge = client_conn.auth_challenge().unwrap();
|
||||
let event = auth_event_without_relay(challenge);
|
||||
|
||||
let result = client_conn.authenticate(&event, &RELAY.into());
|
||||
|
||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fail_to_authenticate_with_invalid_challenge() {
|
||||
let mut client_conn = ClientConn::new("127.0.0.1".into());
|
||||
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
client_conn.generate_auth_challenge();
|
||||
|
||||
assert_ne!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
let event = auth_event(&"invalid challenge".into());
|
||||
|
||||
let result = client_conn.authenticate(&event, &RELAY.into());
|
||||
|
||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fail_to_authenticate_with_invalid_relay() {
|
||||
let mut client_conn = ClientConn::new("127.0.0.1".into());
|
||||
|
||||
assert_eq!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
client_conn.generate_auth_challenge();
|
||||
|
||||
assert_ne!(client_conn.auth_challenge(), None);
|
||||
assert_eq!(client_conn.auth_pubkey(), None);
|
||||
|
||||
let challenge = client_conn.auth_challenge().unwrap();
|
||||
let event = auth_event_with_relay(challenge, &"xyz".into());
|
||||
|
||||
let result = client_conn.authenticate(&event, &RELAY.into());
|
||||
|
||||
assert!(matches!(result, Err(Error::AuthFailure)));
|
||||
}
|
||||
|
||||
fn auth_event(challenge: &String) -> Event {
|
||||
create_auth_event(Some(challenge), Some(&RELAY.into()), 22242, unix_time())
|
||||
}
|
||||
|
||||
fn auth_event_with_kind(challenge: &String, kind: u64) -> Event {
|
||||
create_auth_event(Some(challenge), Some(&RELAY.into()), kind, unix_time())
|
||||
}
|
||||
|
||||
fn auth_event_with_created_at(challenge: &String, created_at: u64) -> Event {
|
||||
create_auth_event(Some(challenge), Some(&RELAY.into()), 22242, created_at)
|
||||
}
|
||||
|
||||
fn auth_event_without_challenge() -> Event {
|
||||
create_auth_event(None, Some(&RELAY.into()), 22242, unix_time())
|
||||
}
|
||||
|
||||
fn auth_event_without_relay(challenge: &String) -> Event {
|
||||
create_auth_event(Some(challenge), None, 22242, unix_time())
|
||||
}
|
||||
|
||||
fn auth_event_without_tags() -> Event {
|
||||
create_auth_event(None, None, 22242, unix_time())
|
||||
}
|
||||
|
||||
fn auth_event_with_relay(challenge: &String, relay: &String) -> Event {
|
||||
create_auth_event(Some(challenge), Some(relay), 22242, unix_time())
|
||||
}
|
||||
|
||||
fn create_auth_event(
|
||||
challenge: Option<&String>,
|
||||
relay: Option<&String>,
|
||||
kind: u64,
|
||||
created_at: u64,
|
||||
) -> Event {
|
||||
let secp = Secp256k1::new();
|
||||
let key_pair = KeyPair::new(&secp, &mut rand::thread_rng());
|
||||
let public_key = XOnlyPublicKey::from_keypair(&key_pair);
|
||||
|
||||
let mut tags: Vec<Vec<String>> = vec![];
|
||||
|
||||
if let Some(c) = challenge {
|
||||
let tag = vec!["challenge".into(), c.into()];
|
||||
tags.push(tag);
|
||||
}
|
||||
|
||||
if let Some(r) = relay {
|
||||
let tag = vec!["relay".into(), r.into()];
|
||||
tags.push(tag);
|
||||
}
|
||||
|
||||
let mut event = Event {
|
||||
id: "0".to_owned(),
|
||||
pubkey: public_key.to_hex(),
|
||||
delegated_by: None,
|
||||
created_at: created_at,
|
||||
kind: kind,
|
||||
tags: tags,
|
||||
content: "".to_owned(),
|
||||
sig: "0".to_owned(),
|
||||
tagidx: None,
|
||||
};
|
||||
|
||||
let c = event.to_canonical().unwrap();
|
||||
let digest: sha256::Hash = sha256::Hash::hash(c.as_bytes());
|
||||
|
||||
let msg = secp256k1::Message::from_slice(digest.as_ref()).unwrap();
|
||||
let sig = secp.sign_schnorr(&msg, &key_pair);
|
||||
|
||||
event.id = format!("{digest:x}");
|
||||
event.sig = sig.to_hex();
|
||||
|
||||
event
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user