mirror of
https://github.com/mikedilger/chorus.git
synced 2026-07-02 07:13:21 +00:00
Management API started (handles request, checks auth, does not service yet)
This commit is contained in:
parent
a615fd50c4
commit
cb2bcc9b40
35
src/error.rs
35
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<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
118
src/web/management/auth.rs
Normal 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
44
src/web/management/mod.rs
Normal 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)
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user