Skip to main content

coven_storage/providers/
desktop_oauth.rs

1use super::{OAuthError, OAuthPresenter};
2use crate::CloudProvider;
3use coven_crypto::SecretText;
4use tokio::io::{AsyncReadExt, AsyncWriteExt};
5
6/// Desktop sign-in using the system browser and a loopback callback listener.
7/// Register `http://localhost:19284/callback` with each provider's client.
8pub 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;