From 65f1b0bc5eb2baceabe9ce9678ca1f0d884b495e Mon Sep 17 00:00:00 2001 From: Louis Vallat Date: Tue, 28 Jun 2022 00:22:37 +0200 Subject: [PATCH] feat: Added get_status for client and the Status data structure for deserializing API response Signed-off-by: Louis Vallat --- Cargo.toml | 7 +++-- src/client.rs | 52 +++++++++++++++++++++++++++++++---- src/lib.rs | 1 + src/router_info.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 src/router_info.rs diff --git a/Cargo.toml b/Cargo.toml index 0271930..9d7b626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,13 @@ license = "GPL-3.0" maintenance = { status = "experimental" } [dependencies] -serde_json = "1.0.81" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" 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" + +[dev-dependencies] +tokio-test = "0.4.2" diff --git a/src/client.rs b/src/client.rs index d7859c7..a685714 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,10 +4,12 @@ use std::net::Ipv4Addr; use cookie::Cookie; -use serde_json::json; +use serde_json::{json, Value}; use hyper::{Method, Request, body::Body, header::{CONTENT_TYPE, SET_COOKIE, AUTHORIZATION, COOKIE}, client::HttpConnector}; use log::{trace, debug}; +use crate::router_info::Status; + /// A structure to hold all the necessary data to connect to the router. pub struct Client { ip: String, @@ -21,9 +23,9 @@ pub struct 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 { + pub fn new(password: &str) -> Self { trace!("Creating a new client."); - return Client { + return Self { ip: Ipv4Addr::new(192, 168, 1, 1).to_string(), username: "admin".to_string(), password: password.to_string(), @@ -57,8 +59,10 @@ impl Client { .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()); + let body_bytes = hyper::body::to_bytes(body).await.unwrap(); + let json: Value = serde_json::from_slice(&body_bytes) + .expect("Could not parse JSON."); + debug!("Body was: '{}'.", std::str::from_utf8(&body_bytes).unwrap()); assert!(parts.status.is_success(), "Router answered with something else than a success code."); @@ -68,6 +72,44 @@ impl Client { } assert!(!self.cookies.is_empty(), "No cookie detected on login, there should be an error."); + assert!(json["status"].as_u64().unwrap() == 0, "Status wasn't 0."); + self.context_id = + Some(json["data"]["contextID"].as_str().unwrap().to_string()); + } + + /// Get various status data from the router. + pub async fn get_status(&self) -> Status { + assert!(!self.cookies.is_empty() && self.context_id.is_some(), + "Cannot get status without logging in beforehand."); + trace!("Getting router status."); + let post_data = json!({ + "service": "DeviceInfo", + "method": "get", + "parameters": {} + }); + + let req = Request::builder() + .method(Method::POST) + .uri(format!("http://{}/ws", self.ip)) + .header(CONTENT_TYPE, "application/x-sah-ws-4-call+json") + .header("X-Context", self.context_id.clone().unwrap()) + .header(COOKIE, self.cookies.join("; ")) + .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()); + let body_bytes = hyper::body::to_bytes(body).await.unwrap(); + let json: Value = serde_json::from_slice(&body_bytes) + .expect("Could not parse JSON."); + debug!("Body was: '{}'.", std::str::from_utf8(&body_bytes).unwrap()); + assert!(parts.status.is_success(), "Router answered with something else + than a success code."); + let status: Status = serde_json::from_value(json["status"].clone()) + .expect("Looks like the deserialized data is incomplete."); + debug!("Deserialized status is: {:?}", status); + return status; } /// Logout from the router. diff --git a/src/lib.rs b/src/lib.rs index 6c49a47..530ea06 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,3 +2,4 @@ //! A crate aimed to gather (read only) data from a Livebox 4 or more recent. pub mod client; +pub mod router_info; diff --git a/src/router_info.rs b/src/router_info.rs new file mode 100644 index 0000000..bd6a1ee --- /dev/null +++ b/src/router_info.rs @@ -0,0 +1,68 @@ + +///! All the necessary struct elements to store the router status. + +use serde::Deserialize; + +/// The Status structure is used to store status data from the router. +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct Status { + #[serde(rename(deserialize = "Manufacturer"))] + pub manufacturer: String, + #[serde(rename(deserialize = "ManufacturerOUI"))] + pub manufacturer_oui: String, + #[serde(rename(deserialize = "ModelName"))] + pub model_name: String, + #[serde(rename(deserialize = "Description"))] + pub description: String, + #[serde(rename(deserialize = "ProductClass"))] + pub product_class: String, + #[serde(rename(deserialize = "SerialNumber"))] + pub serial_number: String, + #[serde(rename(deserialize = "HardwareVersion"))] + pub hardware_version: String, + #[serde(rename(deserialize = "SoftwareVersion"))] + pub software_version: String, + #[serde(rename(deserialize = "RescueVersion"))] + pub rescue_version: String, + #[serde(rename(deserialize = "ModemFirmwareVersion"))] + pub modem_firmware_version: String, + #[serde(rename(deserialize = "EnabledOptions"))] + pub enabled_options: String, + #[serde(rename(deserialize = "AdditionalHardwareVersion"))] + pub additional_hardware_version: String, + #[serde(rename(deserialize = "AdditionalSoftwareVersion"))] + pub additional_software_version: String, + #[serde(rename(deserialize = "SpecVersion"))] + pub spec_version: String, + #[serde(rename(deserialize = "ProvisioningCode"))] + pub provisioning_code: String, + #[serde(rename(deserialize = "UpTime"))] + pub uptime: u32, + #[serde(rename(deserialize = "FirstUseDate"))] + pub first_use_date: String, + #[serde(rename(deserialize = "DeviceLog"))] + pub device_log: String, + #[serde(rename(deserialize = "VendorConfigFileNumberOfEntries"))] + pub vendor_configfile_number_of_entries: u32, + #[serde(rename(deserialize = "ManufacturerURL"))] + pub manufacturer_url: String, + #[serde(rename(deserialize = "Country"))] + pub country: String, + #[serde(rename(deserialize = "ExternalIPAddress"))] + pub external_ip_address: String, + #[serde(rename(deserialize = "DeviceStatus"))] + pub device_status: String, + #[serde(rename(deserialize = "NumberOfReboots"))] + pub number_of_reboots: u32, + #[serde(rename(deserialize = "UpgradeOccurred"))] + pub upgrade_occured: bool, + #[serde(rename(deserialize = "ResetOccurred"))] + pub reset_occurred: bool, + #[serde(rename(deserialize = "RestoreOccurred"))] + pub restore_occurred: bool, + #[serde(rename(deserialize = "X_SOFTATHOME-COM_AdditionalSoftwareVersions"))] + pub softathome_additional_software_versions: String, + #[serde(rename(deserialize = "BaseMAC"))] + pub base_mac: String, +}