Microservice to bring 2FA to self hosted PDSes

split up some code

authored by baileytownsend.dev and committed by tangled.org 04f863e2 500ac33c

+93 -53
+42 -10
Cargo.lock
··· 85 ] 86 87 [[package]] 88 name = "async-compression" 89 version = "0.4.36" 90 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 139 "futures-lite", 140 "parking", 141 "polling", 142 - "rustix 1.1.2", 143 "slab", 144 "windows-sys 0.61.2", 145 ] 146 147 [[package]] 148 name = "async-lock" 149 - version = "3.4.1" 150 source = "registry+https://github.com/rust-lang/crates.io-index" 151 - checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" 152 dependencies = [ 153 "event-listener 5.4.1", 154 "event-listener-strategy", ··· 170 "cfg-if", 171 "event-listener 5.4.1", 172 "futures-lite", 173 - "rustix 1.1.2", 174 ] 175 176 [[package]] ··· 185 "cfg-if", 186 "futures-core", 187 "futures-io", 188 - "rustix 1.1.2", 189 "signal-hook-registry", 190 "slab", 191 "windows-sys 0.61.2", ··· 2316 ] 2317 2318 [[package]] 2319 name = "litemap" 2320 version = "0.8.1" 2321 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2335 version = "0.4.29" 2336 source = "registry+https://github.com/rust-lang/crates.io-index" 2337 checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 2338 2339 [[package]] 2340 name = "loom" ··· 2920 "concurrent-queue", 2921 "hermit-abi", 2922 "pin-project-lite", 2923 - "rustix 1.1.2", 2924 "windows-sys 0.61.2", 2925 ] 2926 ··· 3417 3418 [[package]] 3419 name = "rustix" 3420 - version = "1.1.2" 3421 source = "registry+https://github.com/rust-lang/crates.io-index" 3422 - checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 3423 dependencies = [ 3424 "bitflags", 3425 "errno", 3426 "libc", 3427 - "linux-raw-sys 0.11.0", 3428 - "windows-sys 0.59.0", 3429 ] 3430 3431 [[package]]
··· 85 ] 86 87 [[package]] 88 + name = "async-channel" 89 + version = "1.9.0" 90 + source = "registry+https://github.com/rust-lang/crates.io-index" 91 + checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" 92 + dependencies = [ 93 + "concurrent-queue", 94 + "event-listener 2.5.3", 95 + "futures-core", 96 + ] 97 + 98 + [[package]] 99 + name = "async-channel" 100 + version = "2.5.0" 101 + source = "registry+https://github.com/rust-lang/crates.io-index" 102 + checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" 103 + dependencies = [ 104 + "concurrent-queue", 105 + "event-listener-strategy", 106 + "futures-core", 107 + "pin-project-lite", 108 + ] 109 + 110 + [[package]] 111 name = "async-compression" 112 version = "0.4.36" 113 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 162 "futures-lite", 163 "parking", 164 "polling", 165 + "rustix", 166 "slab", 167 "windows-sys 0.61.2", 168 ] 169 170 [[package]] 171 name = "async-lock" 172 + version = "3.4.2" 173 source = "registry+https://github.com/rust-lang/crates.io-index" 174 + checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" 175 dependencies = [ 176 "event-listener 5.4.1", 177 "event-listener-strategy", ··· 193 "cfg-if", 194 "event-listener 5.4.1", 195 "futures-lite", 196 + "rustix", 197 ] 198 199 [[package]] ··· 208 "cfg-if", 209 "futures-core", 210 "futures-io", 211 + "rustix", 212 "signal-hook-registry", 213 "slab", 214 "windows-sys 0.61.2", ··· 2339 ] 2340 2341 [[package]] 2342 + name = "linux-raw-sys" 2343 + version = "0.11.0" 2344 + source = "registry+https://github.com/rust-lang/crates.io-index" 2345 + checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 2346 + 2347 + [[package]] 2348 name = "litemap" 2349 version = "0.8.1" 2350 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2364 version = "0.4.29" 2365 source = "registry+https://github.com/rust-lang/crates.io-index" 2366 checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 2367 + dependencies = [ 2368 + "value-bag", 2369 + ] 2370 2371 [[package]] 2372 name = "loom" ··· 2952 "concurrent-queue", 2953 "hermit-abi", 2954 "pin-project-lite", 2955 + "rustix", 2956 "windows-sys 0.61.2", 2957 ] 2958 ··· 3449 3450 [[package]] 3451 name = "rustix" 3452 + version = "1.1.3" 3453 source = "registry+https://github.com/rust-lang/crates.io-index" 3454 + checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" 3455 dependencies = [ 3456 "bitflags", 3457 "errno", 3458 "libc", 3459 + "linux-raw-sys", 3460 + "windows-sys 0.52.0", 3461 ] 3462 3463 [[package]]
+1 -1
Cargo.toml
··· 22 #Leaveing these two cause I think it is needed by the email crate for ssl 23 aws-lc-rs = "1.15.2" 24 rustls = { version = "0.23", default-features = false, features = ["tls12", "std", "logging", "aws_lc_rs"] } 25 - lettre = { version = "0.11", default-features = false, features = ["builder", "webpki-roots", "rustls", "aws-lc-rs", "smtp-transport", "tokio1", "tokio1-rustls"] } 26 handlebars = { version = "6.4.0", features = ["rust-embed"] } 27 rust-embed = "8.9.0" 28 axum-template = { version = "3.0.0", features = ["handlebars"] }
··· 22 #Leaveing these two cause I think it is needed by the email crate for ssl 23 aws-lc-rs = "1.15.2" 24 rustls = { version = "0.23", default-features = false, features = ["tls12", "std", "logging", "aws_lc_rs"] } 25 + lettre = { version = "0.11", default-features = false, features = ["builder", "webpki-roots", "rustls", "aws-lc-rs", "smtp-transport", "sendmail-transport", "tokio1", "tokio1-rustls"] } 26 handlebars = { version = "6.4.0", features = ["rust-embed"] } 27 rust-embed = "8.9.0" 28 axum-template = { version = "3.0.0", features = ["handlebars"] }
+46
src/mailer.rs
···
··· 1 + use anyhow::Context; 2 + use lettre::{AsyncSendmailTransport, AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; 3 + use std::env; 4 + use url::Url; 5 + 6 + pub enum Mailer { 7 + Smtp(AsyncSmtpTransport<Tokio1Executor>), 8 + Sendmail(AsyncSendmailTransport<Tokio1Executor>), 9 + } 10 + 11 + impl Mailer { 12 + pub async fn send(&self, msg: Message) -> anyhow::Result<()> { 13 + match self { 14 + Mailer::Smtp(m) => { 15 + m.send(msg).await.context("SMTP send failed")?; 16 + Ok(()) 17 + } 18 + Mailer::Sendmail(m) => { 19 + m.send(msg).await.context("sendmail send failed")?; 20 + Ok(()) 21 + } 22 + } 23 + } 24 + } 25 + 26 + pub fn build_mailer_from_env() -> anyhow::Result<Mailer> { 27 + let raw = env::var("PDS_EMAIL_SMTP_URL") 28 + .context("PDS_EMAIL_SMTP_URL is not set in your pds.env file")?; 29 + 30 + let url = Url::parse(&raw).context("PDS_EMAIL_SMTP_URL is not a valid URL")?; 31 + 32 + let use_sendmail = url.scheme() == "sendmail" 33 + || url 34 + .query_pairs() 35 + .any(|(k, v)| k == "sendmail" && v == "true"); 36 + 37 + if use_sendmail { 38 + Ok(Mailer::Sendmail( 39 + AsyncSendmailTransport::<Tokio1Executor>::new(), 40 + )) 41 + } else { 42 + Ok(Mailer::Smtp( 43 + AsyncSmtpTransport::<Tokio1Executor>::from_url(raw.as_str())?.build(), 44 + )) 45 + } 46 + }
+4 -42
src/main.rs
··· 1 #![warn(clippy::unwrap_used)] 2 use crate::gate::{get_gate, post_gate}; 3 use crate::oauth_provider::sign_in; 4 use crate::xrpc::com_atproto_server::{ 5 create_account, create_session, describe_server, get_session, update_email, 6 }; 7 - use anyhow::{Result, Context}; 8 use axum::{ 9 Router, 10 body::Body, ··· 19 use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; 20 use jacquard_common::types::did::Did; 21 use jacquard_identity::{PublicResolver, resolver::PlcSource}; 22 - use lettre::{AsyncTransport, AsyncSmtpTransport, AsyncSendmailTransport, Message, Tokio1Executor}; 23 use rand::Rng; 24 use rust_embed::RustEmbed; 25 use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode}; ··· 37 }; 38 use tracing::log; 39 use tracing_subscriber::{EnvFilter, fmt, prelude::*}; 40 - use url::Url; 41 42 mod auth; 43 mod gate; 44 pub mod helpers; 45 mod middleware; 46 mod oauth_provider; 47 mod xrpc; ··· 160 app_config: AppConfig, 161 } 162 163 - pub enum Mailer { 164 - Smtp(AsyncSmtpTransport<Tokio1Executor>), 165 - Sendmail(AsyncSendmailTransport<Tokio1Executor>), 166 - } 167 - 168 - impl Mailer { 169 - pub async fn send(&self, msg: Message) -> Result<()> { 170 - match self { 171 - Mailer::Smtp(m) => { 172 - m.send(msg).await.context("SMTP send failed")?; 173 - Ok(()) 174 - } 175 - Mailer::Sendmail(m) => { 176 - m.send(msg).await.context("sendmail send failed")?; 177 - Ok(()) 178 - } 179 - } 180 - } 181 - } 182 - 183 - fn build_mailer_from_env() -> Result<Mailer> { 184 - let raw = env::var("PDS_EMAIL_SMTP_URL") 185 - .context("PDS_EMAIL_SMTP_URL is not set in your pds.env file")?; 186 - 187 - let url = Url::parse(&raw).context("PDS_EMAIL_SMTP_URL is not a valid URL")?; 188 - 189 - let use_sendmail = url.scheme() == "sendmail" 190 - || url.query_pairs().any(|(k, v)| k == "sendmail" && v == "true"); 191 - 192 - if use_sendmail { 193 - Ok(Mailer::Sendmail(AsyncSendmailTransport::<Tokio1Executor>::new())) 194 - } else { 195 - Ok(Mailer::Smtp( 196 - AsyncSmtpTransport::<Tokio1Executor>::from_url(raw.as_str())? 197 - .build(), 198 - )) 199 - } 200 - } 201 - 202 async fn root_handler() -> impl axum::response::IntoResponse { 203 let body = r" 204 ··· 254 let account_db_url = format!("{pds_root}/account.sqlite"); 255 256 let account_options = SqliteConnectOptions::new() 257 .filename(account_db_url) 258 .busy_timeout(Duration::from_secs(5)); 259
··· 1 #![warn(clippy::unwrap_used)] 2 use crate::gate::{get_gate, post_gate}; 3 + use crate::mailer::{Mailer, build_mailer_from_env}; 4 use crate::oauth_provider::sign_in; 5 use crate::xrpc::com_atproto_server::{ 6 create_account, create_session, describe_server, get_session, update_email, 7 }; 8 + use anyhow::Result; 9 use axum::{ 10 Router, 11 body::Body, ··· 20 use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; 21 use jacquard_common::types::did::Did; 22 use jacquard_identity::{PublicResolver, resolver::PlcSource}; 23 use rand::Rng; 24 use rust_embed::RustEmbed; 25 use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode}; ··· 37 }; 38 use tracing::log; 39 use tracing_subscriber::{EnvFilter, fmt, prelude::*}; 40 41 mod auth; 42 mod gate; 43 pub mod helpers; 44 + pub mod mailer; 45 mod middleware; 46 mod oauth_provider; 47 mod xrpc; ··· 160 app_config: AppConfig, 161 } 162 163 async fn root_handler() -> impl axum::response::IntoResponse { 164 let body = r" 165 ··· 215 let account_db_url = format!("{pds_root}/account.sqlite"); 216 217 let account_options = SqliteConnectOptions::new() 218 + .journal_mode(SqliteJournalMode::Wal) 219 .filename(account_db_url) 220 .busy_timeout(Duration::from_secs(5)); 221