game_manager_lib\utils/
oauth.rs

1//! Utilitários genéricos para fluxo OAuth2 com PKCE.
2//!
3//! Fornece funcionalidades para gerar desafios PKCE e iniciar um servidor temporário
4//! para capturar o código de autorização retornado pelo provedor OAuth2.
5
6use crate::constants::OAUTH_CALLBACK_TIMEOUT_SECS;
7use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
8use rand::{distr::Alphanumeric, Rng};
9use sha2::{Digest, Sha256};
10use std::{sync::mpsc, thread, time::Duration};
11use tiny_http::{Response, Server};
12use url::Url;
13
14/// Estrutura para o desafio PKCE (Proof Key for Code Exchange).
15pub struct PkceChallenge {
16    pub verifier: String,
17    pub challenge: String,
18}
19
20impl PkceChallenge {
21    pub fn generate() -> Self {
22        let verifier: String = rand::rng()
23            .sample_iter(&Alphanumeric)
24            .take(64)
25            .map(char::from)
26            .collect();
27
28        let hash = Sha256::digest(verifier.as_bytes());
29        let challenge = URL_SAFE_NO_PAD.encode(hash);
30
31        Self {
32            verifier,
33            challenge,
34        }
35    }
36}
37
38/// Inicia um servidor temporário para receber o callback do OAuth.
39/// Bloqueia a thread até receber o código ou dar timeout.
40pub fn wait_for_auth_code(port: u16) -> Result<String, String> {
41    let address = format!("127.0.0.1:{}", port);
42    // Tenta iniciar o servidor
43    let server =
44        Server::http(&address).map_err(|e| format!("Porta ocupada ou erro de rede: {}", e))?;
45
46    let (tx, rx) = mpsc::channel::<String>();
47
48    thread::spawn(move || {
49        // Aguarda requisição (Timeout configurável)
50        if let Ok(Some(request)) =
51            server.recv_timeout(Duration::from_secs(OAUTH_CALLBACK_TIMEOUT_SECS))
52        {
53            // Parser da URL de callback
54            let url_string = format!("http://localhost{}", request.url());
55            if let Ok(url) = Url::parse(&url_string) {
56                // Busca o parâmetro ?code=...
57                if let Some((_, code)) = url.query_pairs().find(|(k, _)| k == "code") {
58                    let _ = tx.send(code.to_string());
59
60                    // Resposta bonita para o usuário no navegador
61                    let html = "
62                        <html>
63                        <body style='background:#1a1a1a; color:#fff; font-family:sans-serif; text-align:center; padding-top:50px;'>
64                            <h1 style='color:#4ade80'>Login Concluído!</h1>
65                            <p>Você já pode fechar esta janela e voltar para o Playlite.</p>
66                            <script>window.close();</script>
67                        </body>
68                        </html>
69                    ";
70                    let _ = request.respond(
71                        Response::from_string(html).with_header(
72                            tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"text/html"[..])
73                                .unwrap(),
74                        ),
75                    );
76                    return;
77                }
78            }
79            // Se não achou code, responde erro
80            let _ = request.respond(Response::from_string("Erro: Código não encontrado."));
81        }
82    });
83
84    // Aguarda o canal enviar o código
85    rx.recv_timeout(Duration::from_secs(OAUTH_CALLBACK_TIMEOUT_SECS))
86        .map_err(|_| "Tempo limite de login excedido.".to_string())
87}