diff --git a/pubky-homeserver/src/core/database/tables/entries.rs b/pubky-homeserver/src/core/database/tables/entries.rs index 255b314..c2a9712 100644 --- a/pubky-homeserver/src/core/database/tables/entries.rs +++ b/pubky-homeserver/src/core/database/tables/entries.rs @@ -28,6 +28,9 @@ pub type EntriesTable = Database; pub const ENTRIES_TABLE: &str = "entries"; impl DB { + /// Write an entry by an author at a given path. + /// + /// The path has to start with a forward slash `/` pub fn write_entry( &mut self, public_key: &PublicKey, @@ -36,10 +39,13 @@ impl DB { EntryWriter::new(self, public_key, path) } + /// Delete an entry by an author at a given path. + /// + /// The path has to start with a forward slash `/` pub fn delete_entry(&mut self, public_key: &PublicKey, path: &str) -> anyhow::Result { let mut wtxn = self.env.write_txn()?; - let key = format!("{public_key}/{path}"); + let key = format!("{public_key}{path}"); let deleted = if let Some(bytes) = self.tables.entries.get(&wtxn, &key)? { let entry = Entry::deserialize(bytes)?; @@ -62,7 +68,7 @@ impl DB { let deleted_entry = self.tables.entries.delete(&mut wtxn, &key)?; // create DELETE event - if path.starts_with("pub/") { + if path.starts_with("/pub/") { let url = format!("pubky://{key}"); let event = Event::delete(&url); @@ -92,7 +98,7 @@ impl DB { public_key: &PublicKey, path: &str, ) -> anyhow::Result> { - let key = format!("{public_key}/{path}"); + let key = format!("{public_key}{path}"); if let Some(bytes) = self.tables.entries.get(txn, &key)? { return Ok(Some(Entry::deserialize(bytes)?)); @@ -336,7 +342,7 @@ impl<'db> EntryWriter<'db> { let buffer = File::create(&buffer_path)?; - let entry_key = format!("{public_key}/{path}"); + let entry_key = format!("{public_key}{path}"); Ok(Self { db, @@ -345,7 +351,7 @@ impl<'db> EntryWriter<'db> { buffer_path, entry_key, timestamp, - is_public: path.starts_with("pub/"), + is_public: path.starts_with("/pub/"), }) } diff --git a/pubky-homeserver/src/core/extractors.rs b/pubky-homeserver/src/core/extractors.rs index 0d4f3f9..027cb2a 100644 --- a/pubky-homeserver/src/core/extractors.rs +++ b/pubky-homeserver/src/core/extractors.rs @@ -1,8 +1,8 @@ -use std::{collections::HashMap, ops::Deref}; +use std::collections::HashMap; use axum::{ async_trait, - extract::{FromRequestParts, Path, Query}, + extract::{FromRequestParts, Query}, http::{request::Parts, StatusCode}, response::{IntoResponse, Response}, RequestPartsExt, @@ -39,60 +39,12 @@ where )) .map_err(|e| e.into_response())?; - tracing::debug!(?pubky_host); + tracing::debug!(pubky_host = ?pubky_host.public_key().to_string()); Ok(pubky_host) } } -#[derive(Debug)] -pub struct EntryPath(pub(crate) String); - -impl EntryPath { - pub fn as_str(&self) -> &str { - self.as_ref() - } -} - -impl std::fmt::Display for EntryPath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Deref for EntryPath { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[async_trait] -impl FromRequestParts for EntryPath -where - S: Send + Sync, -{ - type Rejection = Response; - - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - let params: Path> = - parts.extract().await.map_err(IntoResponse::into_response)?; - - // TODO: enforce path limits like no trailing '/' - - let path = params - .get("path") - .ok_or_else(|| (StatusCode::NOT_FOUND, "entry path missing").into_response())?; - - if parts.uri.to_string().starts_with("/pub/") { - Ok(EntryPath(format!("pub/{}", path))) - } else { - Ok(EntryPath(path.to_string())) - } - } -} - #[derive(Debug)] pub struct ListQueryParams { pub limit: Option, diff --git a/pubky-homeserver/src/core/layers/authz.rs b/pubky-homeserver/src/core/layers/authz.rs index d357fba..dfe4776 100644 --- a/pubky-homeserver/src/core/layers/authz.rs +++ b/pubky-homeserver/src/core/layers/authz.rs @@ -69,11 +69,6 @@ where Box::pin(async move { let path = req.uri().path(); - // Verify the path - if let Err(e) = verify(path) { - return Ok(e.into_response()); - } - let pubky = match req.extensions().get::() { Some(pk) => pk, None => { @@ -101,17 +96,6 @@ where } } -/// Verifies the path. -fn verify(path: &str) -> Result<()> { - if !path.starts_with("/pub/") { - return Err(Error::new( - StatusCode::FORBIDDEN, - "Writing to directories other than '/pub/' is forbidden".into(), - )); - } - Ok(()) -} - /// Authorize write (PUT or DELETE) for Public paths. fn authorize( state: &AppState, @@ -120,8 +104,18 @@ fn authorize( public_key: &PublicKey, path: &str, ) -> Result<()> { - if path.starts_with("/pub/") && method == Method::GET { + if path == "/session" { + // Checking (or deleting) one's session is ok for everyone return Ok(()); + } else if path.starts_with("/pub/") { + if method == Method::GET { + return Ok(()); + } + } else { + return Err(Error::new( + StatusCode::FORBIDDEN, + "Writing to directories other than '/pub/' is forbidden".into(), + )); } let session_secret = session_secret_from_headers(headers, public_key) diff --git a/pubky-homeserver/src/core/routes/mod.rs b/pubky-homeserver/src/core/routes/mod.rs index 9f485f1..2d7e5da 100644 --- a/pubky-homeserver/src/core/routes/mod.rs +++ b/pubky-homeserver/src/core/routes/mod.rs @@ -1,7 +1,7 @@ //! The controller part of the [crate::HomeserverCore] use axum::{ - routing::{delete, get, post}, + routing::{get, post}, Router, }; use tower_cookies::CookieManagerLayer; @@ -9,28 +9,18 @@ use tower_http::{cors::CorsLayer, trace::TraceLayer}; use crate::core::AppState; -use super::layers::pubky_host::PubkyHostLayer; - mod auth; mod feed; -mod public; mod root; +mod tenants; fn base() -> Router { Router::new() .route("/", get(root::handler)) .route("/signup", post(auth::signup)) .route("/session", post(auth::signin)) - // Routes for Pubky in the Hostname. - // - // The default and wortks with native Pubky client. - // - Session routes - .route("/session", get(auth::session)) - .route("/session", delete(auth::signout)) - // - Data routes // Events .route("/events/", get(feed::feed)) - .layer(CookieManagerLayer::new()) // TODO: add size limit // TODO: revisit if we enable streaming big payloads // TODO: maybe add to a separate router (drive router?). @@ -38,9 +28,9 @@ fn base() -> Router { pub fn create_app(state: AppState) -> Router { base() - .merge(public::data_store_router(state.clone())) + .merge(tenants::router(state.clone())) + .layer(CookieManagerLayer::new()) .layer(CorsLayer::very_permissive()) .layer(TraceLayer::new_for_http()) - .layer(PubkyHostLayer) .with_state(state) } diff --git a/pubky-homeserver/src/core/routes/public/mod.rs b/pubky-homeserver/src/core/routes/public/mod.rs deleted file mode 100644 index dc37314..0000000 --- a/pubky-homeserver/src/core/routes/public/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -use axum::{ - extract::DefaultBodyLimit, - routing::{delete, get, head, put}, - Router, -}; - -use crate::core::{layers::authz::AuthorizationLayer, AppState}; - -pub mod read; -pub mod write; - -pub fn data_store_router(state: AppState) -> Router { - Router::new() - .route("/pub/", get(read::list_root)) - .route("/pub/*path", get(read::get)) - .route("/pub/*path", head(read::head)) - .route("/pub/*path", put(write::put)) - .route("/pub/*path", delete(write::delete)) - .layer(DefaultBodyLimit::max(100 * 1024 * 1024)) - .layer(AuthorizationLayer::new(state.clone())) -} diff --git a/pubky-homeserver/src/core/routes/tenants/mod.rs b/pubky-homeserver/src/core/routes/tenants/mod.rs new file mode 100644 index 0000000..0cfac27 --- /dev/null +++ b/pubky-homeserver/src/core/routes/tenants/mod.rs @@ -0,0 +1,38 @@ +//! Per Tenant (user / Pubky) routes. +//! +//! Every route here is relative to a tenant's Pubky host, +//! as opposed to routes relative to the Homeserver's owner. + +use axum::{ + extract::DefaultBodyLimit, + routing::{delete, get, head, put}, + Router, +}; + +use crate::core::{ + layers::{authz::AuthorizationLayer, pubky_host::PubkyHostLayer}, + AppState, +}; + +use super::auth; + +pub mod read; +pub mod write; + +pub fn router(state: AppState) -> Router { + Router::new() + // - Datastore routes + .route("/pub/", get(read::get)) + .route("/pub/*path", get(read::get)) + .route("/pub/*path", head(read::head)) + .route("/pub/*path", put(write::put)) + .route("/pub/*path", delete(write::delete)) + // - Session routes + .route("/session", get(auth::session)) + .route("/session", delete(auth::signout)) + // Layers + // TODO: different max size for sessions and other routes? + .layer(DefaultBodyLimit::max(100 * 1024 * 1024)) + .layer(AuthorizationLayer::new(state.clone())) + .layer(PubkyHostLayer) +} diff --git a/pubky-homeserver/src/core/routes/public/read.rs b/pubky-homeserver/src/core/routes/tenants/read.rs similarity index 94% rename from pubky-homeserver/src/core/routes/public/read.rs rename to pubky-homeserver/src/core/routes/tenants/read.rs index 6ffb379..70b6413 100644 --- a/pubky-homeserver/src/core/routes/public/read.rs +++ b/pubky-homeserver/src/core/routes/tenants/read.rs @@ -1,6 +1,6 @@ use axum::{ body::Body, - extract::State, + extract::{OriginalUri, State}, http::{header, HeaderMap, HeaderValue, Response, StatusCode}, response::IntoResponse, }; @@ -11,7 +11,7 @@ use std::str::FromStr; use crate::core::{ database::tables::entries::Entry, error::{Error, Result}, - extractors::{EntryPath, ListQueryParams, PubkyHost}, + extractors::{ListQueryParams, PubkyHost}, AppState, }; @@ -19,7 +19,7 @@ pub async fn head( State(state): State, pubky: PubkyHost, headers: HeaderMap, - path: EntryPath, + path: OriginalUri, ) -> Result { let rtxn = state.db.env.read_txn()?; @@ -27,29 +27,22 @@ pub async fn head( headers, state .db - .get_entry(&rtxn, pubky.public_key(), path.as_str())?, + .get_entry(&rtxn, pubky.public_key(), path.0.path())?, None, ) } -pub async fn list_root( - State(state): State, - pubky: PubkyHost, - params: ListQueryParams, -) -> Result { - list(state, pubky.public_key(), "pub/", params) -} - pub async fn get( State(state): State, headers: HeaderMap, pubky: PubkyHost, - path: EntryPath, + path: OriginalUri, params: ListQueryParams, ) -> Result { let public_key = pubky.public_key().clone(); + let path = path.0.path().to_string(); - if path.as_str().ends_with('/') { + if path.ends_with('/') { return list(state, &public_key, &path, params); } @@ -91,7 +84,7 @@ pub fn list( ) -> Result> { let txn = state.db.env.read_txn()?; - let path = format!("{public_key}/{path}"); + let path = format!("{public_key}{path}"); if !state.db.contains_directory(&txn, &path)? { return Err(Error::new( diff --git a/pubky-homeserver/src/core/routes/public/write.rs b/pubky-homeserver/src/core/routes/tenants/write.rs similarity index 75% rename from pubky-homeserver/src/core/routes/public/write.rs rename to pubky-homeserver/src/core/routes/tenants/write.rs index 2f2d3f2..bebb415 100644 --- a/pubky-homeserver/src/core/routes/public/write.rs +++ b/pubky-homeserver/src/core/routes/tenants/write.rs @@ -2,23 +2,28 @@ use std::io::Write; use futures_util::stream::StreamExt; -use axum::{body::Body, extract::State, http::StatusCode, response::IntoResponse}; +use axum::{ + body::Body, + extract::{OriginalUri, State}, + http::StatusCode, + response::IntoResponse, +}; use crate::core::{ error::{Error, Result}, - extractors::{EntryPath, PubkyHost}, + extractors::PubkyHost, AppState, }; pub async fn delete( State(mut state): State, pubky: PubkyHost, - path: EntryPath, + path: OriginalUri, ) -> Result { let public_key = pubky.public_key().clone(); // TODO: should we wrap this with `tokio::task::spawn_blocking` in case it takes too long? - let deleted = state.db.delete_entry(&public_key, &path)?; + let deleted = state.db.delete_entry(&public_key, path.0.path())?; if !deleted { // TODO: if the path ends with `/` return a `CONFLICT` error? @@ -31,12 +36,12 @@ pub async fn delete( pub async fn put( State(mut state): State, pubky: PubkyHost, - path: EntryPath, + path: OriginalUri, body: Body, ) -> Result { let public_key = pubky.public_key().clone(); - let mut entry_writer = state.db.write_entry(&public_key, &path)?; + let mut entry_writer = state.db.write_entry(&public_key, path.0.path())?; let mut stream = body.into_data_stream(); while let Some(next) = stream.next().await {