use crate::parse_remote::Remote;
use clap::{arg, command, ArgAction, Args, Parser, Subcommand};
use http::{
header::HeaderName,
uri::{Authority, PathAndQuery, Scheme},
HeaderValue, Uri,
};
use once_cell::sync::OnceCell;
use std::{ops::Deref, str::FromStr};
use thiserror::Error;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct PenguinCli {
#[clap(subcommand)]
pub(crate) subcommand: Commands,
#[arg(short, long, conflicts_with = "quiet", action = ArgAction::Count, global = true)]
pub(crate) verbose: u8,
#[arg(short, long, conflicts_with = "verbose", action = ArgAction::Count, global = true)]
pub(crate) quiet: u8,
}
pub(crate) static ARGS: OnceCell<PenguinCli> = OnceCell::new();
impl PenguinCli {
pub fn get_global() -> &'static Self {
ARGS.get().expect("ARGS is not initialized (this is a bug)")
}
pub(crate) fn parse_global() {
ARGS.set(Self::parse())
.expect("`parse_global` should not be called twice (this is a bug)");
}
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[clap(name = "client")]
Client(ClientArgs),
#[clap(name = "server")]
Server(Box<ServerArgs>),
}
#[derive(Args, Debug)]
pub struct ClientArgs {
pub(crate) server: ServerUrl,
#[arg(num_args=1..=65535, required = true)]
pub(crate) remote: Vec<Remote>,
#[arg(long)]
pub(crate) ws_psk: Option<HeaderValue>,
#[arg(long, default_value_t = 25)]
pub(crate) keepalive: u64,
#[arg(long, default_value_t = 0)]
pub(crate) max_retry_count: u32,
#[arg(long, default_value_t = 300000)]
pub(crate) max_retry_interval: u64,
#[arg(short = 'x', long)]
pub(crate) proxy: Option<String>,
#[arg(short = 'H', long)]
pub(crate) header: Vec<Header>,
#[arg(long)]
pub(crate) hostname: Option<HeaderValue>,
#[arg(short, long)]
pub(crate) tls_ca: Option<String>,
#[arg(short = 'k', long)]
pub(crate) tls_skip_verify: bool,
#[arg(long, requires = "tls_cert")]
pub(crate) tls_key: Option<String>,
#[arg(long, requires = "tls_key")]
pub(crate) tls_cert: Option<String>,
#[arg(long = "pid")]
pub(crate) _pid: bool,
#[arg(long = "fingerprint")]
pub(crate) _fingerprint: Option<String>,
#[arg(long = "auth")]
pub(crate) _auth: Option<String>,
}
#[derive(Args, Debug)]
#[allow(clippy::struct_excessive_bools)]
pub struct ServerArgs {
#[arg(long, default_value = "::")]
pub(crate) host: String,
#[arg(short, long, default_value_t = 8080)]
pub(crate) port: u16,
#[arg(long)]
pub(crate) backend: Option<BackendUrl>,
#[arg(long)]
pub(crate) obfs: bool,
#[arg(long = "404-resp", default_value = "Not found")]
pub(crate) not_found_resp: String,
#[arg(long)]
pub(crate) ws_psk: Option<HeaderValue>,
#[arg(long, requires = "tls_cert")]
pub(crate) tls_key: Option<String>,
#[arg(long, requires = "tls_key")]
pub(crate) tls_cert: Option<String>,
#[arg(long)]
pub(crate) tls_ca: Option<String>,
#[arg(long = "pid")]
pub(crate) _pid: bool,
#[arg(long = "socks5")]
pub(crate) _socks5: bool,
#[arg(long = "reverse")]
pub(crate) _reverse: bool,
#[arg(long = "keepalive", default_value_t = 0)]
pub(crate) _keepalive: u64,
#[arg(long = "auth")]
pub(crate) _auth: Option<String>,
#[arg(long = "authfile")]
pub(crate) _authfile: Option<String>,
#[arg(long = "key")]
pub(crate) _key: Option<String>,
}
#[derive(Debug, Error)]
pub enum ServerUrlError {
#[error("failed to parse server URL: {0}")]
UrlParse(#[from] http::uri::InvalidUri),
#[error("incorrect scheme in server URL: {0}")]
IncorrectScheme(String),
#[error("missing host in server URL")]
MissingHost,
#[error("cannot build server URL: {0}")]
BuildUrl(#[from] http::Error),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ServerUrl(pub Uri);
impl FromStr for ServerUrl {
type Err = ServerUrlError;
fn from_str(url: &str) -> Result<Self, Self::Err> {
let url_parts = match Uri::from_str(url) {
Ok(url) => url.into_parts(),
Err(e) => {
if !url.starts_with("http://")
&& !url.starts_with("https://")
&& !url.starts_with("ws://")
&& !url.starts_with("wss://")
{
let url = format!("ws://{url}");
Uri::from_str(&url)?.into_parts()
} else {
return Err(e.into());
}
}
};
let old_scheme = url_parts.scheme.unwrap_or(http::uri::Scheme::HTTP);
let new_scheme = match old_scheme.as_ref() {
"http" | "ws" => Ok("ws"),
"https" | "wss" => Ok("wss"),
_ => Err(ServerUrlError::IncorrectScheme(old_scheme.to_string())),
}?;
let url = Uri::builder()
.scheme(new_scheme)
.authority(url_parts.authority.ok_or(Self::Err::MissingHost)?)
.path_and_query(
url_parts
.path_and_query
.unwrap_or_else(|| PathAndQuery::from_static("/")),
)
.build()?;
Ok(Self(url))
}
}
impl Deref for ServerUrl {
type Target = Uri;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Error)]
pub enum BackendUrlError {
#[error("failed to parse backend URL: {0}")]
UrlParse(#[from] http::uri::InvalidUri),
#[error("missing authority in backend URL")]
MissingAuthority,
#[error("invalid backend scheme: {0}")]
InvalidScheme(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendUrl {
pub scheme: Scheme,
pub authority: Authority,
pub path: PathAndQuery,
}
impl FromStr for BackendUrl {
type Err = BackendUrlError;
fn from_str(url: &str) -> Result<Self, Self::Err> {
let url_parts = Uri::from_str(url)?.into_parts();
let scheme = url_parts.scheme.unwrap_or(Scheme::HTTP);
if scheme != Scheme::HTTP && scheme != Scheme::HTTPS {
return Err(BackendUrlError::InvalidScheme(scheme.to_string()));
}
Ok(Self {
scheme,
authority: url_parts
.authority
.ok_or(BackendUrlError::MissingAuthority)?,
path: url_parts
.path_and_query
.unwrap_or_else(|| PathAndQuery::from_static("/")),
})
}
}
impl ToString for BackendUrl {
fn to_string(&self) -> String {
let mut url = String::new();
url.push_str(self.scheme.as_str());
url.push_str("://");
url.push_str(self.authority.as_str());
url.push_str(self.path.as_str());
url
}
}
#[derive(Debug, Error)]
pub enum HeaderError {
#[error("invalid header value or hostname: {0}")]
Value(#[from] http::header::InvalidHeaderValue),
#[error("invalid header name: {0}")]
Name(#[from] http::header::InvalidHeaderName),
#[error("invalid header: {0}")]
Format(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Header {
pub(crate) name: HeaderName,
pub(crate) value: HeaderValue,
}
impl FromStr for Header {
type Err = HeaderError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (name, value) = s
.split_once(':')
.ok_or_else(|| Self::Err::Format(s.to_string()))?;
let name = HeaderName::from_str(name)?;
let value = HeaderValue::from_str(value.trim())?;
Ok(Self { name, value })
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use crate::parse_remote::{LocalSpec, Protocol, RemoteSpec};
use super::*;
#[test]
fn test_serverurl_fromstr() {
assert_eq!(
ServerUrl::from_str("example.com").unwrap().to_string(),
"ws://example.com/"
);
assert_eq!(
ServerUrl::from_str("wss://example.com")
.unwrap()
.to_string(),
"wss://example.com/"
);
assert_eq!(
ServerUrl::from_str("ws://example.com").unwrap().to_string(),
"ws://example.com/"
);
assert_eq!(
ServerUrl::from_str("https://example.com")
.unwrap()
.to_string(),
"wss://example.com/"
);
assert_eq!(
ServerUrl::from_str("http://example.com")
.unwrap()
.to_string(),
"ws://example.com/"
);
ServerUrl::from_str("ftp://example.com").unwrap_err();
}
#[test]
fn test_backendurl_fromstr() {
assert_eq!(
BackendUrl::from_str("https://example.com")
.unwrap()
.to_string(),
"https://example.com/"
);
assert_eq!(
BackendUrl::from_str("http://example.com")
.unwrap()
.to_string(),
"http://example.com/"
);
assert_eq!(
BackendUrl::from_str("https://example.com/foo").unwrap(),
BackendUrl {
scheme: Scheme::HTTPS,
authority: Authority::from_static("example.com"),
path: PathAndQuery::from_static("/foo"),
}
);
assert_eq!(
BackendUrl::from_str("http://example.com/foo?bar")
.unwrap()
.to_string(),
"http://example.com/foo?bar"
);
BackendUrl::from_str("ftp://example.com").unwrap_err();
BackendUrl::from_str("http://").unwrap_err();
}
#[test]
fn test_header_parser() {
let header = Header::from_str("X-Test: test").unwrap();
assert_eq!(header.name.as_str().to_lowercase(), "X-Test".to_lowercase());
header.value.to_str().unwrap();
assert_eq!(header.value.to_str().unwrap(), "test");
Header::from_str("X-Test").unwrap_err();
Header::from_str(": test").unwrap_err();
let header = Header::from_str("X-Test: test: test").unwrap();
assert_eq!(header.name.as_str().to_lowercase(), "X-Test".to_lowercase());
header.value.to_str().unwrap();
assert_eq!(header.value.to_str().unwrap(), "test: test");
let header = Header::from_str("X-Test:test").unwrap();
assert_eq!(header.name.as_str().to_lowercase(), "X-Test".to_lowercase());
header.value.to_str().unwrap();
assert_eq!(header.value.to_str().unwrap(), "test");
}
#[test]
fn test_client_args_minimal() {
let args = PenguinCli::parse_from(["penguin", "client", "127.0.0.1:9999/endpoint", "1234"]);
assert!(matches!(args.subcommand, Commands::Client(_)));
if let Commands::Client(args) = args.subcommand {
let server_uri = args.server.0;
assert_eq!(server_uri.scheme_str(), Some("ws"));
assert_eq!(server_uri.host(), Some("127.0.0.1"));
assert_eq!(server_uri.port_u16(), Some(9999));
assert_eq!(server_uri.path(), "/endpoint");
assert_eq!(
args.remote,
[Remote {
local_addr: LocalSpec::Inet(("0.0.0.0".to_string(), 1234)),
remote_addr: RemoteSpec::Inet(("127.0.0.1".to_string(), 1234)),
protocol: Protocol::Tcp,
}]
);
}
}
#[test]
fn test_client_args_full() {
let args = PenguinCli::parse_from([
"penguin",
"client",
"wss://127.0.0.1:9999/endpoint",
"stdio:localhost:53/udp",
"192.168.1.1:8080:localhost:80/tcp",
"--ws-psk",
"avocado",
"--keepalive",
"10",
"--max-retry-count",
"400",
"--max-retry-interval",
"1000",
"--proxy",
"socks5://abc:123@localhost:1080",
"--header",
"X-Test: test",
"--hostname",
"example.com",
]);
assert!(matches!(args.subcommand, Commands::Client(_)));
if let Commands::Client(args) = args.subcommand {
assert_eq!(
args.server,
ServerUrl::from_str("wss://127.0.0.1:9999/endpoint").unwrap()
);
assert_eq!(
args.remote,
[
Remote {
local_addr: LocalSpec::Stdio,
remote_addr: RemoteSpec::Inet(("localhost".to_string(), 53)),
protocol: Protocol::Udp,
},
Remote {
local_addr: LocalSpec::Inet(("192.168.1.1".to_string(), 8080)),
remote_addr: RemoteSpec::Inet(("localhost".to_string(), 80)),
protocol: Protocol::Tcp,
},
]
);
assert_eq!(args.ws_psk, Some(HeaderValue::from_static("avocado")));
assert_eq!(args.keepalive, 10);
assert_eq!(args.max_retry_count, 400);
assert_eq!(args.max_retry_interval, 1000);
assert_eq!(
args.proxy,
Some("socks5://abc:123@localhost:1080".to_string())
);
assert_eq!(args.header, [Header::from_str("X-Test:test").unwrap()]);
assert_eq!(args.hostname, Some(HeaderValue::from_static("example.com")));
}
}
}