Skip to content

Commit

Permalink
feat(homeserver): remove EntryPath extractor and add Authz and PubkyH…
Browse files Browse the repository at this point in the history
…ost layers
  • Loading branch information
Nuhvi committed Dec 22, 2024
1 parent 96be581 commit bcb8d39
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 129 deletions.
16 changes: 11 additions & 5 deletions pubky-homeserver/src/core/database/tables/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)?;
Expand All @@ -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);
Expand Down Expand Up @@ -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)?));
Expand Down Expand Up @@ -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,
Expand All @@ -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/"),
})
}

Expand Down
54 changes: 3 additions & 51 deletions pubky-homeserver/src/core/extractors.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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>,
Expand Down
28 changes: 11 additions & 17 deletions pubky-homeserver/src/core/layers/authz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
18 changes: 4 additions & 14 deletions pubky-homeserver/src/core/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,46 +1,36 @@
//! The controller part of the [crate::HomeserverCore]
use axum::{
routing::{delete, get, post},
routing::{get, post},
Router,
};
use tower_cookies::CookieManagerLayer;
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?).
}

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)
}
21 changes: 0 additions & 21 deletions pubky-homeserver/src/core/routes/public/mod.rs

This file was deleted.

38 changes: 38 additions & 0 deletions pubky-homeserver/src/core/routes/tenants/mod.rs
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use axum::{
body::Body,
extract::State,
extract::{OriginalUri, State},
http::{header, HeaderMap, HeaderValue, Response, StatusCode},
response::IntoResponse,
};
Expand All @@ -11,45 +11,38 @@ use std::str::FromStr;
use crate::core::{
database::tables::entries::Entry,
error::{Error, Result},
extractors::{EntryPath, ListQueryParams, PubkyHost},
extractors::{ListQueryParams, PubkyHost},
AppState,
};

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()?;

get_entry(
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);
}

Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit bcb8d39

Please sign in to comment.