mirror of
https://github.com/aljazceru/pubky-core.git
synced 2025-12-31 04:44:37 +01:00
feat(homeserver): remove EntryPath extractor and add Authz and PubkyHost layers
This commit is contained in:
@@ -28,6 +28,9 @@ pub type EntriesTable = Database<Str, Bytes>;
|
||||
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<bool> {
|
||||
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<Option<Entry>> {
|
||||
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/"),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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<S> FromRequestParts<S> for EntryPath
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Response;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
let params: Path<HashMap<String, String>> =
|
||||
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<u16>,
|
||||
|
||||
@@ -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::<PubkyHost>() {
|
||||
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)
|
||||
|
||||
@@ -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<AppState> {
|
||||
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<AppState> {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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<AppState> {
|
||||
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()))
|
||||
}
|
||||
38
pubky-homeserver/src/core/routes/tenants/mod.rs
Normal file
38
pubky-homeserver/src/core/routes/tenants/mod.rs
Normal file
@@ -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<AppState> {
|
||||
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)
|
||||
}
|
||||
@@ -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<AppState>,
|
||||
pubky: PubkyHost,
|
||||
headers: HeaderMap,
|
||||
path: EntryPath,
|
||||
path: OriginalUri,
|
||||
) -> Result<impl IntoResponse> {
|
||||
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<AppState>,
|
||||
pubky: PubkyHost,
|
||||
params: ListQueryParams,
|
||||
) -> Result<impl IntoResponse> {
|
||||
list(state, pubky.public_key(), "pub/", params)
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
pubky: PubkyHost,
|
||||
path: EntryPath,
|
||||
path: OriginalUri,
|
||||
params: ListQueryParams,
|
||||
) -> Result<impl IntoResponse> {
|
||||
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<Response<Body>> {
|
||||
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(
|
||||
@@ -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<AppState>,
|
||||
pubky: PubkyHost,
|
||||
path: EntryPath,
|
||||
path: OriginalUri,
|
||||
) -> Result<impl IntoResponse> {
|
||||
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<AppState>,
|
||||
pubky: PubkyHost,
|
||||
path: EntryPath,
|
||||
path: OriginalUri,
|
||||
body: Body,
|
||||
) -> Result<impl IntoResponse> {
|
||||
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 {
|
||||
Reference in New Issue
Block a user