use std::collections::HashMap;
use anyhow::{Context, Result};
use reqwest::{Client, Proxy};
use serde::Deserialize;
use rand::Rng;
const DEFAULT_IP: &str = "192.168.1.1";
const DEFAULT_UNAME: &str = "useradmin";
const DEFAULT_UPWD: &str = "";
pub struct TianyiBuilder {
ip: String,
username: String,
password: String,
}
impl Default for TianyiBuilder {
fn default() -> Self {
Self {
ip: DEFAULT_IP.to_string(),
username: DEFAULT_UNAME.to_string(),
password: DEFAULT_UPWD.to_string(),
}
}
}
impl TianyiBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn ip(mut self, ip: &str) -> Self {
self.ip = ip.to_string();
self
}
pub fn username(mut self, username: &str) -> Self {
self.username = username.to_string();
self
}
pub fn password(mut self, password: &str) -> Self {
self.password = password.to_string();
self
}
pub async fn build(self) -> Result<Tianyi> {
Tianyi::new(&self.ip, &self.username, &self.password).await
}
}
pub struct Tianyi {
url: String,
token: String,
client: Client,
}
#[derive(Debug, Deserialize)]
pub struct GatewayInfo {
#[serde(rename = "LANIP")]
pub lan_ip: String,
#[serde(rename = "LANIPv6")]
pub lan_ipv6: String,
#[serde(rename = "MAC")]
pub mac: String,
#[serde(rename = "WANIP")]
pub wan_ip: String,
#[serde(rename = "WANIPv6")]
pub wan_ipv6: String,
#[serde(rename = "ProductSN")]
pub product_sn: String,
#[serde(rename = "DevType")]
pub dev_type: String,
#[serde(rename = "SWVer")]
pub sw_ver: String,
#[serde(rename = "ProductCls")]
pub product_cls: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct PortForwardingRule {
#[serde(rename = "protocol")]
pub protocol: String,
#[serde(rename = "inPort")]
pub in_port: u16,
#[serde(rename = "enable")]
pub enable: u8,
#[serde(rename = "desp")]
pub description: String,
#[serde(rename = "client")]
pub client: String,
#[serde(rename = "exPort")]
pub ex_port: u16,
}
#[derive(Debug, Deserialize)]
pub struct PortForwardingData {
#[serde(rename = "mask")]
pub mask: String,
#[serde(rename = "lanIp")]
pub lan_ip: String,
#[serde(rename = "count")]
pub count: u32,
#[serde(flatten)]
pub rules: HashMap<String, PortForwardingRule>,
}
#[derive(Debug, Deserialize)]
pub struct ActionResult {
#[serde(rename = "retVal")]
pub ret_val: i32,
}
#[derive(Debug, Deserialize)]
pub enum PortForwardingAction {
Add,
Enable,
Disable,
Delete,
}
impl PortForwardingAction {
fn as_str(&self) -> &str {
match self {
PortForwardingAction::Add => "add",
PortForwardingAction::Enable => "enable",
PortForwardingAction::Disable => "disable",
PortForwardingAction::Delete => "del",
}
}
}
impl Tianyi {
async fn rand_str() -> String {
let mut rng = rand::thread_rng();
rng.gen::<f64>().to_string()
}
async fn new(ip: &str, username: &str, password: &str) -> Result<Self> {
let url = format!("http://{}", ip);
let client = Client::builder()
.cookie_store(true)
.build()?;
let login_payload = [("username", username), ("psd", password)];
let response = client.post(&format!("{}/cgi-bin/luci", url))
.form(&login_payload)
.send()
.await?;
let token = match response.text().await {
Ok(text) => {
let re = regex::Regex::new(r"token: '([a-z0-9]{32})'").unwrap();
re.captures(&text).context("Failed to parse token")?[1].to_string()
}
Err(err) => return Err(err.into()),
};
Ok(Tianyi { url, client, token })
}
pub async fn logout(&self) -> Result<()> {
let payload = [("token", &self.token), ("_", &Self::rand_str().await)];
let response = self.client.post(&format!("{}/cgi-bin/luci/admin/logout", self.url))
.form(&payload)
.send()
.await?;
if response.status().is_success() {
Ok(())
} else {
Err(anyhow::anyhow!("Failed to logout"))
}
}
pub async fn gwinfo(&self) -> Result<GatewayInfo> {
let payload = [("get", "part"), ("_", &Self::rand_str().await)];
let response = self.client.get(&format!("{}/cgi-bin/luci/admin/settings/gwinfo", self.url))
.query(&payload)
.send()
.await?;
let gw_info: GatewayInfo = response.json().await?;
Ok(gw_info)
}
pub async fn port_forwarding(&self) -> Result<PortForwardingData> {
let payload = [("_", &Self::rand_str().await)];
let response = self.client.get(&format!("{}/cgi-bin/luci/admin/settings/pmDisplay", self.url))
.query(&payload)
.send()
.await?;
let port_forwarding_data: PortForwardingData = response.json().await?;
Ok(port_forwarding_data)
}
pub async fn get_port_forwarding_rules(&self) -> Result<Vec<PortForwardingRule>> {
let port_forwarding_data = self.port_forwarding().await?;
let rules = port_forwarding_data.rules.into_iter().map(|(_, rule)| rule).collect();
Ok(rules)
}
pub async fn set_port_forwarding_rule(&self, action: PortForwardingAction, srvname: &str, rule: Option<&PortForwardingRule>) -> Result<ActionResult> {
let rand_str = Self::rand_str().await;
let mut payload = vec![
("srvname", srvname),
("token", &self.token),
("op", action.as_str()),
("_", &rand_str),
];
let ex_port = rule.map_or("".to_owned(), |rule| rule.ex_port.to_string());
let in_port = rule.map_or("".to_owned(), |rule| rule.in_port.to_string());
if let Some(rule) = rule {
payload.push(("client", &rule.client));
payload.push(("protocol", &rule.protocol));
payload.push(("exPort", &ex_port));
payload.push(("inPort", &in_port));
}
let response = self.client.post(&format!("{}/cgi-bin/luci/admin/settings/pmSetSingle", self.url))
.form(&payload)
.send()
.await?;
let action_result: ActionResult = response.json().await?;
Ok(action_result)
}
pub async fn update_port_forwarding_rule(
&self,
old_ip: &str,
new_ip: &str,
) -> Result<()> {
let rules = self.get_port_forwarding_rules().await?;
let mut updated_rules = Vec::new();
for rule in rules {
if rule.client == old_ip {
let mut updated_rule = rule.clone();
updated_rule.client = new_ip.to_string();
updated_rules.push(updated_rule);
self.set_port_forwarding_rule(PortForwardingAction::Delete, &rule.description, Some(&rule))
.await?;
}
}
for updated_rule in updated_rules {
self.set_port_forwarding_rule(PortForwardingAction::Add, &updated_rule.description, Some(&updated_rule))
.await?;
}
Ok(())
}
}