diff --git a/src/web/blossom/mod.rs b/src/web/blossom/mod.rs new file mode 100644 index 0000000..f0892bc --- /dev/null +++ b/src/web/blossom/mod.rs @@ -0,0 +1,136 @@ +use crate::error::{ChorusError, Error}; +use http::header::{ + ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, + ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, ALLOW, CONTENT_LENGTH, ORIGIN, + WWW_AUTHENTICATE, +}; +use http::{Method, StatusCode}; +//ACCEPT, AUTHORIZATION, CONTENT_TYPE, DATE, ETAG, ORIGIN +use http_body_util::combinators::BoxBody; +use http_body_util::{BodyExt, Empty}; +use hyper::body::{Bytes, Incoming}; +use hyper::{Request, Response}; + +pub async fn handle(request: &Request) -> Result>, Error> { + match route(request).await { + Ok(response) => Ok(response), + Err(e) => match e.inner { + ChorusError::SignalNotBlossom => Err(e), + _ => error_response(e), + }, + } +} + +pub async fn route(request: &Request) -> Result>, Error> { + let p = request.uri().path(); + if p.starts_with("/") + && p.len() >= 1 + 64 + && p.chars().skip(1).take(64).all(|c| c.is_ascii_hexdigit()) + { + handle_hash(request).await + } else if p == "/upload" { + handle_upload(request).await + } else if p.starts_with("/list/") + && p.len() >= 6 + 64 + && p.chars().skip(6).take(64).all(|c| c.is_ascii_hexdigit()) + { + handle_list(request).await + } else if p == "/mirror" { + handle_mirror(request).await + } else { + Err(ChorusError::SignalNotBlossom.into()) + } +} + +fn error_response(e: Error) -> Result>, Error> { + use std::io::ErrorKind; + + let mut response = Response::builder().header(ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + + let (status, reason) = match e.inner { + ChorusError::BlossomAuthFailure(m) => { + response = response.header(WWW_AUTHENTICATE, "Nostr"); + (StatusCode::UNAUTHORIZED, m) + } + ChorusError::FromHex(_) => (StatusCode::BAD_REQUEST, format!("{e}")), + ChorusError::Io(ref ioerror) => match ioerror.kind() { + ErrorKind::NotFound => (StatusCode::NOT_FOUND, "Not Found".to_owned()), + _ => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")), + }, + _ => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")), + }; + + Ok(response + .header("X-Reason", reason) + .status(status) + .body(Empty::new().map_err(|e| e.into()).boxed())?) +} + +fn options_response( + request: &Request, + methods: &str, +) -> Result>, Error> { + if request + .headers() + .contains_key(ACCESS_CONTROL_REQUEST_HEADERS) + || request + .headers() + .contains_key(ACCESS_CONTROL_REQUEST_METHOD) + || request.headers().contains_key(ORIGIN) + { + // CORS OPTIONS response + Ok(Response::builder() + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .header(ACCESS_CONTROL_ALLOW_HEADERS, "Authorization, *") + .header(ACCESS_CONTROL_ALLOW_METHODS, methods) + .header(CONTENT_LENGTH, "0") + .status(StatusCode::OK) + .body(Empty::new().map_err(|e| e.into()).boxed())?) + } else { + // Normal OPTIONS response + Ok(Response::builder() + .header(ALLOW, methods) + .status(StatusCode::NO_CONTENT) + .body(Empty::new().map_err(|e| e.into()).boxed())?) + } +} + +pub async fn handle_hash( + request: &Request, +) -> Result>, Error> { + if matches!(request.method(), &Method::OPTIONS) { + return options_response(request, "OPTIONS, HEAD, GET, DELETE"); + } + + unimplemented!() +} + +pub async fn handle_upload( + request: &Request, +) -> Result>, Error> { + if matches!(request.method(), &Method::OPTIONS) { + return options_response(request, "OPTIONS, HEAD, PUT"); + } + + unimplemented!() +} + +pub async fn handle_list( + request: &Request, +) -> Result>, Error> { + if matches!(request.method(), &Method::OPTIONS) { + return options_response(request, "OPTIONS, GET"); + } + + unimplemented!() +} + +pub async fn handle_mirror( + request: &Request, +) -> Result>, Error> { + if matches!(request.method(), &Method::OPTIONS) { + return options_response(request, "OPTIONS, PUT"); + } + + unimplemented!() +} diff --git a/src/web/mod.rs b/src/web/mod.rs index 1989c96..b626ccb 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -1,7 +1,9 @@ +mod blossom; mod management; mod nip11; -use crate::error::Error; +use crate::error::{ChorusError, Error}; +use crate::globals::GLOBALS; use crate::ip::HashedPeer; use http::Method; use http_body_util::combinators::BoxBody; @@ -41,6 +43,18 @@ pub async fn serve_http( } } + // Try blossom if enabled + if GLOBALS.config.read().blossom_directory.is_some() { + match blossom::handle(&request).await { + Ok(response) => return Ok(response), + Err(e) => { + if !matches!(e.inner, ChorusError::SignalNotBlossom) { + return Err(e); + } + } + } + } + log::debug!(target: "Client", "{}: HTTP request for {}", peer, request.uri()); let response = Response::builder() .header("Access-Control-Allow-Origin", "*")