puppy/bin/server/src/main.rs
Riley Apeldoorn 37acb67aa5 Major cleanup
* Rename `fetch::keys` to `fetch::signatures`
* Clean up the public api of `fetch::signatures`
* Switch from axum to hyper
* Add request signature validation (buggy, wip)
2024-04-28 23:40:37 +02:00

237 lines
8 KiB
Rust

#![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<dyn std::error::Error + Send + Sync>> {
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<hyper::body::Incoming>;
type Response<T = Full<Bytes>> = hyper::Response<T>;
/// The request handler.
async fn handle(req: Request, cfg: Config) -> Result<Response, Infallible> {
// 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<Bytes>) -> 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<const N: usize>(
status: u16,
body: Option<impl Into<Bytes>>,
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()
}
}