From 33508a973271974d25c0d869e9bdb7aa0151c2ad Mon Sep 17 00:00:00 2001 From: lemonsh Date: Thu, 4 May 2023 23:10:17 +0200 Subject: [PATCH] logout and port forwarding --- connectbox/examples/devices.rs | 35 +++++++- connectbox/src/error.rs | 2 + connectbox/src/functions.rs | 4 +- connectbox/src/lib.rs | 157 ++++++++++++++++++++++++++++----- connectbox/src/models.rs | 10 +-- 5 files changed, 176 insertions(+), 32 deletions(-) diff --git a/connectbox/examples/devices.rs b/connectbox/examples/devices.rs index 2ac0343..90418f9 100644 --- a/connectbox/examples/devices.rs +++ b/connectbox/examples/devices.rs @@ -1,7 +1,10 @@ -use std::env; +use std::{env, net::Ipv4Addr}; use color_eyre::Result; -use connectbox::ConnectBox; +use connectbox::{ + models::{PortForwardEntry, PortForwardProtocol}, + ConnectBox, PortForwardAction, +}; #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { @@ -14,8 +17,32 @@ async fn main() -> Result<()> { let connect_box = ConnectBox::new(ip, code, true)?; connect_box.login().await?; - let devices = connect_box.get_devices().await?; - println!("{devices:#?}"); + connect_box + .edit_port_forwards(|v| { + if v.local_ip == Ipv4Addr::new(192, 168, 0, 180) { + PortForwardAction::Delete + } else { + PortForwardAction::Keep + } + }) + .await?; + + let pf = PortForwardEntry { + id: 0, + local_ip: "192.168.0.180".parse().unwrap(), + start_port: 25565, + end_port: 25565, + start_port_in: 25565, + end_port_in: 25565, + protocol: PortForwardProtocol::Both, + enable: true, + }; + connect_box.add_port_forward(&pf).await?; + + let portforwards = connect_box.port_forwards().await?; + println!("{portforwards:#?}"); + + connect_box.logout().await?; Ok(()) } diff --git a/connectbox/src/error.rs b/connectbox/src/error.rs index e734eba..0ec5eee 100644 --- a/connectbox/src/error.rs +++ b/connectbox/src/error.rs @@ -15,6 +15,8 @@ pub enum Error { AccessDenied, #[error("an unexpected redirection has occurred: {0:?}")] UnexpectedRedirect(String), + #[error("remote error: {0:?}")] + Remote(String), #[error(transparent)] URLParseError(#[from] url::ParseError), diff --git a/connectbox/src/functions.rs b/connectbox/src/functions.rs index 9d7444f..f8fc188 100644 --- a/connectbox/src/functions.rs +++ b/connectbox/src/functions.rs @@ -1,8 +1,10 @@ // Setters pub const LOGIN: u32 = 15; +pub const LOGOUT: u32 = 16; +pub const EDIT_FORWARDS: u32 = 122; // Getters pub const LAN_TABLE: u32 = 123; -pub const FORWARDS: u32 = 121; \ No newline at end of file +pub const FORWARDS: u32 = 121; diff --git a/connectbox/src/lib.rs b/connectbox/src/lib.rs index ebcd9d0..2a2ef21 100644 --- a/connectbox/src/lib.rs +++ b/connectbox/src/lib.rs @@ -1,12 +1,15 @@ //! API client library for the Compal CH7465CE, which is a cable modem provided by various European ISPs under the name Connect Box. +#![allow(clippy::missing_errors_doc)] use std::{borrow::Cow, fmt::Display, sync::Arc}; pub use error::Error; +use models::PortForwardEntry; use reqwest::{ cookie::{CookieStore, Jar}, + header::HeaderValue, redirect::Policy, - Client, Url, header::HeaderValue, + Client, Url, }; use serde::de::DeserializeOwned; @@ -47,16 +50,16 @@ impl ConnectBox { let setter_url = base_url.join("xml/setter.xml")?; Ok(ConnectBox { http, + code, cookie_store, base_url, getter_url, setter_url, - code, auto_reauth, }) } - fn cookie<'a>(&self, name: &str) -> Result> { + fn cookie(&self, name: &str) -> Result> { let Some(cookies) = self.cookie_store.cookies(&self.base_url) else { return Ok(None) }; @@ -66,9 +69,8 @@ impl ConnectBox { }; cookie_start += name.len() + 1; let cookie_end = cookies[cookie_start..] - .find(";") - .map(|p| p + cookie_start) - .unwrap_or(cookies.len()); + .find(';') + .map_or(cookies.len(), |p| p + cookie_start); Ok(Some(cookies[cookie_start..cookie_end].to_string())) } @@ -76,16 +78,20 @@ impl ConnectBox { let mut reauthed = false; loop { let session_token = self.cookie("sessionToken")?.ok_or(Error::NoSessionToken)?; - let form: Vec = vec![ + let form: &[Field] = &[ ("token".into(), session_token.into()), ("fun".into(), function.to_string().into()), ]; - let req = self.http.post(self.getter_url.clone()).form(&form); + tracing::debug!("Executing getter {function}"); + let req = self.http.post(self.getter_url.clone()).form(form); let resp = req.send().await?; if resp.status().is_redirection() { if self.auto_reauth && !reauthed { reauthed = true; - tracing::info!("session <{}> has expired, attempting reauth", self.cookie("SID")?.as_deref().unwrap_or("unknown")); + tracing::info!( + "session <{}> has expired, attempting reauth", + self.cookie("SID")?.as_deref().unwrap_or("unknown") + ); self._login().await?; continue; } @@ -95,29 +101,29 @@ impl ConnectBox { } } - async fn xml_setter( - &self, - function: u32, - fields: Option]>>, - ) -> Result { + async fn xml_setter(&self, function: u32, fields: Option<&[Field<'_, '_>]>) -> Result { let mut reauthed = false; loop { let session_token = self.cookie("sessionToken")?.ok_or(Error::NoSessionToken)?; - let mut form: Vec<(Cow, Cow)> = vec![ + let mut form = vec![ ("token".into(), session_token.into()), ("fun".into(), function.to_string().into()), ]; - if let Some(fields) = &fields { - for (key, value) in fields.as_ref() { + if let Some(fields) = fields { + for (key, value) in fields { form.push((key.clone(), value.clone())); } } + tracing::debug!("Executing setter {function} with body {form:?}"); let req = self.http.post(self.setter_url.clone()).form(&form); let resp = req.send().await?; if resp.status().is_redirection() { if self.auto_reauth && !reauthed { reauthed = true; - tracing::info!("session <{}> has expired, attempting reauth", self.cookie("SID")?.as_deref().unwrap_or("unknown")); + tracing::info!( + "session <{}> has expired, attempting reauth", + self.cookie("SID")?.as_deref().unwrap_or("unknown") + ); self._login().await?; continue; } @@ -129,13 +135,13 @@ impl ConnectBox { async fn _login(&self) -> Result<()> { let session_token = self.cookie("sessionToken")?.ok_or(Error::NoSessionToken)?; - let form: Vec<(Cow, Cow)> = vec![ + let form: &[Field] = &[ ("token".into(), session_token.into()), ("fun".into(), functions::LOGIN.to_string().into()), ("Username".into(), "NULL".into()), ("Password".into(), (&self.code).into()), ]; - let req = self.http.post(self.setter_url.clone()).form(&form); + let req = self.http.post(self.setter_url.clone()).form(form); let resp = req.send().await?; if resp.status().is_redirection() { if let Some(location) = resp.headers().get("Location").map(HeaderValue::to_str) { @@ -144,7 +150,7 @@ impl ConnectBox { Err(Error::AccessDenied) } else { Err(Error::UnexpectedRedirect(location.to_string())) - } + }; } } let resp_text = resp.text().await?; @@ -172,13 +178,120 @@ impl ConnectBox { self._login().await } + /// Logout of the router. + /// + /// The Connect Box allows only one session at a time, thus you should call this method after you're done with using the client, so that other users can log in. + pub async fn logout(&self) -> Result<()> { + self.xml_setter(functions::LOGOUT, None).await?; + tracing::info!( + "session <{}>: logged out", + self.cookie("SID")?.as_deref().unwrap_or("unknown") + ); + Ok(()) + } + /// Get all devices connected to the router. pub async fn devices(&self) -> Result { self.xml_getter(functions::LAN_TABLE).await } - /// Get all port forwarding entries. + /// Get all port forwards. pub async fn port_forwards(&self) -> Result { self.xml_getter(functions::FORWARDS).await } + + /// Toggle or remove port forwards. + pub async fn edit_port_forwards(&self, mut f: F) -> Result<()> + where + F: FnMut(models::PortForwardEntry) -> PortForwardAction, + { + let mut instance = String::new(); + let mut enable = String::new(); + let mut delete = String::new(); + for entry in self.port_forwards().await?.entries { + let id = entry.id; + match f(entry) { + PortForwardAction::Enable => { + enable.push_star("1"); + delete.push_star("0"); + } + PortForwardAction::Disable => { + enable.push_star("0"); + delete.push_star("0"); + } + PortForwardAction::Delete => { + enable.push_star("0"); + delete.push_star("1"); + } + PortForwardAction::Keep => continue, + } + instance.push_star(&id.to_string()); + } + let fields = [ + ("action".into(), "apply".into()), + ("instance".into(), instance.into()), + ("local_IP".into(), "".into()), + ("start_port".into(), "".into()), + ("end_port".into(), "".into()), + ("start_portIn".into(), "".into()), + ("end_portIn".into(), "".into()), + ("protocol".into(), "".into()), + ("enable".into(), enable.into()), + ("delete".into(), delete.into()), + ("idd".into(), "".into()), + ]; + let resp = self + .xml_setter(functions::EDIT_FORWARDS, Some(&fields)) + .await?; + if resp.is_empty() { + Ok(()) + } else { + Err(Error::Remote(resp)) + } + } + + /// Add a port forward. The `id` field of the port is ignored. + pub async fn add_port_forward(&self, port: &PortForwardEntry) -> Result<()> { + let fields = [ + ("action".into(), "add".into()), + ("instance".into(), "".into()), + ("local_IP".into(), port.local_ip.to_string().into()), + ("start_port".into(), port.start_port.to_string().into()), + ("end_port".into(), port.end_port.to_string().into()), + ("start_portIn".into(), port.start_port_in.to_string().into()), + ("end_portIn".into(), port.end_port_in.to_string().into()), + ("protocol".into(), port.protocol.id().to_string().into()), + ("enable".into(), u8::from(port.enable).to_string().into()), + ("delete".into(), "0".into()), + ("idd".into(), "".into()), + ]; + let resp = self + .xml_setter(functions::EDIT_FORWARDS, Some(&fields)) + .await?; + if resp.is_empty() { + Ok(()) + } else { + Err(Error::Remote(resp)) + } + } +} + +pub enum PortForwardAction { + Keep, + Enable, + Disable, + Delete, +} + +trait StringExt { + fn push_star(&mut self, string: &str); +} + +impl StringExt for String { + fn push_star(&mut self, string: &str) { + if !self.is_empty() { + self.push('*'); + } + self.push_str(string); + } } diff --git a/connectbox/src/models.rs b/connectbox/src/models.rs index c4972db..0ae2444 100644 --- a/connectbox/src/models.rs +++ b/connectbox/src/models.rs @@ -1,7 +1,7 @@ use std::net::Ipv4Addr; use std::time::Duration; -use serde::de::{Error, self, Unexpected}; +use serde::de::{self, Error, Unexpected}; use serde::{Deserialize, Deserializer}; #[derive(Deserialize, Debug)] @@ -59,7 +59,7 @@ pub struct PortForwardEntry { pub end_port_in: u16, pub protocol: PortForwardProtocol, #[serde(deserialize_with = "bool_from_int")] - pub enable: bool + pub enable: bool, } #[derive(Debug)] @@ -70,7 +70,7 @@ pub enum PortForwardProtocol { } impl PortForwardProtocol { - fn id(&self) -> u8 { + pub(crate) fn id(&self) -> u8 { match self { PortForwardProtocol::Tcp => 1, PortForwardProtocol::Udp => 2, @@ -101,7 +101,7 @@ where 0 => Ok(false), 1 => Ok(true), other => Err(de::Error::invalid_value( - Unexpected::Unsigned(other as u64), + Unexpected::Unsigned(u64::from(other)), &"zero or one", )), } @@ -143,5 +143,5 @@ where .next() .ok_or(D::Error::custom("no secs field in lease time"))??; let secs_total = days * 86400 + hours * 3600 + mins * 60 + secs; - Ok(Duration::from_secs(secs_total as u64)) + Ok(Duration::from_secs(u64::from(secs_total))) }