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" }
|
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"
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
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