Implement AUTH (NIP-42)

This commit is contained in:
Mike Dilger 2024-02-18 08:03:22 +13:00
parent 19c8707850
commit e1f29354be
3 changed files with 82 additions and 5 deletions

View File

@ -16,7 +16,7 @@ use crate::globals::GLOBALS;
use crate::reply::NostrReply;
use crate::store::Store;
use crate::tls::MaybeTlsStream;
use crate::types::OwnedFilter;
use crate::types::{OwnedFilter, Pubkey};
use futures::{sink::SinkExt, stream::StreamExt};
use hyper::service::Service;
use hyper::upgrade::Upgraded;
@ -31,6 +31,7 @@ use std::io::Read;
use std::net::SocketAddr;
use std::pin::Pin;
use std::task::{Context, Poll};
use textnonce::TextNonce;
use tokio::net::{TcpListener, TcpStream};
use tungstenite::protocol::WebSocketConfig;
use tungstenite::Message;
@ -176,6 +177,8 @@ async fn handle_http_request(
// We start with a 1-page buffer, and grow it if needed.
buffer: vec![0; 4096],
websocket,
challenge: TextNonce::new().into_string(),
user: None,
};
// Handle the websocket
@ -219,6 +222,8 @@ struct WebSocketService {
pub subscriptions: HashMap<String, Vec<OwnedFilter>>,
pub buffer: Vec<u8>,
pub websocket: WebSocketStream<Upgraded>,
pub challenge: String,
pub user: Option<Pubkey>,
}
impl WebSocketService {
@ -226,6 +231,10 @@ impl WebSocketService {
// Subscribe to the new_events broadcast channel
let mut new_events = GLOBALS.new_events.subscribe();
// Offer AUTH to clients right off the bat
let reply = NostrReply::Auth(self.challenge.clone());
self.websocket.send(Message::text(reply.as_json())).await?;
loop {
tokio::select! {
message_option = self.websocket.next() => {

View File

@ -3,10 +3,11 @@ use crate::globals::GLOBALS;
use crate::reply::{NostrReply, NostrReplyPrefix};
use crate::types::parse::json_escape::json_unescape;
use crate::types::parse::json_parse::*;
use crate::types::{Event, Filter, Kind, OwnedFilter};
use crate::types::{Event, Filter, Kind, OwnedFilter, Time};
use crate::WebSocketService;
use futures::SinkExt;
use hyper_tungstenite::tungstenite::Message;
use url::Url;
impl WebSocketService {
pub async fn handle_nostr_message(&mut self, msg: String) -> Result<(), Error> {
@ -182,11 +183,76 @@ impl WebSocketService {
Ok(())
}
pub async fn auth(&mut self, msg: String, _inpos: usize) -> Result<(), Error> {
let _input = msg.as_bytes();
pub async fn auth(&mut self, msg: String, mut inpos: usize) -> Result<(), Error> {
let input = msg.as_bytes();
let reply = NostrReply::Notice("AUTH is not yet supported".to_owned());
eat_whitespace(input, &mut inpos);
verify_char(input, b',', &mut inpos)?;
eat_whitespace(input, &mut inpos);
// Read the event into the session buffer
let (_incount, event) = Event::from_json(&input[inpos..], &mut self.buffer)?;
// Verify the event (even if config.verify_events is off, because this is
// strictly necessary for AUTH)
event.verify()?;
// Verify the event is the right kind
if event.kind() != Kind(22242) {
return Err(ChorusError::AuthFailure.into());
}
// Verify the challenge and relay tags
let mut challenge_ok = false;
let mut relay_ok = false;
for mut tag in event.tags()?.iter() {
match tag.next() {
Some(b"relay") => {
if let Some(value) = tag.next() {
// We check if the URL host matches
// (when normalized, puny-encoded IDNA, etc)
let utf8value = std::str::from_utf8(value)?;
let url = match Url::parse(utf8value) {
Ok(u) => u,
Err(_) => return Err(ChorusError::AuthFailure.into()),
};
if let Some(h) = url.host() {
let theirhost = h.to_owned();
if theirhost == GLOBALS.config.read().await.hostname {
relay_ok = true;
}
}
}
}
Some(b"challenge") => {
if let Some(value) = tag.next() {
if value == self.challenge.as_bytes() {
challenge_ok = true;
}
}
}
None => break,
_ => continue,
}
}
if !(challenge_ok && relay_ok) {
return Err(ChorusError::AuthFailure.into());
}
// Verify the created_at timestamp is within reason
let timediff = (Time::now().0 as i64).abs_diff(event.created_at().0 as i64);
if timediff < 600 {
return Err(ChorusError::AuthFailure.into());
}
// They are now authenticated
self.user = Some(event.pubkey());
// Confirm the AUTH
let reply = NostrReply::Ok(event.id(), true, NostrReplyPrefix::None, "".to_string());
self.websocket.send(Message::text(reply.as_json())).await?;
Ok(())
}
}

View File

@ -30,6 +30,7 @@ impl fmt::Display for NostrReplyPrefix {
}
pub enum NostrReply<'a> {
Auth(String),
Event(&'a str, Event<'a>),
Ok(Id, bool, NostrReplyPrefix, String),
Eose(&'a str),
@ -40,6 +41,7 @@ pub enum NostrReply<'a> {
impl NostrReply<'_> {
pub fn as_json(&self) -> String {
match self {
NostrReply::Auth(challenge) => format!(r#"["AUTH", "{challenge}"]"#),
NostrReply::Event(subid, event) => format!(r#"["EVENT", "{subid}", {}]"#, event),
NostrReply::Ok(id, ok, prefix, msg) => format!(r#"["OK","{id}",{ok},"{prefix}{msg}"]"#),
NostrReply::Eose(subid) => format!(r#"["EOSE","{subid}"]"#),