From 4f890b1ce8cbdd10bf1e7f446197c439ea9a0dda Mon Sep 17 00:00:00 2001 From: "Christopher L. Crutchfield" Date: Fri, 6 Dec 2024 19:53:30 -0800 Subject: [PATCH] feat: use device id so that we do not need to provide a totp --- .vscode/launch.json | 7 +++ Cargo.lock | 35 ++++++++++++++- Cargo.toml | 1 + src/credential_manager.rs | 10 +---- src/subcommands/login_subcommand.rs | 35 ++++++++++++--- src/subcommands/main_subcommand.rs | 2 +- src/synology_api/file_station.rs | 69 +++++++++++++++++------------ src/synology_api/mod.rs | 3 +- src/synology_api/responses.rs | 3 +- 9 files changed, 116 insertions(+), 49 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 4ab27d6..1bcd5b7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,6 +6,13 @@ "request": "launch", "name": "Launch", "program": "${workspaceFolder}/target/debug/git-lfs-synology", + "args": [ + "login", + "--url", + "https://e4e-nas.ucsd.edu:6021", + "--user", + "ccrutchf" + ] } ] } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index fffa8cb..2916277 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -612,6 +612,7 @@ dependencies = [ "futures-macro", "futures-util", "gix-config", + "hostname", "keyring", "named-lock", "num-derive", @@ -918,6 +919,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows 0.52.0", +] + [[package]] name = "http" version = "1.1.0" @@ -1398,7 +1410,7 @@ dependencies = [ "once_cell", "parking_lot", "thiserror 1.0.69", - "windows", + "windows 0.53.0", ] [[package]] @@ -2638,13 +2650,32 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efc5cf48f83140dcaab716eeaea345f9e93d0018fb81162753a3f76c3397b538" dependencies = [ - "windows-core", + "windows-core 0.53.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ "windows-targets 0.52.6", ] diff --git a/Cargo.toml b/Cargo.toml index d94009d..ac5053f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ educe = "0.6.0" futures-macro = "0.3.31" futures-util = "0.3.31" gix-config = "0.42.0" +hostname = "0.4.0" keyring = { version = "3.6.1", features = ["apple-native", "windows-native", "sync-secret-service"] } named-lock = "0.4.1" num-derive = "0.4.2" diff --git a/src/credential_manager.rs b/src/credential_manager.rs index 0be4c8a..01a15d9 100644 --- a/src/credential_manager.rs +++ b/src/credential_manager.rs @@ -33,22 +33,15 @@ pub struct Credential { pub user: String, #[educe(Debug(ignore))] // Do not include password in logs. pub password: String, - pub totp: Option, pub device_id: Option } impl Credential { #[tracing::instrument] pub fn new(user: String, password: String) -> Credential { - Credential::new_totp(user, password, None) - } - - #[tracing::instrument] - pub fn new_totp(user: String, password: String, totp: Option) -> Credential { Credential { user, password, - totp, device_id: None } } @@ -158,12 +151,11 @@ impl CredentialManager { Ok(version) => { match version { 0 => { - conn.execute( + conn.execute_batch( "ALTER TABLE Credentials DROP COLUMN totp_command_encrypted; ALTER TABLE Credentials DROP COLUMN totp_nonce; ALTER TABLE Credentials ADD COLUMN device_id_encrypted BLOB; ALTER TABLE Credentials ADD COLUMN device_id_nonce BLOB;", - (), // empty list of parameters. )?; self.create_tables(&conn) diff --git a/src/subcommands/login_subcommand.rs b/src/subcommands/login_subcommand.rs index ab71ace..a68b8df 100644 --- a/src/subcommands/login_subcommand.rs +++ b/src/subcommands/login_subcommand.rs @@ -1,11 +1,23 @@ use anyhow::{Context, Result}; use clap::ArgMatches; +use std::io::{self, Write}; use crate::credential_manager::{Credential, CredentialManager}; -use crate::synology_api::SynologyFileStation; +use crate::synology_api::{SynologyErrorStatus, SynologyFileStation}; use super::Subcommand; +fn get_input(prompt: &str) -> Result{ + print!("{}",prompt); + io::stdout().flush()?; + let mut input = String::new(); + match io::stdin().read_line(&mut input) { + Ok(_goes_into_input_above) => {}, + Err(_no_updates_is_fine) => {}, + } + Ok(input.trim().to_string()) +} + #[derive(Debug)] pub struct LoginSubcommand { } @@ -19,23 +31,34 @@ impl Subcommand for LoginSubcommand { let mut credential_manager = CredentialManager::new()?; let password: String; - let credential_ref: Option; + let device_id: Option; if credential_manager.has_credential(url)? { let credential = credential_manager.get_credential(url)?.context("Credential should not be null")?; password = credential.password.clone(); - credential_ref = Some(credential); + device_id = credential.device_id; } else { password = rpassword::prompt_password("Synology NAS Password: ")?; - credential_ref = None; + device_id = None; } - let credential = Credential::new( + let mut credential = Credential::new( user.clone(), password.clone()); + credential.device_id = device_id; let mut file_station = SynologyFileStation::new(url); - file_station.login(&credential).await?; + let credential = match file_station.login(&credential, false, None).await { + Ok(credential) => Ok(credential), + Err(error) => match error { + SynologyErrorStatus::NoTotp => { + let totp = get_input("TOTP: ")?; + + file_station.login(&credential, true, Some(totp)).await + }, + _ => Err(error) + } + }?; credential_manager.set_credential(url, &credential)?; diff --git a/src/subcommands/main_subcommand.rs b/src/subcommands/main_subcommand.rs index 0bb4651..999c243 100644 --- a/src/subcommands/main_subcommand.rs +++ b/src/subcommands/main_subcommand.rs @@ -73,7 +73,7 @@ impl CustomTransferAgent for MainSubcommand { let mut file_station = SynologyFileStation::new(nas_url); let credential = credential_manager.get_credential(nas_url)?.context("Credential should not be null")?; - match file_station.login(&credential).await { + match file_station.login(&credential, false, None).await { Ok(_) => Ok(()), Err(error) => { error_init(1, error.to_string().as_str())?; diff --git a/src/synology_api/file_station.rs b/src/synology_api/file_station.rs index cd340ae..29a164f 100644 --- a/src/synology_api/file_station.rs +++ b/src/synology_api/file_station.rs @@ -207,18 +207,46 @@ impl SynologyFileStation { } #[tracing::instrument] - pub async fn login(&mut self, credential: &Credential) -> Result<(), SynologyErrorStatus> { - let login_url = format!( - "{}/webapi/entry.cgi?api=SYNO.API.Auth&version={}&method=login&account={}&passwd={}&session=FileStation&fromat=sid", + pub async fn login(&mut self, credential: &Credential, enable_device_token: bool, totp: Option) -> Result { + let device_name = format!( + "{}::{}", + hostname::get()?.to_string_lossy(), + "rust_synology_api" + ); + + let mut login_url = format!( + "{}/webapi/entry.cgi?api=SYNO.API.Auth&version={}&method=login&account={}&passwd={}&enable_device_token={}&device_name={}&session=FileStation&fromat=sid", self.url, 6, - credential.user, - encode(credential.password.as_str()) // Encode the password in case it has characters not allowed in URLs in it. + encode(credential.user.as_str()), + encode(credential.password.as_str()), // Encode the password in case it has characters not allowed in URLs in it. + enable_device_token, + device_name ); + if let Some(did) = credential.device_id.clone() { + info!("Credential has device ID"); + + login_url = format!( + "{}&device_id={}", + login_url, + did + ) + } + + if let Some(totp) = totp { + info!("TOTP has been provided."); + + login_url = format!( + "{}&otp_code={}", + login_url, + totp + ) + } + // Make initial request to the server. This will fail if the user needs a TOTP. let response = reqwest::get(login_url).await; - let (mut login_result, login_error) = self.parse_data_and_error::(response).await?; + let (login_result, login_error) = self.parse_data_and_error::(response).await?; match login_error { Some(login_error) => @@ -232,28 +260,8 @@ impl SynologyFileStation { Some(errors) => if errors.types.iter().any(|f| f.contains_key("type") && f["type"] == "otp") { info!("Server requested TOTP"); - let totp: Option = None; - - match totp { - Some(totp) => { - info!("Requested TOTP from TOTP command"); - - let login_url = format!( - "{}/webapi/entry.cgi?api=SYNO.API.Auth&version={}&method=login&account={}&passwd={}&session=FileStation&fromat=sid&otp_code={}", - self.url, - 6, - credential.user, - errors.token, - totp - ); - - let response = reqwest::get(login_url).await; - login_result= Some(self.parse::(response).await?); - - Ok(()) - }, - None => Err(SynologyErrorStatus::NoTotp) - } + + Err(SynologyErrorStatus::NoTotp) } else { Err(SynologyErrorStatus::ServerError(SynologyStatusCode::InvalidUserDoesThisFileOperation)) @@ -272,7 +280,10 @@ impl SynologyFileStation { Some(login_result) => { self.sid = Some(login_result.sid); - Ok(()) + let mut cred = Credential::new(credential.user.to_string(), credential.password.to_string()); + cred.device_id = login_result.did; + + Ok(cred) }, None => Err(SynologyErrorStatus::UnknownError) } diff --git a/src/synology_api/mod.rs b/src/synology_api/mod.rs index 54e5ce0..96d4a9a 100644 --- a/src/synology_api/mod.rs +++ b/src/synology_api/mod.rs @@ -3,4 +3,5 @@ mod progress_reporter; mod responses; pub use file_station::SynologyFileStation; -pub use progress_reporter::ProgressReporter; \ No newline at end of file +pub use progress_reporter::ProgressReporter; +pub use responses::SynologyErrorStatus; \ No newline at end of file diff --git a/src/synology_api/responses.rs b/src/synology_api/responses.rs index 388cf23..b3c7d93 100644 --- a/src/synology_api/responses.rs +++ b/src/synology_api/responses.rs @@ -116,7 +116,8 @@ pub struct SynologyError { #[derive(Debug, Serialize, Deserialize, Clone)] #[serde[rename_all = "snake_case"]] pub struct LoginResponse { - pub sid: String + pub sid: String, + pub did: Option } #[derive(Debug, Serialize, Deserialize, Clone)]