dlipower/
powerstrip.rs

1use md5::{Digest, Md5};
2use regex::Regex;
3use reqwest::Client;
4use scraper::{Html, Selector};
5use std::collections::HashMap;
6use std::str::FromStr;
7
8#[derive(Debug, Clone)]
9pub enum Status {
10    ON,
11    OFF,
12}
13
14impl FromStr for Status {
15    type Err = ();
16
17    fn from_str(input: &str) -> Result<Self, Self::Err> {
18        match input {
19            "ON" => Ok(Self::ON),
20            "OFF" => Ok(Self::OFF),
21            _ => Err(()),
22        }
23    }
24}
25
26pub struct PowerStrip {
27    base_url: String,
28    client: Client,
29}
30impl PowerStrip {
31    pub async fn new(user: String, password: String, ip: String) -> Result<Self, ()> {
32        let base_url = format!("http://{}", ip);
33        let client = login(&user, &password, &base_url).await.unwrap();
34        Ok(PowerStrip { base_url, client })
35    }
36    pub async fn update_all(&self, status: Status) -> Result<(), ()> {
37        let _resp = self
38            .client
39            .get(format!("{}/outlet?a={:?}", self.base_url, status))
40            .send()
41            .await
42            .unwrap();
43        Ok(())
44    }
45    pub async fn update(&self, outlet: u8, status: Status) -> Result<(), ()> {
46        let _resp = self
47            .client
48            .get(format!("{}/outlet?{}={:?}", self.base_url, outlet, status))
49            .send()
50            .await
51            .unwrap();
52        Ok(())
53    }
54
55    pub async fn status(&self) -> Result<Vec<Status>, ()> {
56        let resp = self
57            .client
58            .get(format!("{}/index.htm", self.base_url))
59            .send()
60            .await
61            .unwrap();
62        let status = resp.text().await.unwrap();
63        let doc = Html::parse_document(&status);
64        let elems = Selector::parse("td").unwrap();
65        let outlet_regex = Regex::new(r"^Outlet (\d+)$").unwrap();
66        let status_regex = Regex::new(r"^\n<b><font color=.*>([A-Z]+)</font></b>$").unwrap();
67        let mut elems_iter = doc.select(&elems);
68        let mut outlets = Vec::new();
69        while let Some(elem) = elems_iter.next() {
70            if let Some(_outlet) = outlet_regex.captures(&elem.inner_html()) {
71                if let Some(status) =
72                    status_regex.captures(&elems_iter.next().unwrap().inner_html())
73                {
74                    outlets.push(status[1].parse().unwrap());
75                }
76            }
77        }
78        Ok(outlets)
79    }
80}
81async fn login(user: &str, password: &str, base_url: &str) -> Result<Client, ()> {
82    let client = Client::builder().cookie_store(true).build().unwrap();
83    let resp = client
84        .get(base_url)
85        .send()
86        .await
87        .unwrap()
88        .text()
89        .await
90        .unwrap();
91    let c = challenge(resp);
92    let form_response = format!("{}{}{}{}", c, user, password, c);
93    let mut hasher = Md5::new();
94    hasher.update(form_response);
95    let login = hasher.finalize();
96    //Content-Type': 'application/x-www-form-urlencoded
97    let mut login_data = HashMap::new();
98    login_data.insert("Username", user.to_string());
99    login_data.insert("Password", format!("{:x}", login));
100    let _resp = client
101        .post(format!("{}/login.tgi", base_url))
102        .form(&login_data)
103        .send()
104        .await
105        .unwrap();
106    //let cookie = &resp.headers().get("Set-Cookie").unwrap();
107    Ok(client)
108}
109
110fn challenge(resp: String) -> String {
111    for l in resp.lines() {
112        if let Some(challenge_line) =
113            l.strip_prefix("<input type=\"hidden\" name=\"Challenge\" value=\"")
114        {
115            let (c, _) = challenge_line.split_once('"').unwrap();
116            return c.to_string();
117        }
118    }
119    panic!("No challenge found");
120}