qr-login: add a --image argument for providing an image to scan for the QR code (#381)

closes #380
This commit is contained in:
Carson McManus 2024-06-01 18:47:24 -04:00 committed by GitHub
parent c7fefc1452
commit ab5cb00ee3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 753 additions and 55 deletions

725
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -55,7 +55,7 @@ uuid = { version = "0.8", features = ["v4"] }
steamguard = { version = "^0.13.0", path = "./steamguard" } steamguard = { version = "^0.13.0", path = "./steamguard" }
dirs = "3.0.2" dirs = "3.0.2"
aes = { version = "0.8.3", features = ["zeroize"] } aes = { version = "0.8.3", features = ["zeroize"] }
thiserror = "1.0.26" thiserror = "1.0.61"
crossterm = { version = "0.23.2", features = ["event-stream"] } crossterm = { version = "0.23.2", features = ["event-stream"] }
qrcode = { version = "0.12.0", optional = true } qrcode = { version = "0.12.0", optional = true }
gethostname = "0.4.3" gethostname = "0.4.3"
@ -73,6 +73,8 @@ argon2 = { version = "0.5.0", features = ["std", "zeroize"] }
pbkdf2 = { version = "0.12.1", features = ["parallel"] } pbkdf2 = { version = "0.12.1", features = ["parallel"] }
sha1 = "0.10.5" sha1 = "0.10.5"
rayon = "1.7.0" rayon = "1.7.0"
rqrr = "0.7.1"
image = "0.25"
[dev-dependencies] [dev-dependencies]
tempfile = "3" tempfile = "3"

View file

@ -339,8 +339,9 @@ impl AccountManager {
debug!("Adding missing account names"); debug!("Adding missing account names");
for i in 0..self.manifest.entries.len() { for i in 0..self.manifest.entries.len() {
let account = self.load_account_by_entry(&self.manifest.entries[i].clone())?; let account = self.load_account_by_entry(&self.manifest.entries[i].clone())?;
self.manifest.entries[i].account_name = self.manifest.entries[i]
account.lock().unwrap().account_name.clone(); .account_name
.clone_from(&account.lock().unwrap().account_name);
} }
upgraded = true; upgraded = true;
} }

View file

@ -1,6 +1,10 @@
use std::sync::{Arc, Mutex}; use std::{
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use log::*; use log::*;
use rqrr::PreparedImage;
use steamguard::{QrApprover, QrApproverError}; use steamguard::{QrApprover, QrApproverError};
use crate::AccountManager; use crate::AccountManager;
@ -10,11 +14,8 @@ use super::*;
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
#[clap(about = "Log in to Steam on another device using the QR code that it's displaying.")] #[clap(about = "Log in to Steam on another device using the QR code that it's displaying.")]
pub struct QrLoginCommand { pub struct QrLoginCommand {
#[clap( #[clap(flatten)]
long, login_url_source: LoginUrlSource,
help = "The URL that would normally open in the Steam app. This is the URL that the QR code is displaying. It should start with \"https://s.team/...\""
)]
pub url: String,
} }
impl<T> AccountCommand<T> for QrLoginCommand impl<T> AccountCommand<T> for QrLoginCommand
@ -32,6 +33,11 @@ where
accounts.len() == 1, accounts.len() == 1,
"You can only log in to one account at a time." "You can only log in to one account at a time."
); );
// FIXME: in clap v4, this constraint can be expressed as a arg group: https://stackoverflow.com/questions/76315540/how-do-i-require-one-of-the-two-clap-options
ensure!(
self.login_url_source.url.is_some() || self.login_url_source.image.is_some(),
"You must provide either a URL with --url or an image file with --image."
);
let mut account = accounts[0].lock().unwrap(); let mut account = accounts[0].lock().unwrap();
@ -41,6 +47,8 @@ where
crate::do_login(transport.clone(), &mut account, args.password.clone())?; crate::do_login(transport.clone(), &mut account, args.password.clone())?;
} }
let url = self.login_url_source.url()?;
debug!("Using login URL to approve: {}", url);
loop { loop {
let Some(tokens) = account.tokens.as_ref() else { let Some(tokens) = account.tokens.as_ref() else {
error!( error!(
@ -51,7 +59,7 @@ where
}; };
let mut approver = QrApprover::new(transport.clone(), tokens); let mut approver = QrApprover::new(transport.clone(), tokens);
match approver.approve(&account, &self.url) { match approver.approve(&account, url.to_owned()) {
Ok(_) => { Ok(_) => {
info!("Login approved."); info!("Login approved.");
break; break;
@ -70,3 +78,55 @@ where
Ok(()) Ok(())
} }
} }
#[derive(Debug, Clone, clap::Args)]
pub struct LoginUrlSource {
/// The URL that would normally open in the Steam app. This is the URL that the QR code is displaying. It should start with \"https://s.team/...\"
#[clap(long)]
url: Option<String>,
/// Path to an image file containing the QR code. The QR code will be scanned from this image.
#[clap(long)]
image: Option<PathBuf>,
}
impl LoginUrlSource {
fn url(&self) -> anyhow::Result<String> {
match self {
Self { url: Some(url), .. } => Ok(url.clone()),
Self {
image: Some(path), ..
} => read_qr_image(path),
_ => Err(anyhow!(
"You must provide either a URL with --url or an image file with --image."
)),
}
}
}
fn read_qr_image(path: &Path) -> anyhow::Result<String> {
use image::io::Reader as ImageReader;
let image = ImageReader::open(path)?.decode()?.to_luma8();
let mut img = PreparedImage::prepare(image);
let grids = img.detect_grids();
for grid in grids {
let (_meta, text) = grid.decode()?;
// a rough validation that the QR code is a Steam login code
if text.contains("s.team") {
return Ok(text);
}
}
Err(anyhow!("No Steam login url found in the QR code"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_read_qr_image() {
let path = Path::new("src/fixtures/qr-codes/login-qr.png");
let url = read_qr_image(path).unwrap();
assert_eq!(url, "https://s.team/q/1/2372462679780599330");
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB