use anyhow::{bail, Context, Result};
use std::sync::{Arc, Mutex};
use std::time::Duration;
const AUTHORIZE_URL: &str = "https://cloudsign.webnotarius.pl/idp/oauth2.0/authorize";
const LOGIN_URL: &str = "https://cloudsign.webnotarius.pl/idp/login";
const TOKEN_URL: &str = "https://cloudsign.webnotarius.pl/idp/oauth2.0/accessToken";
const SCOPE: &str = "https://cloudsign.webnotarius.pl/idp/oauth2.0/profile";
const REDIRECT_URI: &str = "https://cloudsign.webnotarius.pl/redirect";
const CLIENT_ID: &str = "44rvDKKEWY53a7xBeF5w";
const CLIENT_SECRET: &str = "BRSE2u2nY3p3m77QHTt8";
pub struct Token(pub String);
pub fn login(email: &str, otp_code: &str) -> Result<Token> {
let seen: Arc<Mutex<Vec<reqwest::Url>>> = Arc::new(Mutex::new(Vec::new()));
let seen_cl = seen.clone();
let redirect = reqwest::redirect::Policy::custom(move |attempt| {
seen_cl.lock().unwrap().push(attempt.url().clone());
if attempt.previous().len() > 20 {
attempt.error("too many redirects")
} else {
attempt.follow()
}
});
let client = reqwest::blocking::Client::builder()
.cookie_store(true)
.redirect(redirect)
.timeout(Duration::from_secs(30))
.user_agent("ssign")
.build()
.context("building HTTP client")?;
let resp = client
.get(AUTHORIZE_URL)
.query(&[
("response_type", "code"),
("client_id", CLIENT_ID),
("redirect_uri", REDIRECT_URI),
("scope", SCOPE),
("api_key", ""),
])
.send()
.context("authorize request")?;
let login_url = resp.url().clone();
let html = resp.text().context("reading login page")?;
let execution = extract_hidden(&html, "execution")
.context("could not find the `execution` field on the login page")?;
let service = login_url
.query_pairs()
.find(|(k, _)| k == "service")
.map(|(_, v)| v.into_owned());
seen.lock().unwrap().clear();
let mut post = client.post(LOGIN_URL);
if let Some(s) = &service {
post = post.query(&[("service", s.as_str())]);
}
let resp = post
.form(&[
("username", email),
("password", otp_code),
("execution", execution.as_str()),
("_eventId", "submit"),
("geolocation", ""),
("submit", "LOGIN"),
("lt", ""),
])
.send()
.context("login POST")?;
let final_url = resp.url().clone();
let code = seen
.lock()
.unwrap()
.iter()
.chain(std::iter::once(&final_url))
.find_map(|u| {
u.query_pairs()
.find(|(k, _)| k == "code")
.map(|(_, v)| v.into_owned())
})
.context("no authorization code after login — wrong e-mail or OTP?")?;
let resp = client
.post(TOKEN_URL)
.query(&[
("client_id", CLIENT_ID),
("client_secret", CLIENT_SECRET),
("scope", SCOPE),
("code", code.as_str()),
("redirect_uri", REDIRECT_URI),
("grant_type", "authorization_code"),
])
.send()
.context("accessToken request")?;
let status = resp.status();
let body: serde_json::Value = resp
.json()
.with_context(|| format!("accessToken response was not JSON (status {status})"))?;
match body.get("access_token").and_then(|v| v.as_str()) {
Some(tok) => Ok(Token(tok.to_string())),
None => bail!("token exchange failed (status {status}): {body}"),
}
}
fn extract_hidden(html: &str, name: &str) -> Option<String> {
let by_name_then_value = regex::Regex::new(&format!(
r#"(?is)<input[^>]*\bname=["']{}["'][^>]*\bvalue=["']([^"']*)["']"#,
regex::escape(name)
))
.unwrap();
let by_value_then_name = regex::Regex::new(&format!(
r#"(?is)<input[^>]*\bvalue=["']([^"']*)["'][^>]*\bname=["']{}["']"#,
regex::escape(name)
))
.unwrap();
by_name_then_value
.captures(html)
.or_else(|| by_value_then_name.captures(html))
.map(|c| c[1].to_string())
}