Microservice to bring 2FA to self hosted PDSes

added support for sending emails with sendmail

authored by mackuba.eu and committed by tangled.org 500ac33c 750a68c0

+276 -8
+230 -1
Cargo.lock
··· 98 98 ] 99 99 100 100 [[package]] 101 + name = "async-executor" 102 + version = "1.13.3" 103 + source = "registry+https://github.com/rust-lang/crates.io-index" 104 + checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" 105 + dependencies = [ 106 + "async-task", 107 + "concurrent-queue", 108 + "fastrand", 109 + "futures-lite", 110 + "pin-project-lite", 111 + "slab", 112 + ] 113 + 114 + [[package]] 115 + name = "async-global-executor" 116 + version = "2.4.1" 117 + source = "registry+https://github.com/rust-lang/crates.io-index" 118 + checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" 119 + dependencies = [ 120 + "async-channel 2.5.0", 121 + "async-executor", 122 + "async-io", 123 + "async-lock", 124 + "blocking", 125 + "futures-lite", 126 + "once_cell", 127 + ] 128 + 129 + [[package]] 130 + name = "async-io" 131 + version = "2.6.0" 132 + source = "registry+https://github.com/rust-lang/crates.io-index" 133 + checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" 134 + dependencies = [ 135 + "autocfg", 136 + "cfg-if", 137 + "concurrent-queue", 138 + "futures-io", 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", 155 + "pin-project-lite", 156 + ] 157 + 158 + [[package]] 159 + name = "async-process" 160 + version = "2.5.0" 161 + source = "registry+https://github.com/rust-lang/crates.io-index" 162 + checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" 163 + dependencies = [ 164 + "async-channel 2.5.0", 165 + "async-io", 166 + "async-lock", 167 + "async-signal", 168 + "async-task", 169 + "blocking", 170 + "cfg-if", 171 + "event-listener 5.4.1", 172 + "futures-lite", 173 + "rustix 1.1.2", 174 + ] 175 + 176 + [[package]] 177 + name = "async-signal" 178 + version = "0.2.13" 179 + source = "registry+https://github.com/rust-lang/crates.io-index" 180 + checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" 181 + dependencies = [ 182 + "async-io", 183 + "async-lock", 184 + "atomic-waker", 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", 192 + ] 193 + 194 + [[package]] 195 + name = "async-std" 196 + version = "1.13.2" 197 + source = "registry+https://github.com/rust-lang/crates.io-index" 198 + checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" 199 + dependencies = [ 200 + "async-channel 1.9.0", 201 + "async-global-executor", 202 + "async-io", 203 + "async-lock", 204 + "async-process", 205 + "crossbeam-utils", 206 + "futures-channel", 207 + "futures-core", 208 + "futures-io", 209 + "futures-lite", 210 + "gloo-timers", 211 + "kv-log-macro", 212 + "log", 213 + "memchr", 214 + "once_cell", 215 + "pin-project-lite", 216 + "pin-utils", 217 + "slab", 218 + "wasm-bindgen-futures", 219 + ] 220 + 221 + [[package]] 222 + name = "async-task" 223 + version = "4.7.1" 224 + source = "registry+https://github.com/rust-lang/crates.io-index" 225 + checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" 226 + 227 + [[package]] 101 228 name = "async-trait" 102 229 version = "0.1.89" 103 230 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 287 414 checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 288 415 dependencies = [ 289 416 "generic-array", 417 + ] 418 + 419 + [[package]] 420 + name = "blocking" 421 + version = "1.6.2" 422 + source = "registry+https://github.com/rust-lang/crates.io-index" 423 + checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" 424 + dependencies = [ 425 + "async-channel 2.5.0", 426 + "async-task", 427 + "futures-io", 428 + "futures-lite", 429 + "piper", 290 430 ] 291 431 292 432 [[package]] ··· 1009 1149 1010 1150 [[package]] 1011 1151 name = "event-listener" 1152 + version = "2.5.3" 1153 + source = "registry+https://github.com/rust-lang/crates.io-index" 1154 + checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" 1155 + 1156 + [[package]] 1157 + name = "event-listener" 1012 1158 version = "5.4.1" 1013 1159 source = "registry+https://github.com/rust-lang/crates.io-index" 1014 1160 checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" 1015 1161 dependencies = [ 1016 1162 "concurrent-queue", 1017 1163 "parking", 1164 + "pin-project-lite", 1165 + ] 1166 + 1167 + [[package]] 1168 + name = "event-listener-strategy" 1169 + version = "0.5.4" 1170 + source = "registry+https://github.com/rust-lang/crates.io-index" 1171 + checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" 1172 + dependencies = [ 1173 + "event-listener 5.4.1", 1018 1174 "pin-project-lite", 1019 1175 ] 1020 1176 ··· 1302 1458 ] 1303 1459 1304 1460 [[package]] 1461 + name = "gloo-timers" 1462 + version = "0.3.0" 1463 + source = "registry+https://github.com/rust-lang/crates.io-index" 1464 + checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" 1465 + dependencies = [ 1466 + "futures-channel", 1467 + "futures-core", 1468 + "js-sys", 1469 + "wasm-bindgen", 1470 + ] 1471 + 1472 + [[package]] 1305 1473 name = "governor" 1306 1474 version = "0.10.4" 1307 1475 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1463 1631 version = "0.5.0" 1464 1632 source = "registry+https://github.com/rust-lang/crates.io-index" 1465 1633 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 1634 + 1635 + [[package]] 1636 + name = "hermit-abi" 1637 + version = "0.5.2" 1638 + source = "registry+https://github.com/rust-lang/crates.io-index" 1639 + checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 1466 1640 1467 1641 [[package]] 1468 1642 name = "hex" ··· 2050 2224 ] 2051 2225 2052 2226 [[package]] 2227 + name = "kv-log-macro" 2228 + version = "1.0.7" 2229 + source = "registry+https://github.com/rust-lang/crates.io-index" 2230 + checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" 2231 + dependencies = [ 2232 + "log", 2233 + ] 2234 + 2235 + [[package]] 2053 2236 name = "langtag" 2054 2237 version = "0.4.0" 2055 2238 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2075 2258 source = "registry+https://github.com/rust-lang/crates.io-index" 2076 2259 checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f" 2077 2260 dependencies = [ 2261 + "async-std", 2078 2262 "async-trait", 2079 2263 "base64", 2080 2264 "chumsky", ··· 2594 2778 "tower_governor", 2595 2779 "tracing", 2596 2780 "tracing-subscriber", 2781 + "url", 2597 2782 "urlencoding", 2598 2783 ] 2599 2784 ··· 2688 2873 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 2689 2874 2690 2875 [[package]] 2876 + name = "piper" 2877 + version = "0.2.4" 2878 + source = "registry+https://github.com/rust-lang/crates.io-index" 2879 + checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" 2880 + dependencies = [ 2881 + "atomic-waker", 2882 + "fastrand", 2883 + "futures-io", 2884 + ] 2885 + 2886 + [[package]] 2691 2887 name = "pkcs1" 2692 2888 version = "0.7.5" 2693 2889 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2715 2911 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 2716 2912 2717 2913 [[package]] 2914 + name = "polling" 2915 + version = "3.11.0" 2916 + source = "registry+https://github.com/rust-lang/crates.io-index" 2917 + checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" 2918 + dependencies = [ 2919 + "cfg-if", 2920 + "concurrent-queue", 2921 + "hermit-abi", 2922 + "pin-project-lite", 2923 + "rustix 1.1.2", 2924 + "windows-sys 0.61.2", 2925 + ] 2926 + 2927 + [[package]] 2718 2928 name = "portable-atomic" 2719 2929 version = "1.13.0" 2720 2930 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3206 3416 ] 3207 3417 3208 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]] 3209 3432 name = "rustls" 3210 3433 version = "0.23.35" 3211 3434 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3666 3889 "crc", 3667 3890 "crossbeam-queue", 3668 3891 "either", 3669 - "event-listener", 3892 + "event-listener 5.4.1", 3670 3893 "futures-core", 3671 3894 "futures-intrusive", 3672 3895 "futures-io", ··· 4422 4645 version = "0.1.1" 4423 4646 source = "registry+https://github.com/rust-lang/crates.io-index" 4424 4647 checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 4648 + 4649 + [[package]] 4650 + name = "value-bag" 4651 + version = "1.12.0" 4652 + source = "registry+https://github.com/rust-lang/crates.io-index" 4653 + checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" 4425 4654 4426 4655 [[package]] 4427 4656 name = "vcpkg"
+1
Cargo.toml
··· 35 35 multibase = "0.9.2" 36 36 reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } 37 37 urlencoding = "2.1" 38 + url = "2.5.7" 38 39 html-escape = "0.2.13" 39 40 josekit = "0.10.3" 40 41 dashmap = "6.1"
+1 -1
src/helpers.rs
··· 17 17 use jacquard_identity::{PublicResolver, resolver::IdentityResolver}; 18 18 use josekit::jwe::alg::direct::DirectJweAlgorithm; 19 19 use lettre::{ 20 - AsyncTransport, Message, 20 + Message, 21 21 message::{MultiPart, SinglePart, header}, 22 22 }; 23 23 use rand::Rng;
+44 -6
src/main.rs
··· 4 4 use crate::xrpc::com_atproto_server::{ 5 5 create_account, create_session, describe_server, get_session, update_email, 6 6 }; 7 + use anyhow::{Result, Context}; 7 8 use axum::{ 8 9 Router, 9 10 body::Body, ··· 18 19 use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; 19 20 use jacquard_common::types::did::Did; 20 21 use jacquard_identity::{PublicResolver, resolver::PlcSource}; 21 - use lettre::{AsyncSmtpTransport, Tokio1Executor}; 22 + use lettre::{AsyncTransport, AsyncSmtpTransport, AsyncSendmailTransport, Message, Tokio1Executor}; 22 23 use rand::Rng; 23 24 use rust_embed::RustEmbed; 24 25 use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode}; ··· 36 37 }; 37 38 use tracing::log; 38 39 use tracing_subscriber::{EnvFilter, fmt, prelude::*}; 40 + use url::Url; 39 41 40 42 mod auth; 41 43 mod gate; ··· 151 153 account_pool: SqlitePool, 152 154 pds_gatekeeper_pool: SqlitePool, 153 155 reverse_proxy_client: HyperUtilClient, 154 - mailer: AsyncSmtpTransport<Tokio1Executor>, 156 + mailer: Arc<Mailer>, 155 157 template_engine: Engine<Handlebars<'static>>, 156 158 resolver: Arc<PublicResolver>, 157 159 handle_cache: auth::HandleCache, 158 160 app_config: AppConfig, 159 161 } 160 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 + 161 202 async fn root_handler() -> impl axum::response::IntoResponse { 162 203 let body = r" 163 204 ··· 244 285 .build(HttpConnector::new()); 245 286 246 287 //Emailer set up 247 - let smtp_url = 248 - env::var("PDS_EMAIL_SMTP_URL").expect("PDS_EMAIL_SMTP_URL is not set in your pds.env file"); 288 + let mailer = Arc::new(build_mailer_from_env()?); 249 289 250 - let mailer: AsyncSmtpTransport<Tokio1Executor> = 251 - AsyncSmtpTransport::<Tokio1Executor>::from_url(smtp_url.as_str())?.build(); 252 290 //Email templates setup 253 291 let mut hbs = Handlebars::new(); 254 292