pub const DEFAULT_CLIENT_ID: &str =
"29377375716-g3p5kvq5v2vde0mb0fc7kdc534e59ljd.apps.googleusercontent.com";
pub const DEFAULT_CLIENT_SECRET: &str = "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";
use std::io::{BufRead, BufReader, Write as IoWrite};
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug)]
pub enum GDriveAuthMode {
Public,
ServiceAccount { path: PathBuf },
UserOAuth { access_token: String },
}
#[derive(Debug, Serialize, Deserialize)]
pub struct OAuthToken {
pub access_token: String,
pub refresh_token: Option<String>,
pub expires_in: Option<u64>,
}
pub fn build_oauth_auth_url(client_id: &str, redirect_uri: &str, state: &str) -> String {
let scope = "https://www.googleapis.com/auth/drive.readonly";
format!(
"https://accounts.google.com/o/oauth2/v2/auth\
?response_type=code\
&client_id={client_id}\
&redirect_uri={redirect_uri}\
&scope={scope}\
&state={state}\
&access_type=offline\
&prompt=consent"
)
}
pub fn parse_auth_code_from_redirect(url: &str) -> Option<String> {
let query = url.split('?').nth(1)?;
for param in query.split('&') {
if let Some(value) = param.strip_prefix("code=") {
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
pub fn token_cache_path() -> PathBuf {
#[cfg(target_os = "windows")]
let base = std::env::var("APPDATA")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("."));
#[cfg(not(target_os = "windows"))]
let base = std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
std::env::var("HOME")
.map(|h| PathBuf::from(h).join(".config"))
.unwrap_or_else(|_| PathBuf::from(".config"))
});
base.join("blazehash").join("gdrive_token.json")
}
pub fn save_token(token: &OAuthToken, path: &Path) -> Result<(), std::io::Error> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(token)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
std::fs::write(path, json)
}
pub fn load_token_from(path: &Path) -> Option<OAuthToken> {
let data = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&data).ok()
}
pub fn load_token() -> Option<OAuthToken> {
load_token_from(&token_cache_path())
}
pub fn find_available_port() -> Result<u16, std::io::Error> {
let listener = TcpListener::bind("127.0.0.1:0")?;
Ok(listener.local_addr()?.port())
}
pub fn exchange_code_for_token(
client_id: &str,
client_secret: &str,
code: &str,
redirect_uri: &str,
token_endpoint: &str,
) -> Result<OAuthToken, Box<dyn std::error::Error>> {
let body = format!(
"grant_type=authorization_code&client_id={client_id}&client_secret={client_secret}\
&code={code}&redirect_uri={redirect_uri}"
);
let response = ureq::post(token_endpoint)
.set("Content-Type", "application/x-www-form-urlencoded")
.send_string(&body)?;
if response.status() != 200 {
return Err(format!(
"token endpoint returned HTTP {}: {}",
response.status(),
response.into_string().unwrap_or_default()
)
.into());
}
let token: OAuthToken = response.into_json()?;
Ok(token)
}
pub fn initiate_browser_auth(
token_endpoint: Option<&str>,
) -> Result<String, Box<dyn std::error::Error>> {
let client_id = std::env::var("BLAZEHASH_GDRIVE_CLIENT_ID")
.unwrap_or_else(|_| DEFAULT_CLIENT_ID.to_string());
let client_secret = std::env::var("BLAZEHASH_GDRIVE_CLIENT_SECRET")
.unwrap_or_else(|_| DEFAULT_CLIENT_SECRET.to_string());
let listener = TcpListener::bind("127.0.0.1:0")?;
let port = listener.local_addr()?.port();
let redirect_uri = format!("http://localhost:{port}/callback");
let state = format!("blazehash-{}", std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs());
let auth_url = build_oauth_auth_url(&client_id, &redirect_uri, &state);
eprintln!("\nOpen this URL to authorise blazehash to access Google Drive:");
eprintln!("\n {auth_url}\n");
let _ = std::process::Command::new("open").arg(&auth_url).status()
.or_else(|_| std::process::Command::new("xdg-open").arg(&auth_url).status())
.or_else(|_| std::process::Command::new("start").arg(&auth_url).status());
eprintln!("Waiting for browser redirect...");
let (stream, _) = listener.accept()?;
let code = read_code_from_callback(stream)?;
let endpoint = token_endpoint.unwrap_or("https://oauth2.googleapis.com/token");
let token = exchange_code_for_token(&client_id, &client_secret, &code, &redirect_uri, endpoint)?;
let cache = token_cache_path();
save_token(&token, &cache)?;
let access_token = token.access_token.clone();
eprintln!("Authentication successful. Token cached at {}", cache.display());
Ok(access_token)
}
fn read_code_from_callback(stream: TcpStream) -> Result<String, Box<dyn std::error::Error>> {
let mut reader = BufReader::new(&stream);
let mut request_line = String::new();
reader.read_line(&mut request_line)?;
let path = request_line
.split_whitespace()
.nth(1)
.ok_or("malformed HTTP request")?;
let full_url = format!("http://localhost{path}");
let code = parse_auth_code_from_redirect(&full_url)
.ok_or("no authorization code in callback URL")?;
let html = b"<html><body><h1>blazehash: authorised.</h1><p>You may close this tab.</p></body></html>";
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
html.len()
);
(&stream).write_all(response.as_bytes())?;
(&stream).write_all(html)?;
Ok(code)
}
pub fn resolve_auth_mode() -> GDriveAuthMode {
if let Ok(path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
let p = PathBuf::from(&path);
if p.exists() {
return GDriveAuthMode::ServiceAccount { path: p };
}
}
if let Some(token) = load_token() {
return GDriveAuthMode::UserOAuth {
access_token: token.access_token,
};
}
GDriveAuthMode::Public
}