diff --git a/Cargo.lock b/Cargo.lock index d8be522..009e584 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -142,9 +165,15 @@ dependencies = [ [[package]] name = "base32" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce0365f4d5fb6646220bb52fe547afd51796d90f914d4063cb0b032ebee088" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" @@ -246,6 +275,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -347,6 +387,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -603,6 +652,30 @@ dependencies = [ "byteorder", ] +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heapless" version = "0.7.17" @@ -913,6 +986,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num_cpus" version = "1.16.0" @@ -1105,6 +1184,12 @@ dependencies = [ "serde", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1153,6 +1238,8 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "axum-extra", + "base32", "bytes", "dirs-next", "heed", @@ -1161,6 +1248,7 @@ dependencies = [ "pubky-common", "serde", "tokio", + "tower-cookies", "tower-http", "tracing", "tracing-subscriber", @@ -1433,6 +1521,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha1_smol" version = "1.0.0" @@ -1610,6 +1709,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -1671,6 +1801,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-cookies" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" +dependencies = [ + "async-trait", + "axum-core", + "cookie", + "futures-util", + "http", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.5.2" @@ -1801,7 +1948,7 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" dependencies = [ - "base64", + "base64 0.22.1", "flate2", "log", "once_cell", diff --git a/pubky-common/src/crypto.rs b/pubky-common/src/crypto.rs index 8bb700a..b51181e 100644 --- a/pubky-common/src/crypto.rs +++ b/pubky-common/src/crypto.rs @@ -12,3 +12,12 @@ pub fn random_hash() -> Hash { let mut rng = rand::thread_rng(); Hash::from_bytes(rng.gen()) } + +pub fn random_bytes() -> [u8; N] { + let mut rng = rand::thread_rng(); + let mut arr = [0u8; N]; + for i in 0..N { + arr[i] = rng.gen(); + } + arr +} diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index d7bde5e..33e18ab 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [dependencies] anyhow = "1.0.82" axum = "0.7.5" +axum-extra = { version = "0.9.3", features = ["typed-header"] } +base32 = "0.5.1" bytes = "1.6.1" dirs-next = "2.0.0" heed = "0.20.3" @@ -14,6 +16,7 @@ postcard = { version = "1.0.8", features = ["alloc"] } pubky-common = { version = "0.1.0", path = "../pubky-common" } serde = { version = "1.0.204", features = ["derive"] } tokio = { version = "1.37.0", features = ["full"] } +tower-cookies = "0.10.0" tower-http = { version = "0.5.2", features = ["cors", "trace"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/pubky-homeserver/src/database.rs b/pubky-homeserver/src/database.rs index 4e4a57e..2f8d591 100644 --- a/pubky-homeserver/src/database.rs +++ b/pubky-homeserver/src/database.rs @@ -30,6 +30,7 @@ impl DB { let mut wtxn = self.env.write_txn()?; migrations::create_users_table(&self.env, &mut wtxn); + migrations::create_sessions_table(&self.env, &mut wtxn); wtxn.commit()?; diff --git a/pubky-homeserver/src/database/migrations.rs b/pubky-homeserver/src/database/migrations.rs index 428a2f4..93c7631 100644 --- a/pubky-homeserver/src/database/migrations.rs +++ b/pubky-homeserver/src/database/migrations.rs @@ -2,10 +2,18 @@ use heed::{types::Str, Database, Env, RwTxn}; use super::tables; -pub const TABLES_COUNT: u32 = 1; +pub const TABLES_COUNT: u32 = 2; pub fn create_users_table(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { - let _: tables::users::UsersTable = env.create_database(wtxn, None)?; + let _: tables::users::UsersTable = + env.create_database(wtxn, Some(tables::users::USERS_TABLE))?; + + Ok(()) +} + +pub fn create_sessions_table(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { + let _: tables::sessions::SessionsTable = + env.create_database(wtxn, Some(tables::sessions::SESSIONS_TABLE))?; Ok(()) } diff --git a/pubky-homeserver/src/database/tables.rs b/pubky-homeserver/src/database/tables.rs index 913bd46..b6e3efc 100644 --- a/pubky-homeserver/src/database/tables.rs +++ b/pubky-homeserver/src/database/tables.rs @@ -1 +1,2 @@ +pub mod sessions; pub mod users; diff --git a/pubky-homeserver/src/database/tables/sessions.rs b/pubky-homeserver/src/database/tables/sessions.rs new file mode 100644 index 0000000..bdb83d1 --- /dev/null +++ b/pubky-homeserver/src/database/tables/sessions.rs @@ -0,0 +1,42 @@ +use std::{borrow::Cow, time::SystemTime}; + +use postcard::{from_bytes, to_allocvec}; +use pubky_common::timestamp::Timestamp; +use serde::{Deserialize, Serialize}; + +use heed::{types::Bytes, BoxedError, BytesDecode, BytesEncode, Database}; +use pkarr::PublicKey; + +extern crate alloc; +use alloc::vec::Vec; + +/// session secret => Session. +pub type SessionsTable = Database; + +pub const SESSIONS_TABLE: &str = "sessions"; + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Session { + pub created_at: u64, + pub name: String, +} + +impl<'a> BytesEncode<'a> for Session { + type EItem = Self; + + fn bytes_encode(session: &Self::EItem) -> Result, BoxedError> { + let vec = to_allocvec(session)?; + + Ok(Cow::Owned(vec)) + } +} + +impl<'a> BytesDecode<'a> for Session { + type DItem = Self; + + fn bytes_decode(bytes: &'a [u8]) -> Result { + let sesison: Session = from_bytes(bytes).unwrap(); + + Ok(sesison) + } +} diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 7288221..75da602 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -2,6 +2,7 @@ use axum::{ routing::{get, post, put}, Router, }; +use tower_cookies::CookieManagerLayer; use tower_http::trace::TraceLayer; use crate::server::AppState; @@ -14,7 +15,9 @@ pub fn create_app(state: AppState) -> Router { Router::new() .route("/", get(root::handler)) .route("/:pubky", put(auth::signup)) + .route("/:pubky/session", get(auth::session)) .route("/:pubky/*key", get(drive::put)) .layer(TraceLayer::new_for_http()) + .layer(CookieManagerLayer::new()) .with_state(state) } diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index fdcb81b..f3c977d 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -1,32 +1,102 @@ -use axum::{extract::State, response::IntoResponse}; +use axum::{ + extract::{Request, State}, + http::{HeaderMap, StatusCode}, + response::IntoResponse, + routing::get, + Router, +}; +use axum_extra::{headers::UserAgent, TypedHeader}; use bytes::Bytes; +use tower_cookies::{Cookie, Cookies}; -use pubky_common::timestamp::Timestamp; +use pubky_common::{ + crypto::{random_bytes, random_hash}, + timestamp::Timestamp, +}; use crate::{ - database::tables::users::{User, UsersTable, USERS_TABLE}, - error::Result, + database::tables::{ + sessions::{Session, SessionsTable, SESSIONS_TABLE}, + users::{User, UsersTable, USERS_TABLE}, + }, + error::{Error, Result}, extractors::Pubky, server::AppState, }; pub async fn signup( State(state): State, + TypedHeader(user_agent): TypedHeader, + cookies: Cookies, pubky: Pubky, body: Bytes, ) -> Result { - state.verifier.verify(&body, pubky.public_key())?; + let public_key = pubky.public_key(); + + state.verifier.verify(&body, public_key)?; let mut wtxn = state.db.env.write_txn()?; let users: UsersTable = state.db.env.create_database(&mut wtxn, Some(USERS_TABLE))?; users.put( &mut wtxn, - pubky.public_key(), + public_key, &User { created_at: Timestamp::now().into_inner(), }, )?; + let session_secret = random_bytes::<16>(); + + let sessions: SessionsTable = state + .db + .env + .open_database(&wtxn, Some(SESSIONS_TABLE))? + .expect("Sessions table already created"); + + // TODO: handle not having a user agent? + let session = &Session { + created_at: Timestamp::now().into_inner(), + name: user_agent.to_string(), + }; + + sessions.put(&mut wtxn, &session_secret, session)?; + + cookies.add(Cookie::new( + public_key.to_string(), + base32::encode(base32::Alphabet::Crockford, &session_secret), + )); + + wtxn.commit()?; + Ok(()) } + +pub async fn session( + State(state): State, + TypedHeader(user_agent): TypedHeader, + cookies: Cookies, + pubky: Pubky, +) -> Result { + if let Some(cookie) = cookies.get(&pubky.public_key().to_string()) { + let rtxn = state.db.env.read_txn()?; + + let sessions: SessionsTable = state + .db + .env + .open_database(&rtxn, Some(SESSIONS_TABLE))? + .expect("Session table already created"); + + if let Some(session) = sessions.get( + &rtxn, + &base32::decode(base32::Alphabet::Crockford, cookie.value()).unwrap_or_default(), + )? { + rtxn.commit()?; + return Ok(()); + }; + + rtxn.commit()?; + }; + + Err(Error::with_status(StatusCode::NOT_FOUND)) +} diff --git a/pubky/src/client.rs b/pubky/src/client.rs index da0bc4f..e05a051 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -63,6 +63,20 @@ impl PubkyClient { Ok(()) } + /// Check the current sesison for a given Pubky in its homeserver. + pub fn session(&self, pubky: &PublicKey) -> Result<()> { + let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; + + url.set_path(&format!("/{}/sesison", pubky)); + + let response = self + .request(HttpMethod::Get, &url) + .call() + .map_err(Box::new)?; + + Ok(()) + } + // === Private Methods === /// Publish the SVCB record for `_pubky.`. diff --git a/pubky/src/client_async.rs b/pubky/src/client_async.rs index d99c301..297dd00 100644 --- a/pubky/src/client_async.rs +++ b/pubky/src/client_async.rs @@ -1,6 +1,6 @@ use std::thread; -use pkarr::Keypair; +use pkarr::{Keypair, PublicKey}; use crate::{error::Result, PubkyClient}; @@ -28,4 +28,19 @@ impl PubkyClientAsync { receiver.recv_async().await? } + + /// Async version of [PubkyClient::session] + pub async fn session(&self, pubky: &PublicKey) -> Result<()> { + let (sender, receiver) = flume::bounded::>(1); + + let client = self.0.clone(); + let pubky = pubky.clone(); + + thread::spawn(move || { + let result = client.session(&pubky); + sender.send(result) + }); + + receiver.recv_async().await? + } } diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 5cf602c..2684bd5 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -25,6 +25,8 @@ mod tests { client .signup(&keypair, &server.public_key().to_string()) .await - .unwrap() + .unwrap(); + + let session = client.session(&keypair.public_key()).await.unwrap(); } }