diff --git a/Cargo.lock b/Cargo.lock index 05fd07a..5214992 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,12 @@ version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "argon2" version = "0.5.3" @@ -153,6 +159,7 @@ dependencies = [ "reqwest", "rpassword", "tokio", + "tracing-subscriber", "url", ] @@ -191,7 +198,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -237,7 +244,7 @@ dependencies = [ "serde", "tokio", "tokio-util", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -254,6 +261,30 @@ dependencies = [ "syn", ] +[[package]] +name = "axum-server" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56bac90848f6a9393ac03c63c640925c4b7c8ca21654de40d53f55964667c7d8" +dependencies = [ + "arc-swap", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower 0.4.13", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -1735,6 +1766,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.15" @@ -1750,7 +1801,7 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkarr" version = "3.0.0" -source = "git+https://github.com/Pubky/pkarr?branch=v3#3eda7808eafd6641049f30cd7b32fee8fe9550ea" +source = "git+https://github.com/Pubky/pkarr?branch=v3#45e905d273982697e2be19ed738b390fc98a2195" dependencies = [ "base32", "byteorder", @@ -1903,6 +1954,7 @@ dependencies = [ "anyhow", "axum", "axum-extra", + "axum-server", "base32", "bytes", "clap", @@ -2844,6 +2896,21 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.1" diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 02edbbe..bf61234 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -24,4 +24,5 @@ pubky-common = { version = "0.1.0", path = "../pubky-common" } reqwest = "0.12.8" rpassword = "7.3.1" tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } url = "2.5.2" diff --git a/examples/request/main.rs b/examples/request/main.rs index 6ada18b..3d5a688 100644 --- a/examples/request/main.rs +++ b/examples/request/main.rs @@ -1,3 +1,5 @@ +use std::env; + use anyhow::Result; use clap::Parser; use reqwest::Method; @@ -16,23 +18,31 @@ struct Cli { #[tokio::main] async fn main() -> Result<()> { - let cli = Cli::parse(); + let args = Cli::parse(); + + tracing_subscriber::fmt() + .with_env_filter(env::var("TRACING").unwrap_or("info".to_string())) + .init(); let client = Client::new()?; - match cli.url.scheme() { - "https" => { - unimplemented!(); - } - "pubky" => { - let response = client.get(cli.url).send().await?.bytes().await?; + // Build the request + let response = client.get(args.url).send().await?; - println!("Got a response: \n {:?}", response); - } - _ => { - panic!("Only https:// and pubky:// URL schemes are supported") + println!("< Response:"); + println!("< {:?} {}", response.version(), response.status()); + for (name, value) in response.headers() { + if let Ok(v) = value.to_str() { + println!("< {name}: {v}"); } } + let bytes = response.bytes().await?; + + match String::from_utf8(bytes.to_vec()) { + Ok(string) => println!("<\n{}", string), + Err(_) => println!("<\n{:?}", bytes), + } + Ok(()) } diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index c78f4ed..799b347 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -28,6 +28,7 @@ tower-http = { version = "0.5.2", features = ["cors", "trace"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } url = "2.5.2" +axum-server = { version = "0.7.1", features = ["tls-rustls-no-provider"] } [dev-dependencies] reqwest = "0.12.8" diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 44fb2a0..5fad93f 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -1,8 +1,12 @@ -use std::{future::IntoFuture, net::SocketAddr}; +use std::{ + net::{SocketAddr, TcpListener}, + sync::Arc, +}; use anyhow::{Error, Result}; +use axum_server::tls_rustls::{RustlsAcceptor, RustlsConfig}; use pubky_common::auth::AuthVerifier; -use tokio::{net::TcpListener, signal, task::JoinSet}; +use tokio::task::JoinSet; use tracing::{debug, info, warn}; use pkarr::{mainline::Testnet, PublicKey}; @@ -45,7 +49,7 @@ impl Homeserver { let mut tasks = JoinSet::new(); - let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], config.port()))).await?; + let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], config.port())))?; let port = listener.local_addr()?.port(); @@ -57,24 +61,24 @@ impl Homeserver { port, }; + let acceptor = RustlsAcceptor::new(RustlsConfig::from_config(Arc::new( + config.keypair().to_rpk_rustls_server_config(), + ))); + let server = axum_server::from_tcp(listener).acceptor(acceptor); + let app = crate::routes::create_app(state.clone()); // Spawn http server task - tasks.spawn( - axum::serve( - listener, - app.into_make_service_with_connect_info::(), - ) - .with_graceful_shutdown(shutdown_signal()) - .into_future(), - ); + tasks.spawn(server.serve(app.into_make_service_with_connect_info::())); info!("Homeserver listening on http://localhost:{port}"); + info!("Publishing Pkarr packet.."); + publish_server_packet(&pkarr_client, &config, port).await?; info!( - "Homeserver listening on http://{}", + "Homeserver listening on https://{}", config.keypair().public_key() ); @@ -134,31 +138,3 @@ impl Homeserver { final_res } } - -async fn shutdown_signal() { - let ctrl_c = async { - signal::ctrl_c() - .await - .expect("failed to install Ctrl+C handler"); - }; - - #[cfg(unix)] - let terminate = async { - signal::unix::signal(signal::unix::SignalKind::terminate()) - .expect("failed to install signal handler") - .recv() - .await; - }; - - #[cfg(not(unix))] - let terminate = std::future::pending::<()>(); - - fn graceful_shutdown() { - info!("Gracefully Shutting down.."); - } - - tokio::select! { - _ = ctrl_c => graceful_shutdown(), - _ = terminate => graceful_shutdown(), - } -} diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index ea0e55f..e8dedf6 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -12,7 +12,6 @@ mod wasm; use std::{fmt::Debug, sync::Arc}; -use native::CookieJar; use wasm_bindgen::prelude::*; pub use error::Error; @@ -25,7 +24,7 @@ pub use crate::shared::list_builder::ListBuilder; #[wasm_bindgen] pub struct Client { #[cfg(not(target_arch = "wasm32"))] - pub(crate) cookie_store: Arc, + pub(crate) cookie_store: Arc, http: reqwest::Client, pub(crate) pkarr: pkarr::Client, } diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 6063235..63fa8c2 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -1,18 +1,16 @@ -use std::{ - collections::HashMap, - sync::{Arc, RwLock}, - time::Duration, -}; +use std::{sync::Arc, time::Duration}; -use pkarr::{mainline::Testnet, PublicKey}; -use reqwest::{cookie::CookieStore, header::HeaderValue, Response}; +use pkarr::mainline::Testnet; use crate::Client; mod api; +mod cookies; mod http; mod internals; +pub(crate) use cookies::CookieJar; + static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); #[derive(Debug, Default)] @@ -54,10 +52,10 @@ impl Settings { let cookie_store = Arc::new(CookieJar::default()); - let http = reqwest::Client::builder() - .cookie_provider(cookie_store.clone()) + let http = reqwest::ClientBuilder::from(pkarr.clone()) // TODO: use persistent cookie jar - .dns_resolver(Arc::new(pkarr.clone())) + .cookie_provider(cookie_store.clone()) + // TODO: allow custom user agent, but force a Pubky user agent information .user_agent(DEFAULT_USER_AGENT) .build() .expect("config expected to not error"); @@ -98,75 +96,3 @@ impl Client { Client::builder().testnet(testnet).build().unwrap() } } - -#[derive(Default)] -pub struct CookieJar { - pubky_sessions: RwLock>, - normal_jar: RwLock, -} - -impl CookieJar { - pub(crate) fn store_session_after_signup(&self, response: &Response, pubky: PublicKey) { - for (header_name, header_value) in response.headers() { - if header_name == "set-cookie" && header_value.as_ref().starts_with(b"session_id=") { - if let Ok(Ok(cookie)) = - std::str::from_utf8(header_value.as_bytes()).map(cookie::Cookie::parse) - { - if cookie.name() == "session_id" { - let domain = format!("_pubky.{pubky}"); - tracing::debug!(?cookie, "Storing coookie after signup"); - - self.pubky_sessions - .write() - .unwrap() - .insert(domain, cookie.value().to_string()); - } - }; - } - } - } - - pub(crate) fn delete_session_after_signout(&self, pubky: &PublicKey) { - self.pubky_sessions - .write() - .unwrap() - .remove(&format!("_pubky.{pubky}")); - } -} - -impl CookieStore for CookieJar { - fn set_cookies(&self, cookie_headers: &mut dyn Iterator, url: &url::Url) { - let iter = cookie_headers.filter_map(|val| { - val.to_str() - .ok() - .and_then(|s| cookie::Cookie::parse(s.to_owned()).ok()) - }); - - self.normal_jar - .write() - .unwrap() - .store_response_cookies(iter, url); - } - - fn cookies(&self, url: &url::Url) -> Option { - let s = self - .normal_jar - .read() - .unwrap() - .get_request_values(url) - .map(|(name, value)| format!("{name}={value}")) - .collect::>() - .join("; "); - - if s.is_empty() { - return self - .pubky_sessions - .read() - .unwrap() - .get(url.host_str().unwrap()) - .map(|secret| HeaderValue::try_from(format!("session_id={secret}")).unwrap()); - } - - HeaderValue::from_maybe_shared(bytes::Bytes::from(s)).ok() - } -} diff --git a/pubky/src/native/cookies.rs b/pubky/src/native/cookies.rs new file mode 100644 index 0000000..5a030cb --- /dev/null +++ b/pubky/src/native/cookies.rs @@ -0,0 +1,76 @@ +use std::{collections::HashMap, sync::RwLock}; + +use pkarr::PublicKey; +use reqwest::{cookie::CookieStore, header::HeaderValue, Response}; + +#[derive(Default)] +pub struct CookieJar { + pubky_sessions: RwLock>, + normal_jar: RwLock, +} + +impl CookieJar { + pub(crate) fn store_session_after_signup(&self, response: &Response, pubky: &PublicKey) { + for (header_name, header_value) in response.headers() { + if header_name == "set-cookie" && header_value.as_ref().starts_with(b"session_id=") { + if let Ok(Ok(cookie)) = + std::str::from_utf8(header_value.as_bytes()).map(cookie::Cookie::parse) + { + if cookie.name() == "session_id" { + let domain = format!("_pubky.{pubky}"); + tracing::debug!(?cookie, "Storing coookie after signup"); + + self.pubky_sessions + .write() + .unwrap() + .insert(domain, cookie.value().to_string()); + } + }; + } + } + } + + pub(crate) fn delete_session_after_signout(&self, pubky: &PublicKey) { + self.pubky_sessions + .write() + .unwrap() + .remove(&format!("_pubky.{pubky}")); + } +} + +impl CookieStore for CookieJar { + fn set_cookies(&self, cookie_headers: &mut dyn Iterator, url: &url::Url) { + let iter = cookie_headers.filter_map(|val| { + val.to_str() + .ok() + .and_then(|s| cookie::Cookie::parse(s.to_owned()).ok()) + }); + + self.normal_jar + .write() + .unwrap() + .store_response_cookies(iter, url); + } + + fn cookies(&self, url: &url::Url) -> Option { + let s = self + .normal_jar + .read() + .unwrap() + .get_request_values(url) + .map(|(name, value)| format!("{name}={value}")) + .collect::>() + .join("; "); + + if s.is_empty() { + return self + .pubky_sessions + .read() + .unwrap() + .get(url.host_str().unwrap()) + .map(|secret| HeaderValue::try_from(format!("session_id={secret}")).unwrap()); + } + + HeaderValue::from_maybe_shared(bytes::Bytes::from(s)).ok() + } +} diff --git a/pubky/src/native/http.rs b/pubky/src/native/http.rs index a8e4c4d..e04883f 100644 --- a/pubky/src/native/http.rs +++ b/pubky/src/native/http.rs @@ -11,7 +11,7 @@ impl Client { /// the request body before sending. /// /// Differs from [reqwest::Client::request], in that it can make requests to: - /// 1. HTTP(s) URLs with with a [pkarr::PublicKey] as Top Level Domain, by resolving + /// 1. HTTPs URLs with with a [pkarr::PublicKey] as Top Level Domain, by resolving /// corresponding endpoints, and verifying TLS certificates accordingly. /// (example: `https://o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy`) /// 2. Pubky URLs like `pubky://o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy` @@ -137,10 +137,8 @@ mod tests { let client = Client::test(&testnet); - let url = format!("http://{}/", homeserver.public_key()); - let response = client - .request(Default::default(), url) + .get(format!("https://{}/", homeserver.public_key())) .send() .await .unwrap(); diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index f5e6b12..80ff432 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use base64::{alphabet::URL_SAFE, engine::general_purpose::NO_PAD, Engine}; -use reqwest::{Method, StatusCode}; +use reqwest::StatusCode; use url::Url; use pkarr::{Keypair, PublicKey}; @@ -17,8 +17,6 @@ use crate::{ Client, }; -use super::pkarr::Endpoint; - impl Client { /// Signup to a homeserver and update Pkarr accordingly. /// @@ -29,27 +27,19 @@ impl Client { keypair: &Keypair, homeserver: &PublicKey, ) -> Result { - let homeserver = homeserver.to_string(); - - let Endpoint { mut url, .. } = self.resolve_endpoint(&homeserver).await?; - - url.set_path("/signup"); - - let body = AuthToken::sign(keypair, vec![Capability::root()]).serialize(); - let response = self - .http - .request(Method::POST, url.clone()) - .body(body) + .post(format!("https://{}", homeserver)) + .body(AuthToken::sign(keypair, vec![Capability::root()]).serialize()) .send() .await? .error_for_status()?; - self.publish_pubky_homeserver(keypair, &homeserver).await?; + self.publish_pubky_homeserver(keypair, &homeserver.to_string()) + .await?; // Store the cookie to the correct URL. self.cookie_store - .store_session_after_signup(&response, keypair.public_key()); + .store_session_after_signup(&response, &keypair.public_key()); let bytes = response.bytes().await?; @@ -62,7 +52,7 @@ impl Client { /// if the response has any other `>=404` status code. pub(crate) async fn inner_session(&self, pubky: &PublicKey) -> Result> { let res = self - .request(Method::GET, format!("pubky://{}/session", pubky)) + .get(format!("pubky://{}/session", pubky)) .send() .await?; @@ -81,7 +71,7 @@ impl Client { /// Signout from a homeserver. pub(crate) async fn inner_signout(&self, pubky: &PublicKey) -> Result<()> { - self.request(Method::DELETE, format!("pubky://{}/session", pubky)) + self.delete(format!("pubky://{}/session", pubky)) .send() .await? .error_for_status()?; @@ -145,8 +135,7 @@ impl Client { path_segments.push(&channel_id); drop(path_segments); - self.inner_request(Method::POST, callback) - .await + self.post(callback) .body(encrypted_token) .send() .await? @@ -157,7 +146,7 @@ impl Client { pub(crate) async fn signin_with_authtoken(&self, token: &AuthToken) -> Result { let response = self - .request(Method::POST, format!("pubky://{}/session", token.pubky())) + .post(format!("pubky://{}/session", token.pubky())) .body(token.serialize()) .send() .await?