diff --git a/Cargo.lock b/Cargo.lock index 4371555..e3ebca0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,6 +165,70 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "base-x" version = "0.2.11" @@ -1424,6 +1488,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1718,6 +1783,29 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "jacquard-axum" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707649f378c7aaac47a267e1eb95dbcf595fca3b0c949e0e7aa742dec7093cde" +dependencies = [ + "axum", + "bytes", + "jacquard", + "jacquard-common", + "jacquard-derive", + "jacquard-identity", + "miette", + "multibase", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tower-http", + "tracing", +] + [[package]] name = "jacquard-common" version = "0.11.0" @@ -1993,6 +2081,23 @@ dependencies = [ "uuid", ] +[[package]] +name = "knotty-knot" +version = "0.0.0" +dependencies = [ + "axum", + "clap", + "jacquard", + "jacquard-axum", + "miette", + "serde", + "serde_json", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2146,6 +2251,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.1" @@ -3736,6 +3847,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -3758,6 +3870,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", "url", ] @@ -3779,6 +3892,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", diff --git a/Cargo.toml b/Cargo.toml index 5c52c20..32f55e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,16 @@ version = "0.0.0" description = "" [workspace.dependencies] +axum = { version = "0.8", features = ["macros"] } +clap = { version = "4.6", features = ["derive", "env"] } +http = "1.4" +jacquard = "0.11" +jacquard-axum = { version = "0.11", features = ["service-auth"] } +miette = "7.6" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1.52", features = ["rt", "macros", "rt-multi-thread"] } +tower-http = { version = "0.6", features = ["trace"] } +tracing = "0.1" +tracing-subscriber = "0.3" +uuid = { version = "1.23.2", features = ["v7"] } diff --git a/crates/knotty-knot/Cargo.toml b/crates/knotty-knot/Cargo.toml new file mode 100644 index 0000000..42b4365 --- /dev/null +++ b/crates/knotty-knot/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "knotty-knot" +edition = "2024" +version.workspace = true +description.workspace = true + +[dependencies] +axum.workspace = true +clap.workspace = true +jacquard.workspace = true +jacquard-axum.workspace = true +miette.workspace = true +serde.workspace = true +serde_json.workspace = true +tokio.workspace = true +tower-http.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true diff --git a/crates/knotty-knot/src/error.rs b/crates/knotty-knot/src/error.rs new file mode 100644 index 0000000..14793d2 --- /dev/null +++ b/crates/knotty-knot/src/error.rs @@ -0,0 +1,54 @@ +use axum::{http::StatusCode, response::IntoResponse, Json}; +use serde::Serialize; + +#[derive(Serialize)] +struct XrpcErrorBody { + error: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + message: Option, +} + +pub struct KnotError { + status: StatusCode, + body: XrpcErrorBody, +} + +#[allow(unused)] +impl KnotError { + pub fn new(status: StatusCode, error: &'static str, message: Option) -> Self { + Self { + status, + body: XrpcErrorBody { error, message }, + } + } + + pub fn not_implemented() -> Self { + Self::new(StatusCode::NOT_IMPLEMENTED, "NotImplemented", None) + } + + pub fn internal(message: impl ToString) -> Self { + Self::new( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalServerError", + Some(message.to_string()), + ) + } + + pub fn repo_not_found() -> Self { + Self::new(StatusCode::NOT_FOUND, "RepoNotFound", None) + } + + pub fn invalid_request(message: impl ToString) -> Self { + Self::new( + StatusCode::BAD_REQUEST, + "InvalidRequest", + Some(message.to_string()), + ) + } +} + +impl IntoResponse for KnotError { + fn into_response(self) -> axum::response::Response { + (self.status, Json(self.body)).into_response() + } +} diff --git a/crates/knotty-knot/src/main.rs b/crates/knotty-knot/src/main.rs new file mode 100644 index 0000000..79ee7a4 --- /dev/null +++ b/crates/knotty-knot/src/main.rs @@ -0,0 +1,53 @@ +mod error; +mod router; +mod state; +mod xrpc; + +use clap::Parser; +use miette::IntoDiagnostic; +use state::KnotState; + +#[derive(Parser, Debug, Clone)] +#[command(author, version, about = "Knotty - tangled git mirroring knot server")] +pub struct Config { + #[arg(long, env = "KNOT_LISTEN_ADDR", default_value = "0.0.0.0:5555")] + pub listen_addr: String, + + /// Public hostname of this knot (used as did:web:) + #[arg(long, env = "KNOT_HOSTNAME")] + pub hostname: String, + + /// DID of the server owner + #[arg(long, env = "KNOT_OWNER")] + pub owner: String, + + /// Path where bare git repos are stored + #[arg(long, env = "KNOT_REPO_PATH", default_value = "/home/git")] + pub repo_path: String, +} + +#[tokio::main] +async fn main() -> miette::Result<()> { + tracing_subscriber::fmt::init(); + let config = Config::parse(); + + let listen_addr = config.listen_addr.clone(); + let state = KnotState::new(config); + + tracing::info!( + hostname = %state.config.hostname, + owner = %state.config.owner, + "starting knotty-knot", + ); + + let listener = tokio::net::TcpListener::bind(&listen_addr) + .await + .into_diagnostic()?; + + tracing::info!("listening on {listen_addr}"); + axum::serve(listener, router::router(state)) + .await + .into_diagnostic()?; + + Ok(()) +} diff --git a/crates/knotty-knot/src/router.rs b/crates/knotty-knot/src/router.rs new file mode 100644 index 0000000..a313cc9 --- /dev/null +++ b/crates/knotty-knot/src/router.rs @@ -0,0 +1,13 @@ +use axum::Router; +use tower_http::trace::TraceLayer; + +use crate::state::KnotState; + +pub fn router(state: KnotState) -> Router { + Router::new() + // TODO: build DidDocument from keypair/config and uncomment + // .merge(jacquard_axum::did_web::did_web_router(did_doc)) + .merge(crate::xrpc::router()) + .layer(TraceLayer::new_for_http()) + .with_state(state) +} diff --git a/crates/knotty-knot/src/state.rs b/crates/knotty-knot/src/state.rs new file mode 100644 index 0000000..e8d0190 --- /dev/null +++ b/crates/knotty-knot/src/state.rs @@ -0,0 +1,41 @@ +use jacquard::identity::{slingshot_resolver_default, JacquardResolver}; +use jacquard::types::string::Did; +use jacquard_axum::service_auth::ServiceAuth; + +use crate::Config; + +#[derive(Clone)] +pub struct KnotState { + pub config: Config, + pub resolver: JacquardResolver, + service_did: Did<'static>, +} + +impl KnotState { + pub fn new(config: Config) -> Self { + let resolver = slingshot_resolver_default(); + let did_str: &'static str = format!("did:web:{}", config.hostname).leak(); + let service_did = Did::new_static(did_str).expect("invalid hostname for did:web"); + Self { + config, + resolver, + service_did, + } + } +} + +impl ServiceAuth for KnotState { + type Resolver = JacquardResolver; + + fn service_did(&self) -> &Did<'_> { + &self.service_did + } + + fn resolver(&self) -> &Self::Resolver { + &self.resolver + } + + fn require_lxm(&self) -> bool { + true + } +} diff --git a/crates/knotty-knot/src/xrpc/create_repo.rs b/crates/knotty-knot/src/xrpc/create_repo.rs new file mode 100644 index 0000000..3ad3bb3 --- /dev/null +++ b/crates/knotty-knot/src/xrpc/create_repo.rs @@ -0,0 +1,14 @@ +use axum::extract::State; +use jacquard::api::sh_tangled::repo::create::CreateRequest; +use jacquard_axum::{ExtractXrpc, service_auth::ExtractServiceAuth}; + +use crate::{error::KnotError, state::KnotState}; + +#[axum::debug_handler(state = KnotState)] +pub async fn handler( + State(_state): State, + ExtractServiceAuth(_auth): ExtractServiceAuth, + ExtractXrpc(_req): ExtractXrpc, +) -> Result<(), KnotError> { + Err(KnotError::not_implemented()) +} diff --git a/crates/knotty-knot/src/xrpc/delete_repo.rs b/crates/knotty-knot/src/xrpc/delete_repo.rs new file mode 100644 index 0000000..c6d6fc5 --- /dev/null +++ b/crates/knotty-knot/src/xrpc/delete_repo.rs @@ -0,0 +1,14 @@ +use axum::extract::State; +use jacquard::api::sh_tangled::repo::delete::DeleteRequest; +use jacquard_axum::{ExtractXrpc, service_auth::ExtractServiceAuth}; + +use crate::{error::KnotError, state::KnotState}; + +#[axum::debug_handler(state = KnotState)] +pub async fn handler( + State(_state): State, + ExtractServiceAuth(_auth): ExtractServiceAuth, + ExtractXrpc(_req): ExtractXrpc, +) -> Result<(), KnotError> { + Err(KnotError::not_implemented()) +} diff --git a/crates/knotty-knot/src/xrpc/mod.rs b/crates/knotty-knot/src/xrpc/mod.rs new file mode 100644 index 0000000..3829317 --- /dev/null +++ b/crates/knotty-knot/src/xrpc/mod.rs @@ -0,0 +1,15 @@ +use axum::Router; +use jacquard::api::sh_tangled::repo::create::CreateRequest; +use jacquard::api::sh_tangled::repo::delete::DeleteRequest; +use jacquard_axum::IntoRouter; + +use crate::state::KnotState; + +mod create_repo; +mod delete_repo; + +pub fn router() -> Router { + Router::new() + .merge(CreateRequest::into_router(create_repo::handler)) + .merge(DeleteRequest::into_router(delete_repo::handler)) +} diff --git a/crates/knotty/Cargo.toml b/crates/knotty/Cargo.toml index 676fa65..9f2837c 100644 --- a/crates/knotty/Cargo.toml +++ b/crates/knotty/Cargo.toml @@ -5,12 +5,12 @@ version.workspace = true description.workspace = true [dependencies] -clap = { version = "4.6", features = ["derive", "env"] } -http = "1.4" -jacquard = "0.11" -miette = "7.6" -serde_json = "1.0" -tokio = { version = "1.52", features = ["rt", "macros", "rt-multi-thread"] } -tracing = "0.1" -tracing-subscriber = "0.3" -uuid = { version = "1.23.2", features = ["v7"] } +clap.workspace = true +http.workspace = true +jacquard.workspace = true +miette.workspace = true +serde_json.workspace = true +tokio.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +uuid.workspace = true