Files
ElyOffice/crates/ely-mail-engine/tests/oauth_fake/mod.rs
T
ZacharyZhang-NYandClaude bd68382921 C12.4: mail credentials and OAuth sign-in
Passwords and refresh tokens go only to the system keychain; a refused
keychain keeps them in memory until exit and says why. Gmail and
Microsoft sign in by authorization code with S256 PKCE on a loopback
redirect; refresh tokens are kept, and revoked grants ask to sign in
again. Client IDs come from the build (env or oauth.env). ureq trace
records never reach the log.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-19 18:59:56 -04:00

100 lines
3.6 KiB
Rust

//! A fake token endpoint and browser for OAuth tests.
#![allow(dead_code)]
use std::{
collections::HashMap,
io::{Read, Write},
net::TcpListener,
sync::mpsc,
thread,
};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use ely_mail_engine::oauth::{Provider, SignIn, Tokens};
/// A token endpoint that answers once and reports the form.
pub fn endpoint(status: u16, body: String) -> (String, mpsc::Receiver<HashMap<String, String>>) {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!("http://{}/token", listener.local_addr().unwrap());
let (sender, receiver) = mpsc::channel();
thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut data = Vec::new();
let mut chunk = [0u8; 4096];
loop {
let count = stream.read(&mut chunk).unwrap();
data.extend_from_slice(&chunk[..count]);
let text = String::from_utf8_lossy(&data).to_string();
if let Some((head, rest)) = text.split_once("\r\n\r\n") {
let length: usize = head
.lines()
.find_map(|line| {
line.to_ascii_lowercase()
.strip_prefix("content-length:")
.map(|v| v.trim().parse().unwrap())
})
.unwrap();
if rest.len() >= length {
let form = form_urlencoded::parse(rest.as_bytes())
.into_owned()
.collect();
let _ = sender.send(form);
break;
}
}
}
let reply = format!(
"HTTP/1.1 {status} X\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
);
stream.write_all(reply.as_bytes()).unwrap();
});
(url, receiver)
}
pub fn provider(token: String, secret: Option<&str>) -> Provider {
Provider {
name: "test",
authorize: "https://auth.example/authorize".into(),
token,
client_id: "client-1".into(),
client_secret: secret.map(str::to_owned),
scope: "mail openid email".into(),
extra: &[("prompt", "consent")],
redirect_host: "127.0.0.1",
}
}
pub fn id_token(claims: &str) -> String {
format!("e30.{}.sig", URL_SAFE_NO_PAD.encode(claims))
}
pub fn query(url: &str) -> HashMap<String, String> {
let (_, query) = url.split_once('?').unwrap();
form_urlencoded::parse(query.as_bytes())
.into_owned()
.collect()
}
/// The browser following the redirect; returns the status line.
pub fn browse(redirect: &str, path: &str) -> String {
let address = redirect.trim_start_matches("http://");
let mut stream = std::net::TcpStream::connect(address).unwrap();
write!(stream, "GET {path} HTTP/1.1\r\nHost: {address}\r\n\r\n").unwrap();
let mut reply = String::new();
stream.read_to_string(&mut reply).unwrap();
reply.lines().next().unwrap().to_owned()
}
/// Completes a sign-in whose endpoint answers with body.
pub async fn signed_in(body: String) -> ely_mail_engine::Result<Tokens> {
let (token, _) = endpoint(200, body);
let sign_in = SignIn::start(provider(token, None), "").await.unwrap();
let asked = query(sign_in.url());
let (redirect, state) = (asked["redirect_uri"].clone(), asked["state"].clone());
let browser = thread::spawn(move || browse(&redirect, &format!("/?code=c&state={state}")));
let result = sign_in.finish().await;
browser.join().unwrap();
result
}