initial commit
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| /target | ||||
| Cargo.lock | ||||
| dumps/ | ||||
							
								
								
									
										2
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| [workspace] | ||||
| members = ["connectbox", "connectbox-shell"] | ||||
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -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. | ||||
							
								
								
									
										6
									
								
								connectbox-shell/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								connectbox-shell/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| [package] | ||||
| name = "connectbox-shell" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
|  | ||||
| [dependencies] | ||||
							
								
								
									
										3
									
								
								connectbox-shell/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								connectbox-shell/src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| fn main() { | ||||
|     println!("Hello, world!"); | ||||
| } | ||||
							
								
								
									
										17
									
								
								connectbox/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								connectbox/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -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" | ||||
							
								
								
									
										21
									
								
								connectbox/src/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								connectbox/src/error.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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), | ||||
| } | ||||
							
								
								
									
										7
									
								
								connectbox/src/functions.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								connectbox/src/functions.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| // Setters | ||||
|  | ||||
| pub const LOGIN: u32 = 15; | ||||
|  | ||||
| // Getters | ||||
|  | ||||
| pub const LAN_TABLE: u32 = 123; | ||||
							
								
								
									
										125
									
								
								connectbox/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								connectbox/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<T> = std::result::Result<T, error::Error>; | ||||
|  | ||||
| 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<Jar>, | ||||
|     base_url: Url, | ||||
|     getter_url: Url, | ||||
|     setter_url: Url, | ||||
| } | ||||
|  | ||||
| impl ConnectBox { | ||||
|     pub fn new(address: impl Display) -> Result<Self> { | ||||
|         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<Option<String>> { | ||||
|         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<T: DeserializeOwned>(&self, function: u32) -> Result<T> { | ||||
|         let session_token = self.cookie("sessionToken")?.ok_or(Error::NoSessionToken)?; | ||||
|         let form: Vec<Field> = 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<impl IntoIterator<Item = Field<'_, '_>>>, | ||||
|     ) -> Result<String> { | ||||
|         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<models::LanUserTable> { | ||||
|         self.xml_getter(functions::LAN_TABLE).await | ||||
|     } | ||||
| } | ||||
							
								
								
									
										34
									
								
								connectbox/src/models.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								connectbox/src/models.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<ClientInfo>, | ||||
| } | ||||
|  | ||||
| #[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, | ||||
| } | ||||
		Reference in New Issue
	
	Block a user