coven_storage/providers/
desktop_oauth.rs1use super::{OAuthError, OAuthPresenter};
2use crate::CloudProvider;
3use coven_crypto::SecretText;
4use tokio::io::{AsyncReadExt, AsyncWriteExt};
5
6pub struct DesktopOAuthPresenter;
9
10#[async_trait::async_trait]
11impl OAuthPresenter for DesktopOAuthPresenter {
12 fn redirect_uri(&self, _provider: CloudProvider) -> &str {
13 "http://localhost:19284/callback"
14 }
15
16 async fn present(&self, authorization_url: &str) -> Result<SecretText, OAuthError> {
17 let listener = tokio::net::TcpListener::bind("127.0.0.1:19284").await?;
18 open::that(authorization_url)?;
19 receive_redirect(listener).await
20 }
21}
22
23#[expect(clippy::map_err_ignore, reason = "a redirect that does not parse is InvalidRedirect")]
24async fn receive_redirect(listener: tokio::net::TcpListener) -> Result<SecretText, OAuthError> {
25 loop {
26 let (mut socket, _) = listener.accept().await?;
27 let mut data = zeroize::Zeroizing::new(Vec::with_capacity(8192));
28 while !data.ends_with(b"\r\n\r\n") {
29 if data.len() >= 8192 {
30 return Err(OAuthError::InvalidRedirect);
31 }
32 data.push(socket.read_u8().await?);
33 }
34 let line = std::str::from_utf8(&data)
35 .map_err(|_| OAuthError::InvalidRedirect)?
36 .lines()
37 .next()
38 .ok_or(OAuthError::InvalidRedirect)?;
39 let target = line
40 .strip_prefix("GET ")
41 .and_then(|s| s.strip_suffix(" HTTP/1.1"))
42 .ok_or(OAuthError::InvalidRedirect)?;
43 if !target.starts_with('/') || target.starts_with("//") {
44 return Err(OAuthError::InvalidRedirect);
45 }
46 let url = url::Url::parse(&format!("http://localhost:19284{target}"))
47 .map_err(|_| OAuthError::InvalidRedirect)?;
48 if url.path() != "/callback" {
49 socket
50 .write_all(
51 b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
52 )
53 .await?;
54 continue;
55 }
56 socket.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n").await?;
57 return Ok(SecretText::new(url.into()));
58 }
59}
60
61#[cfg(test)]
62#[path = "desktop_oauth_tests.rs"]
63mod tests;