qr-login: add a --image
argument for providing an image to scan for the QR code (#381)
closes #380
This commit is contained in:
parent
c7fefc1452
commit
ab5cb00ee3
5 changed files with 753 additions and 55 deletions
725
Cargo.lock
generated
725
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -55,7 +55,7 @@ uuid = { version = "0.8", features = ["v4"] }
|
|||
steamguard = { version = "^0.13.0", path = "./steamguard" }
|
||||
dirs = "3.0.2"
|
||||
aes = { version = "0.8.3", features = ["zeroize"] }
|
||||
thiserror = "1.0.26"
|
||||
thiserror = "1.0.61"
|
||||
crossterm = { version = "0.23.2", features = ["event-stream"] }
|
||||
qrcode = { version = "0.12.0", optional = true }
|
||||
gethostname = "0.4.3"
|
||||
|
@ -73,6 +73,8 @@ argon2 = { version = "0.5.0", features = ["std", "zeroize"] }
|
|||
pbkdf2 = { version = "0.12.1", features = ["parallel"] }
|
||||
sha1 = "0.10.5"
|
||||
rayon = "1.7.0"
|
||||
rqrr = "0.7.1"
|
||||
image = "0.25"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
|
@ -339,8 +339,9 @@ impl AccountManager {
|
|||
debug!("Adding missing account names");
|
||||
for i in 0..self.manifest.entries.len() {
|
||||
let account = self.load_account_by_entry(&self.manifest.entries[i].clone())?;
|
||||
self.manifest.entries[i].account_name =
|
||||
account.lock().unwrap().account_name.clone();
|
||||
self.manifest.entries[i]
|
||||
.account_name
|
||||
.clone_from(&account.lock().unwrap().account_name);
|
||||
}
|
||||
upgraded = true;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use log::*;
|
||||
use rqrr::PreparedImage;
|
||||
use steamguard::{QrApprover, QrApproverError};
|
||||
|
||||
use crate::AccountManager;
|
||||
|
@ -10,11 +14,8 @@ use super::*;
|
|||
#[derive(Debug, Clone, Parser)]
|
||||
#[clap(about = "Log in to Steam on another device using the QR code that it's displaying.")]
|
||||
pub struct QrLoginCommand {
|
||||
#[clap(
|
||||
long,
|
||||
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,
|
||||
#[clap(flatten)]
|
||||
login_url_source: LoginUrlSource,
|
||||
}
|
||||
|
||||
impl<T> AccountCommand<T> for QrLoginCommand
|
||||
|
@ -32,6 +33,11 @@ where
|
|||
accounts.len() == 1,
|
||||
"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();
|
||||
|
||||
|
@ -41,6 +47,8 @@ where
|
|||
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 {
|
||||
let Some(tokens) = account.tokens.as_ref() else {
|
||||
error!(
|
||||
|
@ -51,7 +59,7 @@ where
|
|||
};
|
||||
|
||||
let mut approver = QrApprover::new(transport.clone(), tokens);
|
||||
match approver.approve(&account, &self.url) {
|
||||
match approver.approve(&account, url.to_owned()) {
|
||||
Ok(_) => {
|
||||
info!("Login approved.");
|
||||
break;
|
||||
|
@ -70,3 +78,55 @@ where
|
|||
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");
|
||||
}
|
||||
}
|
||||
|
|
BIN
src/fixtures/qr-codes/login-qr.png
Normal file
BIN
src/fixtures/qr-codes/login-qr.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
Loading…
Reference in a new issue