Riley Apeldoorn
37acb67aa5
* 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)
237 lines
8 KiB
Rust
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()
|
|
}
|
|
}
|