Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f8e210644a | ||
|
ea6b2a3850 | ||
|
a3c8ef9f74 | ||
|
9703d984bb | ||
|
e238369ae5 | ||
|
9494e1b9e0 | ||
|
80f65f7292 | ||
|
dd6a63187e | ||
|
7d5a93e30d | ||
|
470d5f5814 | ||
|
8e9b778f75 | ||
|
b11c20d512 | ||
|
84fefcda09 | ||
|
c15e992974 |
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Projects in this repository:
|
||||||
|
* [`connectbox-rs`](connectbox) - API client crate for the Compal CH7465LG, which is a cable modem provided by various European ISPs under the name Connect Box.
|
||||||
|
* [`connectbox-shell`](connectbox-shell) - Interactive shell for managing a Connect Box modem, powered by the connectbox-rs crate.
|
@ -1,6 +1,19 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "connectbox-shell"
|
name = "connectbox-shell"
|
||||||
|
description = "A shell for managing your Connect Box router, based on the connectbox-rs library"
|
||||||
|
authors = ["lemonsh"]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
rustyline = {version = "11", features = ["derive"]}
|
||||||
|
color-eyre = "0.6"
|
||||||
|
tokio = { version = "1.0", default-features = false, features = ["macros"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
clap = { version = "4.2", default-features = false, features = ["suggestions", "color", "std", "help", "usage", "derive"] }
|
||||||
|
dirs = "5.0"
|
||||||
|
connectbox = { path = "../connectbox" }
|
||||||
|
color-print = "0.3"
|
||||||
|
ascii_table = "4.0"
|
||||||
|
once_cell = "1.17"
|
||||||
|
8
connectbox-shell/README.md
Normal file
8
connectbox-shell/README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# connectbox-shell
|
||||||
|
Interactive shell for managing a Connect Box modem.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
You can install this project easily using this command:
|
||||||
|
```sh
|
||||||
|
cargo install --git https://git.lemonsh.moe/lemon/connectbox-rs.git
|
||||||
|
```
|
63
connectbox-shell/src/cli.rs
Normal file
63
connectbox-shell/src/cli.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
|
use clap::{Command, Parser, Subcommand};
|
||||||
|
use tracing::Level;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about)]
|
||||||
|
pub(crate) struct Args {
|
||||||
|
/// Address of the modem
|
||||||
|
pub address: String,
|
||||||
|
|
||||||
|
/// Password used to log in to the modem. If not supplied, it will be asked interactively
|
||||||
|
pub password: Option<String>,
|
||||||
|
|
||||||
|
/// Log level, one of 'trace', 'debug', 'info', 'warn', 'error'
|
||||||
|
#[arg(short, default_value = "warn")]
|
||||||
|
pub log_level: Level,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub(crate) enum ShellCommand {
|
||||||
|
/// Log out and close the shell
|
||||||
|
Exit,
|
||||||
|
|
||||||
|
/// Manage port forwards
|
||||||
|
#[command(name = "pfw")]
|
||||||
|
PortForwards {
|
||||||
|
#[command(subcommand)]
|
||||||
|
cmd: PortForwardsCommand,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub(crate) enum PortForwardsCommand {
|
||||||
|
/// List all port forwards
|
||||||
|
Show,
|
||||||
|
/// Add a port forward
|
||||||
|
Add {
|
||||||
|
/// LAN address of the host to forward the port to
|
||||||
|
local_ip: Ipv4Addr,
|
||||||
|
/// External port range
|
||||||
|
range: String,
|
||||||
|
/// Internal port range. If unspecified, the same as external
|
||||||
|
int_range: Option<String>,
|
||||||
|
/// TCP, UDP or both
|
||||||
|
#[arg(short, default_value = "both")]
|
||||||
|
protocol: String,
|
||||||
|
/// Add this port forward in disabled state
|
||||||
|
#[arg(short)]
|
||||||
|
disable: bool,
|
||||||
|
},
|
||||||
|
/// Enable, disable or delete a port forward
|
||||||
|
Edit {
|
||||||
|
/// ID of the port. You can use `pfw show` to find it
|
||||||
|
id: u32,
|
||||||
|
/// Action to perform with the port. Can be either enable, disable or delete.
|
||||||
|
action: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn shell_cmd() -> Command {
|
||||||
|
ShellCommand::augment_subcommands(Command::new("").multicall(true))
|
||||||
|
}
|
1
connectbox-shell/src/commands/mod.rs
Normal file
1
connectbox-shell/src/commands/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod pfw;
|
140
connectbox-shell/src/commands/pfw.rs
Normal file
140
connectbox-shell/src/commands/pfw.rs
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
use std::vec;
|
||||||
|
|
||||||
|
use ascii_table::{Align::Right, AsciiTable};
|
||||||
|
use color_eyre::Result;
|
||||||
|
use color_print::{cprintln, cprint};
|
||||||
|
use connectbox::{
|
||||||
|
models::{PortForwardEntry, PortForwardProtocol},
|
||||||
|
PortForwardAction,
|
||||||
|
};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
|
use crate::{cli::PortForwardsCommand, AppState};
|
||||||
|
|
||||||
|
static PORT_FORWARDING_TABLE: OnceCell<AsciiTable> = OnceCell::new();
|
||||||
|
|
||||||
|
fn init_port_forwarding_table() -> AsciiTable {
|
||||||
|
let mut t = AsciiTable::default();
|
||||||
|
t.column(0).set_header("ID").set_align(Right);
|
||||||
|
t.column(1).set_header("Local IP");
|
||||||
|
t.column(2).set_header("Port range");
|
||||||
|
t.column(3).set_header("Int. port range");
|
||||||
|
t.column(4).set_header("Protocol");
|
||||||
|
t.column(5).set_header("Enabled");
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn run(cmd: PortForwardsCommand, state: &AppState) -> Result<()> {
|
||||||
|
match cmd {
|
||||||
|
PortForwardsCommand::Show => {
|
||||||
|
cprintln!("<blue!>Retrieving the port forwarding table...");
|
||||||
|
let port_forwards = state.connect_box.port_forwards().await?;
|
||||||
|
let table_entries = port_forwards.entries.into_iter().map(|e| {
|
||||||
|
let port_range = format!("{}-{}", e.start_port, e.end_port);
|
||||||
|
let in_port_range = format!("{}-{}", e.start_port_in, e.end_port_in);
|
||||||
|
vec![
|
||||||
|
e.id.to_string(),
|
||||||
|
e.local_ip.to_string(),
|
||||||
|
port_range,
|
||||||
|
in_port_range,
|
||||||
|
e.protocol.to_string(),
|
||||||
|
e.enable.to_string(),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
let rendered_table = PORT_FORWARDING_TABLE
|
||||||
|
.get_or_init(init_port_forwarding_table)
|
||||||
|
.format(table_entries);
|
||||||
|
cprint!(
|
||||||
|
"<black!>LAN IP: {}\nSubnet mask: {}\n</black!>{rendered_table}",
|
||||||
|
port_forwards.lan_ip,
|
||||||
|
port_forwards.subnet_mask
|
||||||
|
);
|
||||||
|
}
|
||||||
|
PortForwardsCommand::Add {
|
||||||
|
local_ip,
|
||||||
|
range,
|
||||||
|
int_range,
|
||||||
|
protocol,
|
||||||
|
disable,
|
||||||
|
} => {
|
||||||
|
let Some(protocol) = PortForwardProtocol::new(&protocol) else {
|
||||||
|
cprintln!("<red!>Invalid protocol {protocol:?}");
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
let Some(range) = parse_port_range(&range) else {
|
||||||
|
cprintln!("<red!>Invalid external range {range:?}");
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
let int_range = if let Some(r) = int_range {
|
||||||
|
let Some(r) = parse_port_range(&r) else {
|
||||||
|
cprintln!("<red!>Invalid internal range {r:?}");
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
r
|
||||||
|
} else {
|
||||||
|
range
|
||||||
|
};
|
||||||
|
let port = PortForwardEntry {
|
||||||
|
id: 0,
|
||||||
|
local_ip,
|
||||||
|
start_port: range.0,
|
||||||
|
end_port: range.1,
|
||||||
|
start_port_in: int_range.0,
|
||||||
|
end_port_in: int_range.1,
|
||||||
|
protocol,
|
||||||
|
enable: !disable,
|
||||||
|
};
|
||||||
|
state.connect_box.add_port_forward(&port).await?;
|
||||||
|
cprintln!("<green!>Done!");
|
||||||
|
}
|
||||||
|
PortForwardsCommand::Edit { id, mut action } => {
|
||||||
|
action.make_ascii_lowercase();
|
||||||
|
let action = match action.as_str() {
|
||||||
|
"enable" => {
|
||||||
|
cprintln!("<blue!>Enabling port {id}...");
|
||||||
|
PortForwardAction::Enable
|
||||||
|
}
|
||||||
|
"disable" => {
|
||||||
|
cprintln!("<blue!>Disabling port {id}...");
|
||||||
|
PortForwardAction::Disable
|
||||||
|
}
|
||||||
|
"delete" => {
|
||||||
|
cprintln!("<blue!>Deleting port {id}...");
|
||||||
|
PortForwardAction::Delete
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
cprintln!("<red!>Invalid action {action:?}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut modified = false;
|
||||||
|
state
|
||||||
|
.connect_box
|
||||||
|
.edit_port_forwards(|p| {
|
||||||
|
if p.id == id {
|
||||||
|
modified = true;
|
||||||
|
action
|
||||||
|
} else {
|
||||||
|
PortForwardAction::Keep
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
if modified {
|
||||||
|
cprintln!("<green!>Done!");
|
||||||
|
} else {
|
||||||
|
cprintln!("<red!>No port with id {id} exists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_port_range(s: &str) -> Option<(u16, u16)> {
|
||||||
|
if let Some((start, end)) = s.split_once('-') {
|
||||||
|
Some((start.parse().ok()?, end.parse().ok()?))
|
||||||
|
} else if let Ok(port) = s.parse() {
|
||||||
|
Some((port, port))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,118 @@
|
|||||||
fn main() {
|
use crate::{cli::ShellCommand, utils::QuotableArgs};
|
||||||
println!("Hello, world!");
|
use clap::{FromArgMatches, Parser};
|
||||||
|
use cli::Args;
|
||||||
|
use color_eyre::Result;
|
||||||
|
use color_print::{cformat, cprintln};
|
||||||
|
use connectbox::ConnectBox;
|
||||||
|
use rustyline::{
|
||||||
|
error::ReadlineError,
|
||||||
|
highlight::Highlighter,
|
||||||
|
Completer, Editor, Helper, Hinter, Validator,
|
||||||
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
mod cli;
|
||||||
|
mod commands;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
pub(crate) struct AppState {
|
||||||
|
connect_box: ConnectBox,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Completer, Helper, Hinter, Validator)]
|
||||||
|
struct GreenPrompt;
|
||||||
|
|
||||||
|
impl Highlighter for GreenPrompt {
|
||||||
|
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
|
||||||
|
&'s self,
|
||||||
|
prompt: &'p str,
|
||||||
|
_default: bool,
|
||||||
|
) -> std::borrow::Cow<'b, str> {
|
||||||
|
cformat!("<green!>{prompt}").into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Completer, Helper, Hinter, Validator)]
|
||||||
|
struct PasswordPrompt;
|
||||||
|
|
||||||
|
impl Highlighter for PasswordPrompt {
|
||||||
|
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
|
||||||
|
&'s self,
|
||||||
|
prompt: &'p str,
|
||||||
|
_default: bool,
|
||||||
|
) -> std::borrow::Cow<'b, str> {
|
||||||
|
cformat!("<red!>{prompt}").into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
|
||||||
|
"*".repeat(line.len()).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight_char(&self, _line: &str, _pos: usize) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
let mut shell_cmd = cli::shell_cmd();
|
||||||
|
|
||||||
|
color_eyre::install()?;
|
||||||
|
tracing_subscriber::fmt::fmt()
|
||||||
|
.with_max_level(args.log_level)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let mut rl = Editor::new()?;
|
||||||
|
rl.set_helper(Some(GreenPrompt));
|
||||||
|
|
||||||
|
let password = if let Some(password) = args.password {
|
||||||
|
password
|
||||||
|
} else {
|
||||||
|
let mut rl = Editor::new()?;
|
||||||
|
rl.set_helper(Some(PasswordPrompt));
|
||||||
|
rl.readline("Password: ")?
|
||||||
|
};
|
||||||
|
let history_path = dirs::data_dir()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.join(".connectbox-shell-history");
|
||||||
|
let _err = rl.load_history(&history_path);
|
||||||
|
|
||||||
|
cprintln!("<blue!>Logging in...");
|
||||||
|
let connect_box = ConnectBox::new(args.address, password, true)?;
|
||||||
|
connect_box.login().await?;
|
||||||
|
let state = AppState { connect_box };
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match rl.readline("\n >> ") {
|
||||||
|
Ok(line) => {
|
||||||
|
if line.chars().all(char::is_whitespace) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let matches = shell_cmd.try_get_matches_from_mut(QuotableArgs::new(&line));
|
||||||
|
rl.add_history_entry(line)?;
|
||||||
|
let cmd = match matches {
|
||||||
|
Ok(mut matches) => ShellCommand::from_arg_matches_mut(&mut matches)?,
|
||||||
|
Err(e) => {
|
||||||
|
e.print()?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match cmd {
|
||||||
|
ShellCommand::Exit => break,
|
||||||
|
ShellCommand::PortForwards { cmd } => commands::pfw::run(cmd, &state).await?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(ReadlineError::Interrupted | ReadlineError::Eof) => break,
|
||||||
|
Err(err) => {
|
||||||
|
println!("{err:?}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cprintln!("<blue!>Logging out...");
|
||||||
|
state.connect_box.logout().await?;
|
||||||
|
|
||||||
|
rl.save_history(&history_path)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
40
connectbox-shell/src/utils.rs
Normal file
40
connectbox-shell/src/utils.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
pub(crate) struct QuotableArgs<'a> {
|
||||||
|
s: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> QuotableArgs<'a> {
|
||||||
|
pub fn new(s: &'a str) -> Self {
|
||||||
|
Self { s }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for QuotableArgs<'a> {
|
||||||
|
type Item = &'a str;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.s = self.s.trim_start();
|
||||||
|
if self.s.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if self.s.as_bytes()[0] == b'"' {
|
||||||
|
self.s = &self.s[1..];
|
||||||
|
if let Some(pos) = self.s.find('"') {
|
||||||
|
let result = &self.s[..pos];
|
||||||
|
self.s = &self.s[pos + 1..];
|
||||||
|
return Some(result);
|
||||||
|
}
|
||||||
|
let result = self.s;
|
||||||
|
self.s = &self.s[..0];
|
||||||
|
return Some(result);
|
||||||
|
}
|
||||||
|
if let Some(pos) = self.s.find(char::is_whitespace) {
|
||||||
|
let result = &self.s[..pos];
|
||||||
|
self.s = &self.s[pos..];
|
||||||
|
Some(result)
|
||||||
|
} else {
|
||||||
|
let result = self.s;
|
||||||
|
self.s = &self.s[..0];
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "connectbox"
|
name = "connectbox"
|
||||||
description = "API client library for the Compal CH7465LG, which is a cable modem provided by various European ISPs under the name Connect Box."
|
description = "API client library for the Compal CH7465LG, which is a cable modem provided by various European ISPs under the name Connect Box."
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "EUPL-1.2"
|
license = "EUPL-1.2"
|
||||||
repository = "https://git.lemonsh.moe/lemon/connectbox-rs"
|
repository = "https://git.lemonsh.moe/lemon/connectbox-rs"
|
||||||
|
@ -3,14 +3,18 @@ API client library for the Compal CH7465LG, which is a cable modem provided by v
|
|||||||
|
|
||||||
For more information, see the crate documentation.
|
For more information, see the crate documentation.
|
||||||
|
|
||||||
|
## Supported endpoints
|
||||||
|
- [x] Devices list
|
||||||
|
- [x] Port forwarding
|
||||||
|
- [ ] Wireless settings
|
||||||
|
|
||||||
|
This list will grow as the project progresses.
|
||||||
|
|
||||||
### IPv6 Notice
|
### IPv6 Notice
|
||||||
I am running my modem in the IPv4 mode, so the options available to me are different than what IPv6 mode users see. Thus, this crate will likely not work correctly with IPv6 mode Connect Boxes.
|
I am running my modem in the IPv4 mode, so the options available to me are different than what IPv6 mode users see. Thus, this crate will likely not work correctly with IPv6 mode Connect Boxes.
|
||||||
|
|
||||||
Contributions adding IPv6 support are always welcome, though.
|
Contributions adding IPv6 support are always welcome, though.
|
||||||
|
|
||||||
### Credits
|
### Similar projects
|
||||||
Special thanks to the authors of the following projects:
|
* [home-assistant-ecosystem/python-connect-box](https://github.com/home-assistant-ecosystem/python-connect-box) (Python)
|
||||||
* [home-assistant-ecosystem/python-connect-box](https://github.com/home-assistant-ecosystem/python-connect-box)
|
* [ties/compal_CH7465LG_py](https://github.com/ties/compal_CH7465LG_py) (Python)
|
||||||
* [ties/compal_CH7465LG_py](https://github.com/ties/compal_CH7465LG_py)
|
|
||||||
|
|
||||||
They have saved me from hours of reverse engineering work.
|
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// This example shows you how to add, edit and remove port forwading entries.
|
||||||
|
// Usage: cargo run --example port_forwards -- <Connect Box IP> <login password> <local IP>
|
||||||
|
|
||||||
use std::{env, net::Ipv4Addr};
|
use std::{env, net::Ipv4Addr};
|
||||||
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
@ -10,16 +13,24 @@ use connectbox::{
|
|||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
|
|
||||||
let mut args = env::args().skip(1);
|
let mut args = env::args().skip(1);
|
||||||
let ip = args.next().expect("no ip specified");
|
let ip = args.next().expect("no ip specified");
|
||||||
let code = args.next().expect("no code specified");
|
let password = args.next().expect("no password specified");
|
||||||
|
let local_ip: Ipv4Addr = args
|
||||||
|
.next()
|
||||||
|
.expect("no local ip specified")
|
||||||
|
.parse()
|
||||||
|
.expect("local ip is not a valid ipv4 address");
|
||||||
|
|
||||||
let connect_box = ConnectBox::new(ip, code, true)?;
|
// first, we create a new API client and log in to the router.
|
||||||
|
let connect_box = ConnectBox::new(ip, password, true)?;
|
||||||
connect_box.login().await?;
|
connect_box.login().await?;
|
||||||
|
|
||||||
|
// then, we remove all port forwarding entries for the local ip
|
||||||
connect_box
|
connect_box
|
||||||
.edit_port_forwards(|v| {
|
.edit_port_forwards(|v| {
|
||||||
if v.local_ip == Ipv4Addr::new(192, 168, 0, 180) {
|
if v.local_ip == local_ip {
|
||||||
PortForwardAction::Delete
|
PortForwardAction::Delete
|
||||||
} else {
|
} else {
|
||||||
PortForwardAction::Keep
|
PortForwardAction::Keep
|
||||||
@ -27,9 +38,10 @@ async fn main() -> Result<()> {
|
|||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// then, we add a new port forward for the port 25565 (Minecraft server)
|
||||||
let pf = PortForwardEntry {
|
let pf = PortForwardEntry {
|
||||||
id: 0,
|
id: 0,
|
||||||
local_ip: "192.168.0.180".parse().unwrap(),
|
local_ip,
|
||||||
start_port: 25565,
|
start_port: 25565,
|
||||||
end_port: 25565,
|
end_port: 25565,
|
||||||
start_port_in: 25565,
|
start_port_in: 25565,
|
||||||
@ -39,9 +51,11 @@ async fn main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
connect_box.add_port_forward(&pf).await?;
|
connect_box.add_port_forward(&pf).await?;
|
||||||
|
|
||||||
|
// lastly, we get the new port forwarding table and print it out
|
||||||
let portforwards = connect_box.port_forwards().await?;
|
let portforwards = connect_box.port_forwards().await?;
|
||||||
println!("{portforwards:#?}");
|
println!("{portforwards:#?}");
|
||||||
|
|
||||||
|
// and then we log out so that other users can log in to the web interface
|
||||||
connect_box.logout().await?;
|
connect_box.logout().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
@ -6,7 +6,7 @@ pub enum Error {
|
|||||||
#[error("session token not found, are you logged in?")]
|
#[error("session token not found, are you logged in?")]
|
||||||
NoSessionToken,
|
NoSessionToken,
|
||||||
#[error("incorrect password")]
|
#[error("incorrect password")]
|
||||||
IncorrectCode,
|
IncorrectPassword,
|
||||||
#[error("unexpected response from the server: {0:?}")]
|
#[error("unexpected response from the server: {0:?}")]
|
||||||
UnexpectedResponse(String),
|
UnexpectedResponse(String),
|
||||||
#[error("you are not logged in, or perhaps the session has expired")]
|
#[error("you are not logged in, or perhaps the session has expired")]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//! API client library for the Compal CH7465CE, which is a cable modem provided by various European ISPs under the name Connect Box.
|
//! API client library for the Compal CH7465LG, which is a cable modem provided by various European ISPs under the name Connect Box.
|
||||||
|
|
||||||
#![allow(clippy::missing_errors_doc)]
|
#![allow(clippy::missing_errors_doc)]
|
||||||
use std::{borrow::Cow, fmt::Display, sync::Arc};
|
use std::{borrow::Cow, fmt::Display, sync::Arc};
|
||||||
@ -26,7 +26,7 @@ type Field<'a, 'b> = (Cow<'a, str>, Cow<'b, str>);
|
|||||||
/// The entry point of the library - the API client
|
/// The entry point of the library - the API client
|
||||||
pub struct ConnectBox {
|
pub struct ConnectBox {
|
||||||
http: Client,
|
http: Client,
|
||||||
code: String,
|
password: String,
|
||||||
cookie_store: Arc<Jar>,
|
cookie_store: Arc<Jar>,
|
||||||
base_url: Url,
|
base_url: Url,
|
||||||
getter_url: Url,
|
getter_url: Url,
|
||||||
@ -36,9 +36,9 @@ pub struct ConnectBox {
|
|||||||
|
|
||||||
impl ConnectBox {
|
impl ConnectBox {
|
||||||
/// Create a new client associated with the specified address. You must call [`login`](Self::login()) before use.
|
/// Create a new client associated with the specified address. You must call [`login`](Self::login()) before use.
|
||||||
/// * `code` - the router password
|
/// * `password` - the router password
|
||||||
/// * `auto_reauth` - whether to automatically re-authenticate when the session expires
|
/// * `auto_reauth` - whether to automatically re-authenticate when the session expires
|
||||||
pub fn new(address: impl Display, code: String, auto_reauth: bool) -> Result<Self> {
|
pub fn new(address: impl Display, password: String, auto_reauth: bool) -> Result<Self> {
|
||||||
let cookie_store = Arc::new(Jar::default());
|
let cookie_store = Arc::new(Jar::default());
|
||||||
let http = Client::builder()
|
let http = Client::builder()
|
||||||
.user_agent("Mozilla/5.0")
|
.user_agent("Mozilla/5.0")
|
||||||
@ -50,7 +50,7 @@ impl ConnectBox {
|
|||||||
let setter_url = base_url.join("xml/setter.xml")?;
|
let setter_url = base_url.join("xml/setter.xml")?;
|
||||||
Ok(ConnectBox {
|
Ok(ConnectBox {
|
||||||
http,
|
http,
|
||||||
code,
|
password,
|
||||||
cookie_store,
|
cookie_store,
|
||||||
base_url,
|
base_url,
|
||||||
getter_url,
|
getter_url,
|
||||||
@ -139,7 +139,7 @@ impl ConnectBox {
|
|||||||
("token".into(), session_token.into()),
|
("token".into(), session_token.into()),
|
||||||
("fun".into(), functions::LOGIN.to_string().into()),
|
("fun".into(), functions::LOGIN.to_string().into()),
|
||||||
("Username".into(), "NULL".into()),
|
("Username".into(), "NULL".into()),
|
||||||
("Password".into(), (&self.code).into()),
|
("Password".into(), (&self.password).into()),
|
||||||
];
|
];
|
||||||
let req = self.http.post(self.setter_url.clone()).form(form);
|
let req = self.http.post(self.setter_url.clone()).form(form);
|
||||||
let resp = req.send().await?;
|
let resp = req.send().await?;
|
||||||
@ -155,7 +155,7 @@ impl ConnectBox {
|
|||||||
}
|
}
|
||||||
let resp_text = resp.text().await?;
|
let resp_text = resp.text().await?;
|
||||||
if resp_text == "idloginincorrect" {
|
if resp_text == "idloginincorrect" {
|
||||||
return Err(Error::IncorrectCode);
|
return Err(Error::IncorrectPassword);
|
||||||
}
|
}
|
||||||
let sid = resp_text
|
let sid = resp_text
|
||||||
.strip_prefix("successful;SID=")
|
.strip_prefix("successful;SID=")
|
||||||
@ -201,7 +201,7 @@ impl ConnectBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle or remove port forwards.
|
/// Toggle or remove port forwards.
|
||||||
///
|
///
|
||||||
/// This function accepts a predicate that will be called for every existing port forward. It should decide what to do with each port forward and return a [`PortForwardAction`].
|
/// This function accepts a predicate that will be called for every existing port forward. It should decide what to do with each port forward and return a [`PortForwardAction`].
|
||||||
pub async fn edit_port_forwards<F>(&self, mut f: F) -> Result<()>
|
pub async fn edit_port_forwards<F>(&self, mut f: F) -> Result<()>
|
||||||
where
|
where
|
||||||
@ -262,7 +262,7 @@ impl ConnectBox {
|
|||||||
("end_port".into(), port.end_port.to_string().into()),
|
("end_port".into(), port.end_port.to_string().into()),
|
||||||
("start_portIn".into(), port.start_port_in.to_string().into()),
|
("start_portIn".into(), port.start_port_in.to_string().into()),
|
||||||
("end_portIn".into(), port.end_port_in.to_string().into()),
|
("end_portIn".into(), port.end_port_in.to_string().into()),
|
||||||
("protocol".into(), port.protocol.id().to_string().into()),
|
("protocol".into(), port.protocol.id_str().into()),
|
||||||
("enable".into(), u8::from(port.enable).to_string().into()),
|
("enable".into(), u8::from(port.enable).to_string().into()),
|
||||||
("delete".into(), "0".into()),
|
("delete".into(), "0".into()),
|
||||||
("idd".into(), "".into()),
|
("idd".into(), "".into()),
|
||||||
@ -279,6 +279,7 @@ impl ConnectBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Specifies the action to perform with a given port forward. Used in conjunction with [`ConnectBox::edit_port_forwards`]
|
/// Specifies the action to perform with a given port forward. Used in conjunction with [`ConnectBox::edit_port_forwards`]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum PortForwardAction {
|
pub enum PortForwardAction {
|
||||||
/// Don't do anything with the port forward
|
/// Don't do anything with the port forward
|
||||||
Keep,
|
Keep,
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use std::net::Ipv4Addr;
|
use std::{fmt::Display, net::Ipv4Addr, time::Duration};
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use serde::de::{self, Error, Unexpected};
|
use serde::{
|
||||||
use serde::{Deserialize, Deserializer};
|
de::{self, Error, Unexpected},
|
||||||
|
Deserialize, Deserializer,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct LanUserTable {
|
pub struct LanUserTable {
|
||||||
@ -70,11 +71,21 @@ pub enum PortForwardProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PortForwardProtocol {
|
impl PortForwardProtocol {
|
||||||
pub(crate) fn id(&self) -> u8 {
|
pub(crate) fn id_str(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
PortForwardProtocol::Tcp => 1,
|
PortForwardProtocol::Tcp => "1",
|
||||||
PortForwardProtocol::Udp => 2,
|
PortForwardProtocol::Udp => "2",
|
||||||
PortForwardProtocol::Both => 3,
|
PortForwardProtocol::Both => "3",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(s: &str) -> Option<Self> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"tcp" => Some(Self::Tcp),
|
||||||
|
"udp" => Some(Self::Udp),
|
||||||
|
"both" => Some(Self::Both),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,6 +104,16 @@ impl<'de> Deserialize<'de> for PortForwardProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for PortForwardProtocol {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
PortForwardProtocol::Tcp => "TCP",
|
||||||
|
PortForwardProtocol::Udp => "UDP",
|
||||||
|
PortForwardProtocol::Both => "Both",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn bool_from_int<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
fn bool_from_int<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
|
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
unstable_features = true
|
||||||
|
imports_granularity = "Crate"
|
Loading…
x
Reference in New Issue
Block a user