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 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 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}