Address the gap that the port mapper's UPnP path and gateway discovery had no automated coverage (only NAT-PMP did): - UPnP e2e: split map_via_gateway out of discovery and drive it through igd-next against an in-process mock IGD (a std HTTP server returning a canned SOAP envelope). Verifies the real add_port SOAP round-trip and that we request the exact external/internal port + LAN client. - default_gateway parsing is now pure, testable functions with unit tests for the macOS and Linux formats (including the 0.0.0.0 skip). - New 'portmap' CLI subcommand (needs no login): synchronously probes UPnP/NAT-PMP against the real router and reports whether the port was opened + the external address, then removes the test mapping — so a user can verify reachability on their own network. 9 port_mapping tests now cover the NAT-PMP protocol, the UPnP SOAP round-trip, gateway parsing, and the map/renew/cleanup lifecycle.
142 lines
3.4 KiB
Rust
142 lines
3.4 KiB
Rust
use clap::{Parser, Subcommand};
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(
|
|
name = "soulseek-rs",
|
|
author,
|
|
version,
|
|
about = "Soulseek client in Rust 🦀",
|
|
long_about = None
|
|
)]
|
|
pub struct Cli {
|
|
#[arg(short, long, env = "SOULSEEK_USERNAME")]
|
|
pub username: Option<String>,
|
|
|
|
#[arg(short, long, env = "SOULSEEK_PASSWORD")]
|
|
pub password: Option<String>,
|
|
|
|
#[arg(
|
|
short,
|
|
long,
|
|
env = "SOULSEEK_SERVER",
|
|
default_value = "server.slsknet.org:2416"
|
|
)]
|
|
pub server: String,
|
|
|
|
#[arg(long, env = "DISABLE_LISTENER")]
|
|
pub disable_listener: bool,
|
|
|
|
#[arg(short, long, env = "LISTENER_PORT", default_value = "2234")]
|
|
pub listener_port: u16,
|
|
|
|
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
|
|
pub verbose: u8,
|
|
|
|
#[arg(
|
|
long,
|
|
env = "LOG_FILE",
|
|
help = "Write logs to file instead of stderr"
|
|
)]
|
|
pub log_file: Option<PathBuf>,
|
|
|
|
#[command(subcommand)]
|
|
pub command: Option<Commands>,
|
|
|
|
#[arg(
|
|
short,
|
|
long,
|
|
env = "SOULSEEK_DOWNLOAD_DIR",
|
|
default_value = "~/Downloads"
|
|
)]
|
|
pub download_dir: String,
|
|
|
|
#[arg(
|
|
long,
|
|
env = "SOULSEEK_SHARED_DIR",
|
|
help = "Directory whose files are shared with (uploaded to) other peers"
|
|
)]
|
|
pub shared_dir: Option<String>,
|
|
|
|
#[arg(
|
|
short = 'c',
|
|
long,
|
|
env = "MAX_CONCURRENT_DOWNLOADS",
|
|
default_value = "5"
|
|
)]
|
|
pub max_concurrent_downloads: usize,
|
|
|
|
#[arg(long, default_value = "10")]
|
|
pub search_timeout: u64,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
pub enum Commands {
|
|
Search {
|
|
query: String,
|
|
|
|
#[arg(short, long, default_value = "10")]
|
|
timeout: u64,
|
|
|
|
#[arg(short, long, default_value = "~/Downloads")]
|
|
download_dir: String,
|
|
|
|
#[arg(
|
|
short = 'c',
|
|
long,
|
|
env = "MAX_CONCURRENT_DOWNLOADS",
|
|
default_value = "5"
|
|
)]
|
|
max_concurrent_downloads: usize,
|
|
},
|
|
|
|
/// Send a private message to another user
|
|
Message {
|
|
/// Username of the recipient
|
|
username: String,
|
|
|
|
/// Message text to send
|
|
message: String,
|
|
},
|
|
|
|
/// Browse another user's shared files
|
|
Browse {
|
|
/// Username whose shares to list
|
|
username: String,
|
|
},
|
|
|
|
/// List the public chat rooms and their user counts
|
|
Rooms,
|
|
|
|
/// Join a chat room: print messages, or send one and exit
|
|
Chat {
|
|
/// Name of the room to join
|
|
room: String,
|
|
|
|
/// Optional message to say in the room (omit to just listen)
|
|
message: Option<String>,
|
|
|
|
/// Seconds to stay and print incoming messages when only listening
|
|
#[arg(short, long, default_value = "30")]
|
|
listen_secs: u64,
|
|
},
|
|
|
|
/// Test whether your router lets us auto-open the listen port (UPnP/NAT-PMP)
|
|
Portmap,
|
|
}
|
|
|
|
pub fn parse_server_address(server: &str) -> color_eyre::Result<(String, u16)> {
|
|
let parts: Vec<&str> = server.split(':').collect();
|
|
if parts.len() != 2 {
|
|
return Err(color_eyre::eyre::eyre!(
|
|
"Invalid server format. Expected 'host:port', got '{}'",
|
|
server
|
|
));
|
|
}
|
|
let host = parts[0].to_string();
|
|
let port = parts[1].parse::<u16>().map_err(|_| {
|
|
color_eyre::eyre::eyre!("Invalid port number: {}", parts[1])
|
|
})?;
|
|
Ok((host, port))
|
|
}
|