#![feature(try_blocks, yeet_expr)] use std::convert::Infallible; use std::net::SocketAddr; use http::request::Parts; use http_body_util::{BodyExt as _, Full}; use hyper::body::Bytes; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper_util::rt::TokioIo; use puppy::auth::{SigError, Signer}; use puppy::{auth::verify_signature, config::Config}; use puppy::fetch::signatures::Signature; use serde_json::{from_slice, json, Value}; use tokio::net::TcpListener; #[tokio::main] async fn main() { let config = Config { ap_domain: "test.piss-on.me".to_string(), wf_domain: "test.piss-on.me".to_string(), port: 1312, }; start(&config).await.unwrap(); } pub async fn start(cfg: &Config) -> Result<(), Box> { let addr = SocketAddr::from(([127, 0, 0, 1], cfg.port)); let listener = TcpListener::bind(addr).await?; // We start a loop to continuously accept incoming connections loop { let (stream, _) = listener.accept().await?; // Use an adapter to access something implementing `tokio::io` traits as if they implement // `hyper::rt` IO traits. let io = TokioIo::new(stream); let cfg = cfg.clone(); // Spawn a tokio task to serve multiple connections concurrently tokio::task::spawn(async move { // Finally, we bind the incoming connection to our `hello` service if let Err(err) = http1::Builder::new() // `service_fn` converts our function in a `Service` .serve_connection(io, service_fn(|req| handle(req, cfg.clone()))) .await { eprintln!("Error serving connection: {:?}", err); } }); } } type Request = hyper::Request; type Response> = hyper::Response; /// The request handler. async fn handle(req: Request, cfg: Config) -> Result { // We need to fetch the entire body of the request for signature validation, because that involves making // a digest of the request body in some cases. let request = { let (req, body) = req.into_parts(); let Ok(body) = body.collect().await.map(|b| b.to_bytes()) else { return Ok(error::invalid_body("Could not get request body")); }; http::Request::from_parts(req, body) }; // Simplified representation of a request, so we can pattern match on it more easily in the dispatchers. let req = make_req(&request); eprintln!("{request:?}: open"); // We'll use the path to pick where specifically to send the request. // Check request signature at the door. Even if it isn't needed for a particular endpoint, failing fast // with a clear error message will save anyone trying to get *their* signatures implementation a major // headache. let res = match verify_signature(&request, &cfg).await { // If the request was signed and the signature was accepted, they can access the protected endpoints. Ok(Some(sig)) => dispatch_signed(req, sig, cfg).await, // Unsigned requests can see a smaller subset of endpoints, most notably the verification actor. Ok(None) => dispatch_public(req, cfg).await, // If a signature was provided *but it turned out to be unverifiable*, show them the error message. Err(err) => error::bad_signature(match err { SigError::VerificationFailed { error } => format!("Verification failed: {error}"), SigError::ParseSignature { error } => format!("Failed to parse signature: {error}"), SigError::FailedToFetchKey { keyid } => format!("Failed to fetch {keyid}"), }), }; eprintln!( "{} {}: done (status: {})", request.method(), request.uri(), res.status() ); Ok(res) } // A parsed HTTP request for easy handling. struct Req<'a> { method: &'a Method, body: Bytes, // URI bits params: Vec<(&'a str, &'a str)>, path: Vec<&'a str>, } impl Req<'_> { fn path(&self) -> &[&str] { &self.path } } fn make_req<'x>(r: &'x http::Request) -> Req<'x> { let path: Vec<&str> = r .uri() .path() .split('/') .filter(|s| !s.is_empty()) .collect(); let params: Vec<(&str, &str)> = r .uri() .query() .into_iter() .flat_map(|s| s.split('&')) .filter_map(|s| s.split_once('=')) .collect(); Req { method: r.method(), body: r.body().clone(), params, path, } } use hyper::Method; const POST: &Method = &Method::POST; const GET: &Method = &Method::GET; /// Handle a signed and verified request. /// /// This function is where all requests to a protected endpoint have to go through. If the request /// was signed but does not target a protected endpoint, this function will fall back to the /// [`dispatch_public`] handler. async fn dispatch_signed(req: Req<'_>, sig: Signer, cfg: Config) -> Response { eprintln!("Dispatching signed request"); match (req.method, req.path()) { // Viewing ActivityPub objects requires a signed request, i.e. "authorized fetch". // The one exception for this is `/s/request-verifier`, which is where the request // verification actor lives. (GET, ["o", ulid]) => api::ap::serve_object(ulid), // POSTs to an actor's inbox need to be signed to prevent impersonation. (POST, ["o", ulid, "inbox"]) => with_json(&req.body, |json| { // We only handle the intermediate parsing of the json, full resolution of the // activity object will happen inside the inbox handler itself. api::ap::inbox(ulid, sig, json, cfg) }), // Try the resources for which no signature is required as well. _ => dispatch_public(req, cfg).await, } } /// Dispatch `req` to an unprotected endpoint. If the requested path does not exist, the /// function will return a 404 response. async fn dispatch_public(req: Req<'_>, cfg: Config) -> Response { eprintln!("Dispatching public request"); match (req.method, req.path()) { (GET, ["proxy"]) => api::ap::proxy(&req.params).await, (GET, [".well-known", "webfinger"]) => api::wf::resolve(&req.params, cfg), (GET, ["s", "request-verifier"]) => api::ap::serve_verifier_actor(cfg), (m, p) => { eprintln!("404: {m} {p:?}"); error::not_found() } } } fn with_json(body: &[u8], f: impl FnOnce(Value) -> Response) -> Response { let Ok(json) = from_slice(body) else { return error::invalid_body("Could not decode as JSON"); }; f(json) } /// A quick, simple way to construct a response. fn respond( status: u16, body: Option>, headers: [(&str, &str); N], ) -> Response { let mut resp = Response::<()>::builder().status(status); for (name, data) in headers { resp = resp.header(name, data); } resp.body(match body { Some(bytes) => Full::new(bytes.into()), None => Full::new(Bytes::default()), }) .unwrap() } mod api; mod error { //! Pre-baked error responses. use http_body_util::Full; use super::Response; /// 404 response. pub fn not_found() -> Response { let body = Full::new("Not found".into()); Response::<()>::builder() .status(404) .header("content-type", "text/plain") .body(body) .unwrap() } /// 403 response indicating a bad request signature. pub fn bad_signature(err: String) -> Response { let body = Full::new(err.into()); Response::<()>::builder() .status(403) .header("content-type", "text/plain") .body(body) .unwrap() } pub fn invalid_body(err: impl ToString) -> Response { let body = Full::new(err.to_string().into()); Response::<()>::builder() .status(400) .header("content-type", "text/plain") .body(body) .unwrap() } }