Management API started (handles request, checks auth, does not service yet)

This commit is contained in:
Mike Dilger 2024-06-24 10:49:09 +12:00
parent a615fd50c4
commit cb2bcc9b40
4 changed files with 201 additions and 0 deletions

View File

@ -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<serde_json::Error> for Error {
}
}
}
impl From<std::string::FromUtf8Error> for Error {
#[track_caller]
fn from(err: std::string::FromUtf8Error) -> Self {
Error {
inner: ChorusError::FromUtf8(err),
location: std::panic::Location::caller(),
}
}
}
impl From<base64::DecodeError> for Error {
#[track_caller]
fn from(err: base64::DecodeError) -> Self {
Error {
inner: ChorusError::Base64Decode(err),
location: std::panic::Location::caller(),
}
}
}

118
src/web/management/auth.rs Normal file
View File

@ -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<Value, Error> {
Err(ChorusError::ManagementAuthFailure(s.to_owned()).into())
}
pub async fn check_auth(request: Request<Incoming>) -> Result<Value, Error> {
// 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 = <sha256::Hash as AsRef<[u8]>>::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)?)
}

44
src/web/management/mod.rs Normal file
View File

@ -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<Incoming>,
) -> Result<Response<Full<Bytes>>, 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<Response<Full<Bytes>>, 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)
}

View File

@ -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;
}
}
}