mirror of
https://github.com/narrrl/proton-sdk-rs.git
synced 2026-11-03 16:41:26 +01:00
bumped deps + fixed deprecations + session for live tests
This commit is contained in:
1 parent
b9742e122a
commit
70e573ebe1
10 files changed
+761
-312
No files matched your search
@@ -11,7 +11,7 @@
|
||||
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use hmac::{Hmac, Mac};
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use proton_drive_sdk::ProtonDriveClient;
|
||||
use proton_sdk::config::ProtonClientConfiguration;
|
||||
use proton_sdk::session::ProtonApiSession;
|
||||
|
||||
@@ -22,7 +22,7 @@ use proton_sdk::ids::{AddressId, DriveEventId, LinkId, NodeUid, ShareId, VolumeI
|
||||
use proton_sdk::session::ProtonApiSession;
|
||||
use proton_sdk::telemetry::{NoopTelemetry, Telemetry, TelemetryExt};
|
||||
|
||||
use hmac::{Hmac, Mac};
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use sha1::Sha1;
|
||||
|
||||
use crate::cache::DriveEntityCache;
|
||||
|
||||
@@ -13,10 +13,11 @@
|
||||
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use hmac::{Hmac, Mac};
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use proton_drive_sdk::ProtonDriveClient;
|
||||
use proton_sdk::config::ProtonClientConfiguration;
|
||||
use proton_sdk::session::ProtonApiSession;
|
||||
use proton_sdk::session::{PasswordMode, ProtonApiSession, ResumeParameters};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha1::Sha1;
|
||||
|
||||
// Must follow Proton's required shape (sdk/README.md "Operational requirements").
|
||||
@@ -51,6 +52,12 @@ pub async fn live_client() -> Option<&'static LiveClient> {
|
||||
}
|
||||
|
||||
/// Perform the one-time SRP + 2FA login and build the client.
|
||||
///
|
||||
/// To avoid burning a TOTP window (and tripping Proton anti-abuse) on every run,
|
||||
/// a successful login is persisted to a gitignored cache file. On the next run we
|
||||
/// [`ProtonApiSession::resume`] from that file instead of doing SRP+2FA again; the
|
||||
/// stored refresh token auto-refreshes via the http client's 401 path, so no TOTP
|
||||
/// is needed. A missing/dead cache falls back to a fresh login.
|
||||
async fn build_live_client() -> Option<LiveClient> {
|
||||
let (username, password) = match read_dotenv() {
|
||||
Ok(creds) => creds,
|
||||
@@ -59,6 +66,33 @@ async fn build_live_client() -> Option<LiveClient> {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let config = ProtonClientConfiguration::new(APP_VERSION).with_user_agent(USER_AGENT);
|
||||
|
||||
// Fast path: resume a cached session (no TOTP). Validate with one cheap
|
||||
// authenticated call so a dead/revoked session falls through to a fresh login.
|
||||
if let Some(stored) = load_cached_session() {
|
||||
let session = ProtonApiSession::resume(config.clone(), stored.into_params())
|
||||
.expect("resume cached session");
|
||||
match session
|
||||
.http()
|
||||
.get::<serde_json::Value>("core/v4/users")
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
eprintln!("[auth] resumed cached session (no TOTP)");
|
||||
// Refresh may have rotated the tokens; re-persist the current set.
|
||||
save_session(&session).await;
|
||||
let client = ProtonDriveClient::new(&session, password.into_bytes());
|
||||
return Some(LiveClient {
|
||||
client,
|
||||
_session: session,
|
||||
});
|
||||
}
|
||||
Err(e) => eprintln!("[auth] cached session invalid ({e}); logging in fresh"),
|
||||
}
|
||||
}
|
||||
|
||||
let totp_secret = match read_totp_secret() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
@@ -67,8 +101,6 @@ async fn build_live_client() -> Option<LiveClient> {
|
||||
}
|
||||
};
|
||||
|
||||
let config = ProtonClientConfiguration::new(APP_VERSION).with_user_agent(USER_AGENT);
|
||||
|
||||
let mut session = ProtonApiSession::begin(config, &username, password.as_bytes())
|
||||
.await
|
||||
.expect("SRP login failed");
|
||||
@@ -81,6 +113,7 @@ async fn build_live_client() -> Option<LiveClient> {
|
||||
.expect("2FA submission failed");
|
||||
}
|
||||
|
||||
save_session(&session).await;
|
||||
let client = ProtonDriveClient::new(&session, password.into_bytes());
|
||||
Some(LiveClient {
|
||||
client,
|
||||
@@ -88,6 +121,74 @@ async fn build_live_client() -> Option<LiveClient> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Persisted session credentials. Mirrors [`ResumeParameters`], serialized to a
|
||||
/// gitignored cache file so reruns can skip SRP+2FA.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct StoredSession {
|
||||
session_id: String,
|
||||
username: String,
|
||||
user_id: String,
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
scopes: Vec<String>,
|
||||
/// `1` = single, `2` = dual (matches Proton's wire value).
|
||||
password_mode: u8,
|
||||
}
|
||||
|
||||
impl StoredSession {
|
||||
fn into_params(self) -> ResumeParameters {
|
||||
ResumeParameters {
|
||||
session_id: self.session_id.into(),
|
||||
username: self.username,
|
||||
user_id: self.user_id.into(),
|
||||
access_token: self.access_token,
|
||||
refresh_token: self.refresh_token,
|
||||
scopes: self.scopes,
|
||||
is_waiting_for_second_factor_code: false,
|
||||
password_mode: match self.password_mode {
|
||||
1 => PasswordMode::Single,
|
||||
_ => PasswordMode::Dual,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Repo-root path of the gitignored session cache.
|
||||
fn session_cache_path() -> String {
|
||||
concat!(env!("CARGO_MANIFEST_DIR"), "/../../.proton_session.json").to_owned()
|
||||
}
|
||||
|
||||
/// Load the cached session, or `None` if absent/unparseable.
|
||||
fn load_cached_session() -> Option<StoredSession> {
|
||||
let text = std::fs::read_to_string(session_cache_path()).ok()?;
|
||||
serde_json::from_str(&text).ok()
|
||||
}
|
||||
|
||||
/// Persist the session's current tokens to the cache file (best-effort).
|
||||
async fn save_session(session: &ProtonApiSession) {
|
||||
let tokens = session.current_tokens().await;
|
||||
let stored = StoredSession {
|
||||
session_id: session.session_id().as_str().to_owned(),
|
||||
username: session.username().to_owned(),
|
||||
user_id: session.user_id().as_str().to_owned(),
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
scopes: session.scopes().to_vec(),
|
||||
password_mode: match session.password_mode() {
|
||||
PasswordMode::Single => 1,
|
||||
PasswordMode::Dual => 2,
|
||||
},
|
||||
};
|
||||
match serde_json::to_string_pretty(&stored) {
|
||||
Ok(json) => {
|
||||
if let Err(e) = std::fs::write(session_cache_path(), json) {
|
||||
eprintln!("[auth] could not write session cache: {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("[auth] could not serialize session: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal `.env` reader: `key=value` lines, trims whitespace, ignores `#`.
|
||||
fn read_dotenv() -> Result<(String, String), Box<dyn std::error::Error>> {
|
||||
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../.env");
|
||||
|
||||
Reference in new issue
Block a user