diff --git a/.gitignore b/.gitignore index ea8c4bf..96ef6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index 3fab193..0013026 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +derive_builder = "0.11" +rand = { version = "0.8", features = ["getrandom"] } +regex = "1.5" reqwest = { version = "0.11", features = ["json", "cookies"] } +thiserror = "1.0" [[bin]] name = "vodafone_runner" diff --git a/src/lib.rs b/src/lib.rs index 8cd4114..ef5f593 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,4 @@ pub mod station; + +#[macro_use] +extern crate derive_builder; diff --git a/src/station.rs b/src/station.rs index 4a124f1..84b0545 100644 --- a/src/station.rs +++ b/src/station.rs @@ -1,43 +1,157 @@ pub mod station { - pub use reqwest::Client; - use reqwest::header; - use std::time::Duration; + pub use reqwest::Client; - #[derive(Debug)] - pub struct VodafoneStation { - host: String, - client: Client, - session_id: String, - nonce: String, - csrf_nonce: String, - init_vector: String, - salt: String, - key: String, - cookie: String, - } + use derive_builder::UninitializedFieldError; + use rand::{rngs::OsRng, RngCore}; + use regex::Regex; + use reqwest::header; + use std::time::Duration; + use thiserror::Error; - impl VodafoneStation { - pub fn new(host: String) -> Self { - let mut headers = header::HeaderMap::new(); - headers.insert("X-Requested-With", header::HeaderValue::from_static("XMLHttpRequest")); - headers.insert(header::REFERER, - header::HeaderValue::from_str(&format!("http://{}/?overview", host)) - .expect("Host name is not valid ASCII!")); - headers.insert(header::ORIGIN, - header::HeaderValue::from_str(&format!("http://{}", host)) - .expect("Host name is not valid ASCII!")); + #[derive(Debug, Error)] + pub enum StationError { + #[error("request error")] + Request(#[from] reqwest::Error), + #[error("could not parse data: {0}")] + Parse(String), + } - let client = Client::builder() - .default_headers(headers) - .user_agent("Mozilla/5.0 (Windows NT 6.1; rv:91.0) Gecko/20100101 Firefox/91.0") - .cookie_store(true) - .timeout(Duration::from_secs(10)) - .build().expect("Could not construct reqwest client."); - - VodafoneStation { - host: host, - - } - } - } + #[derive(Debug, Builder)] + #[builder(build_fn(error = "StationError"))] + struct CryptoValues { + session_id: String, + nonce: String, + init_vector: String, + salt: String, + } + + impl From for StationError { + fn from(ufe: UninitializedFieldError) -> StationError { + StationError::Parse(ufe.to_string()) + } + } + + #[derive(Debug, Builder)] + struct LoginState { + csrf_nonce: String, + key: String, + } + + #[derive(Debug)] + pub struct VodafoneStation { + host: String, + client: Client, + crypto: Option, + state: Option, + } + + impl VodafoneStation { + pub fn new(host: &str) -> Self { + let mut headers = header::HeaderMap::new(); + headers.insert( + "X-Requested-With", + header::HeaderValue::from_static("XMLHttpRequest"), + ); + headers.insert( + header::REFERER, + header::HeaderValue::from_str(&format!("http://{}/?overview", host)) + .expect("Host name is not valid ASCII!"), + ); + headers.insert( + header::ORIGIN, + header::HeaderValue::from_str(&format!("http://{}", host)) + .expect("Host name is not valid ASCII!"), + ); + + let client = Client::builder() + .default_headers(headers) + .user_agent("Mozilla/5.0 (Windows NT 6.1; rv:91.0) Gecko/20100101 Firefox/91.0") + .cookie_store(true) + .timeout(Duration::from_secs(10)) + .build() + .expect("Could not construct reqwest client."); + + VodafoneStation { + host: host.to_owned(), + client, + crypto: None, + state: None, + } + } + + pub async fn connect(&mut self) -> Result<(), StationError> { + let response = self.client.get("/").send().await?; + let text = response.text().await?; + self.init_crypto(&text)?; + Ok(()) + } + + fn init_crypto(&mut self, response: &str) -> Result<(), StationError> { + let re = Regex::new( + r"(?x) # Ignore whitespace; allow line comments + (\W|^) [[:space:]]* # Start of Javascript statement + var [[:space:]]+ (?P[[:word:]]+) # variable declaration + [[:space:]]* = [[:space:]]* # Equals sign + '(?P[^']*)' # String value + [[:space:]]*; # End of statement + ", + ) + .unwrap(); + + let mut stbuild = CryptoValuesBuilder::default(); + + for caps in re.captures_iter(&response) { + match &caps["varName"] { + "currentSessionId" => { + stbuild.session_id(caps["strval"].to_owned()); + } + "myIv" => { + stbuild.init_vector(caps["strval"].to_owned()); + } + "mySalt" => { + stbuild.salt(caps["strval"].to_owned()); + } + _ => (), + } + } + + stbuild.nonce(OsRng.next_u32().to_string()); + + self.crypto = Some(stbuild.build()?); + + Ok(()) + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_init_crypto() { + let mut station = VodafoneStation::new("INV"); + let session_id = "cool_session"; + let iv = "so_initialized"; + let salt = "extremely_salty"; + station + .init_crypto( + format!( + r"var currentSessionId = '{}'; + var myIv = '{}'; + var mySalt = '{}'; + ", + &session_id, &iv, &salt + ) + .as_str(), + ) + .expect("Couldn't parse Javascript variable declarations."); + + assert!(station.crypto.is_some()); + let crypto = station.crypto.unwrap(); + assert_eq!(crypto.session_id, session_id); + assert_eq!(crypto.init_vector, iv); + assert_eq!(crypto.salt, salt); + assert!(crypto.nonce.len() > 0); + } + } }