feat(pubky): add TLS support for homeserver

This commit is contained in:
nazeh
2024-11-26 13:04:19 +03:00
parent bb2799e579
commit c9ae34584c
10 changed files with 206 additions and 163 deletions

73
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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(())
}

View File

@@ -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"

View File

@@ -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::<SocketAddr>(),
)
.with_graceful_shutdown(shutdown_signal())
.into_future(),
);
tasks.spawn(server.serve(app.into_make_service_with_connect_info::<SocketAddr>()));
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(),
}
}

View File

@@ -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<CookieJar>,
pub(crate) cookie_store: Arc<native::CookieJar>,
http: reqwest::Client,
pub(crate) pkarr: pkarr::Client,
}

View File

@@ -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<HashMap<String, String>>,
normal_jar: RwLock<cookie_store::CookieStore>,
}
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<Item = &HeaderValue>, 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<HeaderValue> {
let s = self
.normal_jar
.read()
.unwrap()
.get_request_values(url)
.map(|(name, value)| format!("{name}={value}"))
.collect::<Vec<_>>()
.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()
}
}

View File

@@ -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<HashMap<String, String>>,
normal_jar: RwLock<cookie_store::CookieStore>,
}
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<Item = &HeaderValue>, 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<HeaderValue> {
let s = self
.normal_jar
.read()
.unwrap()
.get_request_values(url)
.map(|(name, value)| format!("{name}={value}"))
.collect::<Vec<_>>()
.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()
}
}

View File

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

View File

@@ -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<Session> {
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<Option<Session>> {
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<Session> {
let response = self
.request(Method::POST, format!("pubky://{}/session", token.pubky()))
.post(format!("pubky://{}/session", token.pubky()))
.body(token.serialize())
.send()
.await?