From 9e929d1bae328dbc9af874dd0da2bc93f4687df7 Mon Sep 17 00:00:00 2001 From: lemonsh Date: Sat, 29 Apr 2023 13:18:07 +0200 Subject: [PATCH] initial commit --- .gitignore | 3 + Cargo.toml | 2 + README.md | 11 +++ connectbox-shell/Cargo.toml | 6 ++ connectbox-shell/src/main.rs | 3 + connectbox/Cargo.toml | 17 +++++ connectbox/src/error.rs | 21 ++++++ connectbox/src/functions.rs | 7 ++ connectbox/src/lib.rs | 125 +++++++++++++++++++++++++++++++++++ connectbox/src/models.rs | 34 ++++++++++ 10 files changed, 229 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 connectbox-shell/Cargo.toml create mode 100644 connectbox-shell/src/main.rs create mode 100644 connectbox/Cargo.toml create mode 100644 connectbox/src/error.rs create mode 100644 connectbox/src/functions.rs create mode 100644 connectbox/src/lib.rs create mode 100644 connectbox/src/models.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fceff28 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +dumps/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..02e208e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["connectbox", "connectbox-shell"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c760765 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# connectbox-rs +API client library for the Compal CH7465CE, which is a cable modem provided by various European ISPs under the name Connect Box. + +For more information, see the crate documentation. + +### Credits +Special thanks to the authors of the following projects: + * [home-assistant-ecosystem/python-connect-box](https://github.com/home-assistant-ecosystem/python-connect-box) + * [ties/compal_CH7465LG_py](https://github.com/ties/compal_CH7465LG_py) + +They have saved me from hours of reverse engineering work. \ No newline at end of file diff --git a/connectbox-shell/Cargo.toml b/connectbox-shell/Cargo.toml new file mode 100644 index 0000000..72c5346 --- /dev/null +++ b/connectbox-shell/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "connectbox-shell" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/connectbox-shell/src/main.rs b/connectbox-shell/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/connectbox-shell/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/connectbox/Cargo.toml b/connectbox/Cargo.toml new file mode 100644 index 0000000..616ec82 --- /dev/null +++ b/connectbox/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "connectbox" +version = "0.1.0" +edition = "2021" + +[dependencies] +tracing = { version = "0.1", optional = true } +thiserror = "1.0" +reqwest = { version = "0.11", default-features = false, features = ["cookies"] } +quick-xml = { version = "0.28", features = ["serialize"] } +serde = { version = "1.0", features = ["derive"] } +url = "2.3" + +[dev-dependencies] +color-eyre = "0.6" +tokio = { version = "1.0", default-features = false, features = ["macros"] } +tracing-subscriber = "0.3" diff --git a/connectbox/src/error.rs b/connectbox/src/error.rs new file mode 100644 index 0000000..28a89d4 --- /dev/null +++ b/connectbox/src/error.rs @@ -0,0 +1,21 @@ +use reqwest::header::ToStrError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("session token not found, are you logged in?")] + NoSessionToken, + #[error("incorrect password")] + IncorrectPassword, + #[error("unexpected response from the server: {0:?}")] + UnexpectedResponse(String), + + #[error(transparent)] + URLParseError(#[from] url::ParseError), + #[error(transparent)] + InvalidHeaderValue(#[from] ToStrError), + #[error(transparent)] + HttpError(#[from] reqwest::Error), + #[error(transparent)] + XMLDecodeError(#[from] quick_xml::de::DeError), +} diff --git a/connectbox/src/functions.rs b/connectbox/src/functions.rs new file mode 100644 index 0000000..7d26412 --- /dev/null +++ b/connectbox/src/functions.rs @@ -0,0 +1,7 @@ +// Setters + +pub const LOGIN: u32 = 15; + +// Getters + +pub const LAN_TABLE: u32 = 123; diff --git a/connectbox/src/lib.rs b/connectbox/src/lib.rs new file mode 100644 index 0000000..e27ce9a --- /dev/null +++ b/connectbox/src/lib.rs @@ -0,0 +1,125 @@ +//! API client library for the Compal CH7465CE, which is a cable modem provided by various European ISPs under the name Connect Box. + +use std::{borrow::Cow, fmt::Display, sync::Arc}; + +use error::Error; +use reqwest::{ + cookie::{CookieStore, Jar}, + redirect::Policy, + Client, Url, +}; +use serde::de::DeserializeOwned; + +pub mod error; +mod functions; +pub mod models; + +pub(crate) type Result = std::result::Result; + +type Field<'a, 'b> = (Cow<'a, str>, Cow<'b, str>); + +/// The entry point of the library - the API client +pub struct ConnectBox { + http: Client, + cookie_store: Arc, + base_url: Url, + getter_url: Url, + setter_url: Url, +} + +impl ConnectBox { + pub fn new(address: impl Display) -> Result { + let cookie_store = Arc::new(Jar::default()); + let http = Client::builder() + .user_agent("Mozilla/5.0") + .redirect(Policy::none()) + .cookie_provider(cookie_store.clone()) + .build()?; + let base_url: Url = format!("http://{address}/").parse()?; + let getter_url = base_url.join("xml/getter.xml")?; + let setter_url = base_url.join("xml/setter.xml")?; + Ok(ConnectBox { + http, + cookie_store, + base_url, + getter_url, + setter_url, + }) + } + + fn cookie<'a>(&self, name: &str) -> Result> { + let Some(cookies) = self.cookie_store.cookies(&self.base_url) else { + return Ok(None) + }; + let cookies = cookies.to_str()?; + let Some(mut cookie_start) = cookies.find(&format!("{name}=")) else { + return Ok(None) + }; + cookie_start += name.len() + 1; + let cookie_end = cookies[cookie_start..] + .find(";") + .map(|p| p + cookie_start) + .unwrap_or(cookies.len()); + Ok(Some(cookies[cookie_start..cookie_end].to_string())) + } + + async fn xml_getter(&self, function: u32) -> Result { + let session_token = self.cookie("sessionToken")?.ok_or(Error::NoSessionToken)?; + let form: Vec = vec![ + ("token".into(), session_token.into()), + ("fun".into(), function.to_string().into()), + ]; + let req = self.http.post(self.getter_url.clone()).form(&form); + let resp = req.send().await?.text().await?; + println!("{resp:?}"); + let obj = quick_xml::de::from_str(&resp)?; + Ok(obj) + } + + async fn xml_setter( + &self, + function: u32, + fields: Option>>, + ) -> Result { + let session_token = self.cookie("sessionToken")?.ok_or(Error::NoSessionToken)?; + let mut form = vec![ + ("token".into(), session_token.into()), + ("fun".into(), function.to_string().into()), + ]; + if let Some(fields) = fields { + form.extend(fields); + } + let req = self.http.post(self.setter_url.clone()).form(&form); + let resp = req.send().await?; + Ok(resp.text().await?) + } + + pub async fn login(&self, code: &str) -> Result<()> { + // get the session cookie + self.http + .get(self.base_url.join("common_page/login.html")?) + .send() + .await?; + + // log in + let fields = vec![ + ("Username".into(), "NULL".into()), + ("Password".into(), code.into()), + ]; + let response = self.xml_setter(functions::LOGIN, Some(fields)).await?; + if response == "idloginincorrect" { + return Err(Error::IncorrectPassword); + } + let sid = response + .strip_prefix("successful;SID=") + .ok_or_else(|| Error::UnexpectedResponse(response.clone()))?; + self.cookie_store + .add_cookie_str(&format!("SID={sid}"), &self.base_url); + + Ok(()) + } + + pub async fn get_devices(&self) -> Result { + self.xml_getter(functions::LAN_TABLE).await + } +} diff --git a/connectbox/src/models.rs b/connectbox/src/models.rs new file mode 100644 index 0000000..a3c66e8 --- /dev/null +++ b/connectbox/src/models.rs @@ -0,0 +1,34 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +pub struct LanUserTable { + #[serde(rename = "Ethernet")] + pub ethernet: ClientInfos, + #[serde(rename = "WIFI")] + pub wifi: ClientInfos, + #[serde(rename = "totalClient")] + pub total_clients: u32, + #[serde(rename = "Customer")] + pub customer: String, +} + +#[derive(Deserialize, Debug)] +pub struct ClientInfos { + pub clientinfo: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct ClientInfo { + pub index: u32, + pub interface: String, + #[serde(rename = "interfaceid")] + pub interface_id: u32, + #[serde(rename = "IPv4Addr")] + pub ipv4_addr: String, + pub hostname: String, + #[serde(rename = "MACAddr")] + pub mac: String, + #[serde(rename = "leaseTime")] + pub lease_time: String, + pub speed: u32, +}