use super::html::{Page, TwoFaMethods};
use super::{COOKIE, CAS, Error, HOST, PERSIST_COOKIE, Result, totp};
use rikka::{Redirect, Request};
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct PendingAuth {
fields: BTreeMap<String, String>,
pub solved: bool,
pub is_email_available: bool,
pub is_totp_available: bool,
}
impl PendingAuth {
pub fn from_html(html: &str) -> Self {
let page = Page::parse(html);
let solved = page.form_action() == Some("/registerbrowser");
let methods = if solved {
TwoFaMethods::default()
} else {
page.twofa_methods()
};
Self {
fields: page.hidden_fields(),
solved,
is_email_available: methods.email,
is_totp_available: methods.totp,
}
}
pub async fn send_email_code(&mut self) -> Result<()> {
self.use_method("mail").await
}
pub async fn solve_with_email_code(&mut self, code: &str) -> Result<()> {
self.solve("mail2fcheck", code).await
}
pub async fn solve_with_totp(&mut self, code: &str) -> Result<()> {
self.use_method("totp").await?;
self.solve("totp2fcheck", code).await
}
pub async fn finish(&mut self) -> Result<CAS> {
if !self.solved {
return Err(Error::Api("2fa not solved".into()));
}
let key = self
.fields
.get("totpsecret")
.map(|secret| secret.to_uppercase())
.ok_or_else(|| Error::Api("'totpsecret' is not available".into()))?;
self.fields
.insert("fg".to_owned(), format!("TOTP_{}", totp::generate(&key)?));
let response = Request::builder(format!("{HOST}/registerbrowser"))
.post()
.redirect(Redirect::Manual)
.form(self.body())
.send()
.await?;
match (
response.set_cookie_value(COOKIE),
response.set_cookie_value(PERSIST_COOKIE),
) {
(Some(cookie), Some(connection)) => Ok(CAS::new(cookie, connection, key)),
_ => Err(Error::Api("bad auth".into())),
}
}
async fn use_method(&mut self, choice: &str) -> Result<()> {
self.fields.insert("sf".to_owned(), choice.to_owned());
self.submit("2fchoice").await
}
async fn solve(&mut self, method: &str, code: &str) -> Result<()> {
self.fields.insert("code".to_owned(), code.to_owned());
self.fields
.insert("stayconnected".to_owned(), "1".to_owned());
self.submit(method).await?;
self.solved = true;
Ok(())
}
async fn submit(&mut self, route: &str) -> Result<()> {
let response = Request::builder(format!("{HOST}/{route}"))
.post()
.form(self.body())
.send()
.await?;
self.fields = Page::parse(&response.text()).hidden_fields();
Ok(())
}
fn body(&self) -> Vec<(String, String)> {
self.fields
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::PendingAuth;
fn fixture(name: &str) -> String {
std::fs::read_to_string(format!("{}/tests/{name}", env!("CARGO_MANIFEST_DIR")))
.expect("fixture should exist")
}
#[test]
fn parses_2fa_choice_form() {
let auth = PendingAuth::from_html(&fixture("2fchoice.html"));
assert!(!auth.solved);
assert!(auth.is_email_available);
assert!(auth.is_totp_available);
}
#[test]
fn code_entry_pages_offer_no_method_choice() {
for name in ["mail2fcheck.html", "totp2fcheck.html"] {
let auth = PendingAuth::from_html(&fixture(name));
assert!(!auth.solved, "{name} should not be solved");
assert!(!auth.is_email_available, "{name}");
assert!(!auth.is_totp_available, "{name}");
}
}
}