diff --git a/src/error.rs b/src/error.rs index 29b8f3f..78a0e0a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -34,6 +34,9 @@ pub enum ChorusError { // User is banned BannedUser, + // Base64 Decode Error + Base64Decode(base64::DecodeError), + // Channel Recv ChannelRecv(tokio::sync::broadcast::error::RecvError), @@ -52,6 +55,9 @@ pub enum ChorusError { // Event is Invalid EventIsInvalid(String), + // From UTF8 + FromUtf8(std::string::FromUtf8Error), + // Http Http(hyper::http::Error), @@ -67,6 +73,9 @@ pub enum ChorusError { // I/O Io(std::io::Error), + // Management Authorization failure + ManagementAuthFailure(String), + // Missing Table MissingTable(&'static str), @@ -132,17 +141,20 @@ impl std::fmt::Display for ChorusError { ChorusError::AuthRequired => write!(f, "AUTH required"), ChorusError::BannedEvent => write!(f, "Event is banned"), ChorusError::BannedUser => write!(f, "User is banned"), + ChorusError::Base64Decode(e) => write!(f, "{e}"), ChorusError::ChannelRecv(e) => write!(f, "{e}"), ChorusError::ChannelSend(e) => write!(f, "{e}"), ChorusError::Config(e) => write!(f, "{e}"), ChorusError::Crypto(e) => write!(f, "{e}"), ChorusError::ErrorClose => write!(f, "Closing due to error(s)"), ChorusError::EventIsInvalid(s) => write!(f, "Event is invalid: {s}"), + ChorusError::FromUtf8(e) => write!(f, "{e}"), ChorusError::Http(e) => write!(f, "{e}"), ChorusError::Hyper(e) => write!(f, "{e}"), ChorusError::InvalidUri(e) => write!(f, "{e}"), ChorusError::InvalidUriParts(e) => write!(f, "{e}"), ChorusError::Io(e) => write!(f, "{e}"), + ChorusError::ManagementAuthFailure(s) => write!(f, "Authorization failure: {s}"), ChorusError::MissingTable(t) => write!(f, "Missing table: {t}"), ChorusError::NoPrivateKey => write!(f, "Private Key Not Found"), ChorusError::NoSuchSubscription => write!(f, "No such subscription"), @@ -199,17 +211,20 @@ impl ChorusError { ChorusError::AuthRequired => 0.0, ChorusError::BannedEvent => 0.1, ChorusError::BannedUser => 0.2, + ChorusError::Base64Decode(_) => 0.0, ChorusError::ChannelRecv(_) => 0.0, ChorusError::ChannelSend(_) => 0.0, ChorusError::Config(_) => 0.0, ChorusError::Crypto(_) => 0.1, ChorusError::ErrorClose => 1.0, ChorusError::EventIsInvalid(_) => 0.2, + ChorusError::FromUtf8(_) => 0.2, ChorusError::Http(_) => 0.0, ChorusError::Hyper(_) => 0.0, ChorusError::InvalidUri(_) => 0.0, ChorusError::InvalidUriParts(_) => 0.0, ChorusError::Io(_) => 0.0, + ChorusError::ManagementAuthFailure(_) => 0.0, ChorusError::MissingTable(_) => 0.0, ChorusError::NoPrivateKey => 0.0, ChorusError::NoSuchSubscription => 0.05, @@ -439,3 +454,23 @@ impl From for Error { } } } + +impl From for Error { + #[track_caller] + fn from(err: std::string::FromUtf8Error) -> Self { + Error { + inner: ChorusError::FromUtf8(err), + location: std::panic::Location::caller(), + } + } +} + +impl From for Error { + #[track_caller] + fn from(err: base64::DecodeError) -> Self { + Error { + inner: ChorusError::Base64Decode(err), + location: std::panic::Location::caller(), + } + } +} diff --git a/src/web/management/auth.rs b/src/web/management/auth.rs new file mode 100644 index 0000000..db0aa6a --- /dev/null +++ b/src/web/management/auth.rs @@ -0,0 +1,118 @@ +use crate::error::{ChorusError, Error}; +use crate::globals::GLOBALS; +use base64::prelude::*; +use http_body_util::BodyExt; +use hyper::body::Incoming; +use hyper::Request; +use pocket_types::Event; +use secp256k1::hashes::{sha256, Hash}; +use serde_json::Value; + +fn s_err(s: &str) -> Result { + Err(ChorusError::ManagementAuthFailure(s.to_owned()).into()) +} + +pub async fn check_auth(request: Request) -> Result { + // Must be POST + if request.method() != hyper::Method::POST { + return s_err("Management RPC only supports POST method"); + } + + // Must have AUTHORIZATION header + let authz = match request.headers().get("Authorization") { + Some(h) => h, + None => return s_err("Authorization header not of type nostr"), + }; + + // Authorization header must be type "nostr" + let value = String::from_utf8(authz.as_bytes().to_owned())?; + let mut parts = value.split(|b| b == ' '); + match parts.next() { + Some(s) => { + if s.to_lowercase() != "nostr" { + return s_err("Authorization header not of type nostr"); + } + } + None => return s_err("Authorization header missing"), + } + + // Authorization header second part + let base64event = match parts.next() { + Some(s) => s, + None => return s_err("Authorization header incomplete"), + }; + + // Authorization header must be base64 + let event_bytes = BASE64_STANDARD.decode(base64event)?; + + // Authorization header base64 must decode to a nostr Event + let mut buffer = Vec::with_capacity(base64event.len()); + buffer.resize(base64event.len(), 0); + let (_size, event) = Event::from_json(&event_bytes, &mut buffer)?; + + // Nostr event must be valid + if let Err(e) = event.verify() { + return s_err(&format!("Authorization event is invalid: {}", e)); + } + + // Event kind must be 27235 + if event.kind().as_u16() != 27235 { + return s_err("Authorization event not kind 27235"); + } + + // Event created_at must be within 60 seconds of now + use pocket_types::Time; + let now = Time::now().as_u64(); + if event.created_at().as_u64() > now + 60 { + return s_err("Authorization event too far in the future"); + } + if event.created_at().as_u64() < now - 60 { + return s_err("Authorization event too far in the past"); + } + + let tags = event.tags()?; + + // Tag 'method' must be POST + if let Some(method) = tags.get_value(b"method") { + if method != b"POST" && method != b"post" { + return s_err( + "Authorization event did not authorize POST (and only POST is supported here)", + ); + } + } else { + return s_err("Authorization event method missing"); + } + + // Tag 'u' must be the current URL + if let Some(u) = tags.get_value(b"u") { + let auth_url = String::from_utf8(u.to_owned())?; + let actual_url = { + let uri = GLOBALS.config.read().url(request.uri().to_owned(), true)?; + format!("{}", uri) + }; + + if actual_url != auth_url { + return s_err(&format!( + "Authorization event URL {} does not match requqest URL {}", + auth_url, actual_url + )); + } + } else { + return s_err("Authorization event URL missing"); + } + + let body = request.collect().await?.to_bytes(); + let hash = sha256::Hash::hash(&body); + let hashref = >::as_ref(&hash); + let hashrefhex = hex::encode(hashref); + + if let Some(payload) = tags.get_value(b"payload") { + if hashrefhex.as_bytes() != payload { + return s_err("Authorization failed: body hash mismatch"); + } + } else { + return s_err("Authorization event payload missing"); + } + + Ok(serde_json::from_slice(&body)?) +} diff --git a/src/web/management/mod.rs b/src/web/management/mod.rs new file mode 100644 index 0000000..03f4601 --- /dev/null +++ b/src/web/management/mod.rs @@ -0,0 +1,44 @@ +use crate::error::Error; +use crate::ip::HashedPeer; +use http_body_util::Full; +use hyper::body::{Bytes, Incoming}; +use hyper::{Request, Response, StatusCode}; +use serde_json::{json, Value}; + +mod auth; + +pub async fn handle( + _peer: HashedPeer, + request: Request, +) -> Result>, Error> { + let command: Value = match auth::check_auth(request).await { + Ok(v) => v, + Err(e) => { + let result = json!({ + "result": {}, + "error": format!("{}", e) + }); + return respond(result, StatusCode::UNAUTHORIZED); + } + }; + + println!("command was {}", command); + + let result = json!({ + "result": {}, + "error": "The Management API is not yet implemented" + }); + respond(result, StatusCode::NOT_IMPLEMENTED) +} + +fn respond(json: serde_json::Value, status: StatusCode) -> Result>, Error> { + let s: String = serde_json::to_string(&json)?; + let response = Response::builder() + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Headers", "*") + .header("Access-Control-Allow-Methods", "*") + .header("Content-Type", "application/nostr+json") + .status(status) + .body(s.into_bytes().into())?; + Ok(response) +} diff --git a/src/web/mod.rs b/src/web/mod.rs index 8d8c68e..66baa09 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -1,3 +1,4 @@ +mod management; mod nip11; use crate::error::Error; @@ -16,6 +17,9 @@ pub async fn serve_http( if s == "application/nostr+json" { return nip11::serve_nip11(peer).await; } + if s == "application/nostr+json+rpc" { + return management::handle(peer, request).await; + } } }