diff --git a/Cargo.toml b/Cargo.toml index f05fb31..0271930 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,14 @@ license = "GPL-3.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[badges] +maintenance = { status = "experimental" } + [dependencies] +serde_json = "1.0.81" +hyper = { version = "0.14", features = ["full"] } +tokio = { version = "1", features = ["full"] } +tokio-test = "0.4.2" +log = "0.4.17" +env_logger = "0.9.0" +cookie = "0.16" diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..d7859c7 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,90 @@ + +//! A module to handle the client used to store some important data about the +//! router. + +use std::net::Ipv4Addr; +use cookie::Cookie; +use serde_json::json; +use hyper::{Method, Request, body::Body, header::{CONTENT_TYPE, SET_COOKIE, AUTHORIZATION, COOKIE}, client::HttpConnector}; +use log::{trace, debug}; + +/// A structure to hold all the necessary data to connect to the router. +pub struct Client { + ip: String, + username: String, + password: String, + cookies: Vec, + context_id: Option, + client: hyper::Client +} + +impl Client { + /// Create a new Client using only the password, and using `192.168.1.1` as + /// target IP, and `admin` as the default username. + pub fn new(password: &str) -> Client { + trace!("Creating a new client."); + return Client { + ip: Ipv4Addr::new(192, 168, 1, 1).to_string(), + username: "admin".to_string(), + password: password.to_string(), + cookies: Vec::new(), + context_id: None, + client: hyper::Client::new() + }; + } + + /// Login to the router. + pub async fn login(&mut self) { + trace!("Logging in."); + let post_data = json!({ + "service": "sah.Device.Information", + "method": "createContext", + "parameters": { + "applicationName": "so_sdkut", + "username": self.username, + "password": self.password + } + }); + + let req = Request::builder() + .method(Method::POST) + .uri(format!("http://{}/ws", self.ip)) + .header(CONTENT_TYPE, "application/x-sah-ws-4-call+json") + .header(AUTHORIZATION, "X-Sah-Login") + .body(Body::from(post_data.to_string())) + .expect("Could not build request."); + let (parts, body) = self.client.request(req).await + .expect("There was an issue contacting the router.").into_parts(); + + debug!("Status is {}.", parts.status.as_str()); + debug!("Body was: '{}'.", std::str::from_utf8( + &hyper::body::to_bytes(body).await.unwrap()).unwrap()); + assert!(parts.status.is_success(), "Router answered with something else + than a success code."); + + for ele in parts.headers.get_all(SET_COOKIE) { + let cookie = Cookie::parse(ele.to_str().unwrap()).unwrap(); + self.cookies.push(format!("{}={}", cookie.name(), cookie.value())); + } + assert!(!self.cookies.is_empty(), + "No cookie detected on login, there should be an error."); + } + + /// Logout from the router. + pub async fn logout(&mut self) { + trace!("Logging out."); + let req = Request::builder() + .method(Method::POST) + .uri(format!("http://{}/ws", self.ip)) + .header(COOKIE, self.cookies.join("; ")) + .body(Body::empty()) + .expect("Could not build request."); + + self.client.request(req).await + .expect("There was an issue contacting the router."); + debug!("Logged out."); + self.cookies.clear(); + self.context_id = None; + } +} + diff --git a/src/lib.rs b/src/lib.rs index 1b4a90c..6c49a47 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,4 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} + +//! A crate aimed to gather (read only) data from a Livebox 4 or more recent. + +pub mod client;