mirror of
https://github.com/mikedilger/chorus.git
synced 2026-08-01 07:21:39 +00:00
Implement AUTH (NIP-42)
This commit is contained in:
parent
19c8707850
commit
e1f29354be
11
src/main.rs
11
src/main.rs
@ -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() => {
|
||||
|
||||
74
src/nostr.rs
74
src/nostr.rs
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}"]"#),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user