mirror of
https://github.com/scsibug/nostr-rs-relay.git
synced 2024-11-09 21: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",
|
||||||
"tracing-subscriber 0.2.25",
|
"tracing-subscriber 0.2.25",
|
||||||
"tungstenite",
|
"tungstenite",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ chrono = "0.4.23"
|
||||||
prometheus = "0.13.3"
|
prometheus = "0.13.3"
|
||||||
indicatif = "0.17.3"
|
indicatif = "0.17.3"
|
||||||
bech32 = "0.9.1"
|
bech32 = "0.9.1"
|
||||||
|
url = "2.3.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = "1"
|
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_)
|
- [ ] 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-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-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
|
## Quick Start
|
||||||
|
|
||||||
|
|
|
@ -136,6 +136,8 @@ reject_future_seconds = 1800
|
||||||
# "35d26e4690cbe1a898af61cc3515661eb5fa763b57bd0b42e45099c8b32fd50f",
|
# "35d26e4690cbe1a898af61cc3515661eb5fa763b57bd0b42e45099c8b32fd50f",
|
||||||
# "887645fef0ce0c3c1218d2f5d8e6132a19304cdc57cd20281d082f38cfea0072",
|
# "887645fef0ce0c3c1218d2f5d8e6132a19304cdc57cd20281d082f38cfea0072",
|
||||||
#]
|
#]
|
||||||
|
# Enable NIP-42 authentication
|
||||||
|
#nip42_auth = false
|
||||||
|
|
||||||
[verified_users]
|
[verified_users]
|
||||||
# NIP-05 verification of users. Can be "enabled" to require NIP-05
|
# NIP-05 verification of users. Can be "enabled" to require NIP-05
|
||||||
|
|
|
@ -75,6 +75,7 @@ pub struct Limits {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub struct Authorization {
|
pub struct Authorization {
|
||||||
pub pubkey_whitelist: Option<Vec<String>>, // If present, only allow these pubkeys to publish events
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
@ -250,6 +251,7 @@ impl Default for Settings {
|
||||||
},
|
},
|
||||||
authorization: Authorization {
|
authorization: Authorization {
|
||||||
pubkey_whitelist: None, // Allow any address to publish
|
pubkey_whitelist: None, // Allow any address to publish
|
||||||
|
nip42_auth: false, // Disable NIP-42 authentication
|
||||||
},
|
},
|
||||||
verified_users: VerifiedUsers {
|
verified_users: VerifiedUsers {
|
||||||
mode: VerifiedUsersMode::Disabled,
|
mode: VerifiedUsersMode::Disabled,
|
||||||
|
|
124
src/conn.rs
124
src/conn.rs
|
@ -1,16 +1,30 @@
|
||||||
//! Client connection state
|
//! Client connection state
|
||||||
use crate::close::Close;
|
|
||||||
use crate::error::Error;
|
|
||||||
use crate::error::Result;
|
|
||||||
|
|
||||||
use crate::subscription::Subscription;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
use uuid::Uuid;
|
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
|
/// A subscription identifier has a maximum length
|
||||||
const MAX_SUBSCRIPTION_ID_LEN: usize = 256;
|
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
|
/// State for a client connection
|
||||||
pub struct ClientConn {
|
pub struct ClientConn {
|
||||||
/// Client IP (either from socket, or configured proxy header
|
/// Client IP (either from socket, or configured proxy header
|
||||||
|
@ -21,6 +35,8 @@ pub struct ClientConn {
|
||||||
subscriptions: HashMap<String, Subscription>,
|
subscriptions: HashMap<String, Subscription>,
|
||||||
/// Per-connection maximum concurrent subscriptions
|
/// Per-connection maximum concurrent subscriptions
|
||||||
max_subs: usize,
|
max_subs: usize,
|
||||||
|
/// NIP-42 AUTH
|
||||||
|
auth: Nip42AuthState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ClientConn {
|
impl Default for ClientConn {
|
||||||
|
@ -39,15 +55,18 @@ impl ClientConn {
|
||||||
client_id,
|
client_id,
|
||||||
subscriptions: HashMap::new(),
|
subscriptions: HashMap::new(),
|
||||||
max_subs: 32,
|
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
|
&self.subscriptions
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the given subscription already exists
|
/// 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)
|
self.subscriptions.values().any(|x| x == sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +82,22 @@ impl ClientConn {
|
||||||
&self.client_ip_addr
|
&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.
|
/// Add a new subscription for this connection.
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
|
@ -116,4 +151,79 @@ impl ClientConn {
|
||||||
self.get_client_prefix(),
|
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 source_ip: String,
|
||||||
pub origin: Option<String>,
|
pub origin: Option<String>,
|
||||||
pub user_agent: Option<String>,
|
pub user_agent: Option<String>,
|
||||||
|
pub auth_pubkey: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Database file
|
/// Database file
|
||||||
|
@ -240,7 +241,7 @@ pub async fn db_writer(
|
||||||
if let Some(ref mut c) = grpc_client {
|
if let Some(ref mut c) = grpc_client {
|
||||||
trace!("checking if grpc permits");
|
trace!("checking if grpc permits");
|
||||||
let grpc_start = Instant::now();
|
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 {
|
match decision_res {
|
||||||
Ok(decision) => {
|
Ok(decision) => {
|
||||||
if !decision.permitted() {
|
if !decision.permitted() {
|
||||||
|
|
|
@ -68,6 +68,8 @@ pub enum Error {
|
||||||
AuthzError,
|
AuthzError,
|
||||||
#[error("Tonic GRPC error")]
|
#[error("Tonic GRPC error")]
|
||||||
TonicError(tonic::Status),
|
TonicError(tonic::Status),
|
||||||
|
#[error("Invalid AUTH message")]
|
||||||
|
AuthFailure,
|
||||||
#[error("Unknown/Undocumented")]
|
#[error("Unknown/Undocumented")]
|
||||||
UnknownError,
|
UnknownError,
|
||||||
}
|
}
|
||||||
|
|
20
src/event.rs
20
src/event.rs
|
@ -14,6 +14,8 @@ use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
use crate::event::EventWrapper::WrappedEvent;
|
||||||
|
use crate::event::EventWrapper::WrappedAuth;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// Secp256k1 verification instance.
|
/// Secp256k1 verification instance.
|
||||||
|
@ -83,17 +85,27 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub enum EventWrapper {
|
||||||
|
WrappedEvent(Event),
|
||||||
|
WrappedAuth(Event)
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert network event to parsed/validated event.
|
/// Convert network event to parsed/validated event.
|
||||||
impl From<EventCmd> for Result<Event> {
|
impl From<EventCmd> for Result<EventWrapper> {
|
||||||
fn from(ec: EventCmd) -> Result<Event> {
|
fn from(ec: EventCmd) -> Result<EventWrapper> {
|
||||||
// ensure command is correct
|
// ensure command is correct
|
||||||
if ec.cmd == "EVENT" {
|
if ec.cmd == "EVENT" {
|
||||||
ec.event.validate().map(|_| {
|
ec.event.validate().map(|_| {
|
||||||
let mut e = ec.event;
|
let mut e = ec.event;
|
||||||
e.build_index();
|
e.build_index();
|
||||||
e.update_delegation();
|
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 {
|
} else {
|
||||||
Err(CommandUnknownError)
|
Err(CommandUnknownError)
|
||||||
}
|
}
|
||||||
|
@ -326,7 +338,7 @@ impl Event {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert event to canonical representation for signing.
|
/// 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
|
// create a JsonValue for each event element
|
||||||
let mut c: Vec<Value> = vec![];
|
let mut c: Vec<Value> = vec![];
|
||||||
// id must be set to 0
|
// id must be set to 0
|
||||||
|
|
17
src/info.rs
17
src/info.rs
|
@ -1,6 +1,6 @@
|
||||||
//! Relay metadata using NIP-11
|
//! Relay metadata using NIP-11
|
||||||
/// Relay Info
|
/// Relay Info
|
||||||
use crate::config;
|
use crate::config::Settings;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
|
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
|
/// Convert an Info configuration into public Relay Info
|
||||||
impl From<config::Info> for RelayInfo {
|
impl From<Settings> for RelayInfo {
|
||||||
fn from(i: config::Info) -> Self {
|
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 {
|
RelayInfo {
|
||||||
id: i.relay_url,
|
id: i.relay_url,
|
||||||
name: i.name,
|
name: i.name,
|
||||||
description: i.description,
|
description: i.description,
|
||||||
pubkey: i.pubkey,
|
pubkey: i.pubkey,
|
||||||
contact: i.contact,
|
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()),
|
software: Some("https://git.sr.ht/~gheartsfield/nostr-rs-relay".to_owned()),
|
||||||
version: CARGO_PKG_VERSION.map(std::borrow::ToOwned::to_owned),
|
version: CARGO_PKG_VERSION.map(std::borrow::ToOwned::to_owned),
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,7 @@ impl EventAuthzService {
|
||||||
origin: Option<String>,
|
origin: Option<String>,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
nip05: Option<Nip05Name>,
|
nip05: Option<Nip05Name>,
|
||||||
|
auth_pubkey: Option<Vec<u8>>
|
||||||
) -> Result<Box<dyn AuthzDecision>> {
|
) -> Result<Box<dyn AuthzDecision>> {
|
||||||
self.ready_connection().await;
|
self.ready_connection().await;
|
||||||
let id_blob = hex::decode(&event.id)?;
|
let id_blob = hex::decode(&event.id)?;
|
||||||
|
@ -97,7 +98,7 @@ impl EventAuthzService {
|
||||||
ip_addr: Some(ip.to_string()),
|
ip_addr: Some(ip.to_string()),
|
||||||
origin,
|
origin,
|
||||||
user_agent,
|
user_agent,
|
||||||
auth_pubkey: None,
|
auth_pubkey,
|
||||||
nip05: nip05.map(|x| nauthz_grpc::event_request::Nip05Name::from(x)),
|
nip05: nip05.map(|x| nauthz_grpc::event_request::Nip05Name::from(x)),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -16,6 +16,7 @@ pub struct EventResult {
|
||||||
pub enum Notice {
|
pub enum Notice {
|
||||||
Message(String),
|
Message(String),
|
||||||
EventResult(EventResult),
|
EventResult(EventResult),
|
||||||
|
AuthChallenge(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventResultStatus {
|
impl EventResultStatus {
|
||||||
|
|
|
@ -7,6 +7,8 @@ use crate::repo::NostrRepo;
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::db::SubmittedEvent;
|
use crate::db::SubmittedEvent;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
use crate::event::EventWrapper;
|
||||||
|
use crate::server::EventWrapper::{WrappedAuth, WrappedEvent};
|
||||||
use crate::event::Event;
|
use crate::event::Event;
|
||||||
use crate::event::EventCmd;
|
use crate::event::EventCmd;
|
||||||
use crate::info::RelayInfo;
|
use crate::info::RelayInfo;
|
||||||
|
@ -48,6 +50,7 @@ use tungstenite::error::Error as WsError;
|
||||||
use tungstenite::handshake;
|
use tungstenite::handshake;
|
||||||
use tungstenite::protocol::Message;
|
use tungstenite::protocol::Message;
|
||||||
use tungstenite::protocol::WebSocketConfig;
|
use tungstenite::protocol::WebSocketConfig;
|
||||||
|
use crate::server::Error::CommandUnknownError;
|
||||||
|
|
||||||
/// Handle arbitrary HTTP requests, including for `WebSocket` upgrades.
|
/// Handle arbitrary HTTP requests, including for `WebSocket` upgrades.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
@ -157,7 +160,7 @@ async fn handle_web_request(
|
||||||
if mt_str.contains("application/nostr+json") {
|
if mt_str.contains("application/nostr+json") {
|
||||||
// build a relay info response
|
// build a relay info response
|
||||||
debug!("Responding to server info request");
|
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());
|
let b = Body::from(serde_json::to_string_pretty(&rinfo).unwrap());
|
||||||
return Ok(Response::builder()
|
return Ok(Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
|
@ -268,6 +271,10 @@ fn create_metrics() -> (Registry, NostrMetrics) {
|
||||||
"nostr_cmd_close_total",
|
"nostr_cmd_close_total",
|
||||||
"CLOSE commands",
|
"CLOSE commands",
|
||||||
)).unwrap();
|
)).unwrap();
|
||||||
|
let cmd_auth = IntCounter::with_opts(Opts::new(
|
||||||
|
"nostr_cmd_auth_total",
|
||||||
|
"AUTH commands",
|
||||||
|
)).unwrap();
|
||||||
let disconnects = IntCounterVec::new(
|
let disconnects = IntCounterVec::new(
|
||||||
Opts::new("nostr_disconnects_total", "Client disconnects"),
|
Opts::new("nostr_disconnects_total", "Client disconnects"),
|
||||||
vec!["reason"].as_slice(),
|
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_req.clone())).unwrap();
|
||||||
registry.register(Box::new(cmd_event.clone())).unwrap();
|
registry.register(Box::new(cmd_event.clone())).unwrap();
|
||||||
registry.register(Box::new(cmd_close.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();
|
registry.register(Box::new(disconnects.clone())).unwrap();
|
||||||
let metrics = NostrMetrics {
|
let metrics = NostrMetrics {
|
||||||
query_sub,
|
query_sub,
|
||||||
|
@ -295,6 +303,7 @@ fn create_metrics() -> (Registry, NostrMetrics) {
|
||||||
cmd_req,
|
cmd_req,
|
||||||
cmd_event,
|
cmd_event,
|
||||||
cmd_close,
|
cmd_close,
|
||||||
|
cmd_auth,
|
||||||
};
|
};
|
||||||
(registry,metrics)
|
(registry,metrics)
|
||||||
}
|
}
|
||||||
|
@ -488,7 +497,7 @@ pub fn start_server(settings: &Settings, shutdown_rx: MpscReceiver<()>) -> Resul
|
||||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, Debug)]
|
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, Debug)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum NostrMessage {
|
pub enum NostrMessage {
|
||||||
/// An `EVENT` message
|
/// `EVENT` and `AUTH` messages
|
||||||
EventMsg(EventCmd),
|
EventMsg(EventCmd),
|
||||||
/// A `REQ` message
|
/// A `REQ` message
|
||||||
SubMsg(Subscription),
|
SubMsg(Subscription),
|
||||||
|
@ -528,6 +537,7 @@ fn make_notice_message(notice: &Notice) -> Message {
|
||||||
let json = match notice {
|
let json = match notice {
|
||||||
Notice::Message(ref msg) => json!(["NOTICE", msg]),
|
Notice::Message(ref msg) => json!(["NOTICE", msg]),
|
||||||
Notice::EventResult(ref res) => json!(["OK", res.id, res.status.to_bool(), res.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())
|
Message::text(json.to_string())
|
||||||
|
@ -616,6 +626,14 @@ async fn nostr_server(
|
||||||
// Measure connections
|
// Measure connections
|
||||||
metrics.connections.inc();
|
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 {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = shutdown.recv() => {
|
_ = shutdown.recv() => {
|
||||||
|
@ -729,16 +747,17 @@ async fn nostr_server(
|
||||||
// An EventCmd needs to be validated to be converted into an Event
|
// An EventCmd needs to be validated to be converted into an Event
|
||||||
// handle each type of message
|
// handle each type of message
|
||||||
let evid = ec.event_id().to_owned();
|
let evid = ec.event_id().to_owned();
|
||||||
let parsed : Result<Event> = Result::<Event>::from(ec);
|
let parsed : Result<EventWrapper> = Result::<EventWrapper>::from(ec);
|
||||||
metrics.cmd_event.inc();
|
|
||||||
match parsed {
|
match parsed {
|
||||||
Ok(e) => {
|
Ok(WrappedEvent(e)) => {
|
||||||
|
metrics.cmd_event.inc();
|
||||||
let id_prefix:String = e.id.chars().take(8).collect();
|
let id_prefix:String = e.id.chars().take(8).collect();
|
||||||
debug!("successfully parsed/validated event: {:?} (cid: {}, kind: {})", id_prefix, cid, e.kind);
|
debug!("successfully parsed/validated event: {:?} (cid: {}, kind: {})", id_prefix, cid, e.kind);
|
||||||
// check if the event is too far in the future.
|
// check if the event is too far in the future.
|
||||||
if e.is_valid_timestamp(settings.options.reject_future_seconds) {
|
if e.is_valid_timestamp(settings.options.reject_future_seconds) {
|
||||||
// Write this to the database.
|
// 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();
|
event_tx.send(submit_event).await.ok();
|
||||||
client_published_event_count += 1;
|
client_published_event_count += 1;
|
||||||
} else {
|
} 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) => {
|
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);
|
info!("client sent an invalid event (cid: {})", cid);
|
||||||
ws_stream.send(make_notice_message(&Notice::invalid(evid, &format!("{e}")))).await.ok();
|
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_req: IntCounter, // count of REQ commands received
|
||||||
pub cmd_event: IntCounter, // count of EVENT commands received
|
pub cmd_event: IntCounter, // count of EVENT commands received
|
||||||
pub cmd_close: IntCounter, // count of CLOSE 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
|
//! Common utility functions
|
||||||
use bech32::FromBase32;
|
use bech32::FromBase32;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
/// Seconds since 1970.
|
/// Seconds since 1970.
|
||||||
#[must_use] pub fn unix_time() -> u64 {
|
#[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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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