use std::{collections::BTreeMap, net::SocketAddr};
use clap::{Parser, Subcommand};
use sozu_command_lib::proxy::{LoadBalancingAlgorithms, TlsVersion};
#[derive(Parser, PartialEq, Eq, Clone, Debug)]
#[clap(author, version, about)]
pub struct Args {
#[clap(
short = 'c',
long = "config",
global = true,
help = "Sets a custom config file"
)]
pub config: Option<String>,
#[clap(
short = 't',
long = "timeout",
global = true,
help = "Sets a custom timeout for commands (in milliseconds). 0 disables the timeout"
)]
pub timeout: Option<u64>,
#[clap(subcommand)]
pub cmd: SubCmd,
}
impl paw::ParseArgs for Args {
type Error = std::io::Error;
fn parse_args() -> Result<Self, Self::Error> {
Ok(Self::parse())
}
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum SubCmd {
#[clap(name = "start", about = "launch the main process")]
Start,
#[clap(
name = "worker",
about = "start a worker (internal command, should not be used directly)"
)]
Worker {
#[clap(long = "id", help = "worker identifier")]
id: i32,
#[clap(
long = "fd",
help = "IPC file descriptor of the worker to main channel"
)]
fd: i32,
#[clap(
long = "scm",
help = "IPC SCM_RIGHTS file descriptor of the worker to main scm socket"
)]
scm: i32,
#[clap(
long = "configuration-state-fd",
help = "configuration data file descriptor"
)]
configuration_state_fd: i32,
#[clap(
long = "command-buffer-size",
help = "Worker's channel buffer size",
default_value = "1000000"
)]
command_buffer_size: usize,
#[clap(
long = "max-command-buffer-size",
help = "Worker's channel max buffer size"
)]
max_command_buffer_size: Option<usize>,
},
#[clap(
name = "main",
about = "start a new main process (internal command, should not be used directly)"
)]
Main {
#[clap(long = "fd", help = "IPC file descriptor")]
fd: i32,
#[clap(long = "upgrade-fd", help = "upgrade data file descriptor")]
upgrade_fd: i32,
#[clap(
long = "command-buffer-size",
help = "Main process channel buffer size",
default_value = "1000000"
)]
command_buffer_size: usize,
#[clap(
long = "max-command-buffer-size",
help = "Main process channel max buffer size"
)]
max_command_buffer_size: Option<usize>,
},
#[clap(name = "shutdown", about = "shuts down the proxy")]
Shutdown {
#[clap(long = "hard", help = "do not wait for connections to finish")]
hard: bool,
#[clap(
short = 'w',
long = "worker",
help = "shuts down the worker with this id"
)]
worker: Option<u32>,
},
#[clap(name = "upgrade", about = "upgrade the proxy")]
Upgrade {
#[clap(short = 'w', long = "worker", help = "Upgrade the worker with this id")]
worker: Option<u32>,
},
#[clap(name = "status", about = "gets information on the running workers")]
Status {
#[clap(
short = 'j',
long = "json",
help = "Print the command result in JSON format"
)]
json: bool,
},
#[clap(
name = "metrics",
about = "gets statistics on the main process and its workers"
)]
Metrics {
#[clap(
short = 'j',
long = "json",
help = "Print the command result in JSON format",
global = true
)]
json: bool,
#[clap(subcommand)]
cmd: MetricsCmd,
},
#[clap(name = "logging", about = "change logging level")]
Logging {
#[clap(subcommand)]
level: LoggingLevel,
},
#[clap(name = "state", about = "state management")]
State {
#[clap(subcommand)]
cmd: StateCmd,
},
#[clap(
name = "reload",
about = "Reloads routing configuration (clusters, frontends and backends)"
)]
Reload {
#[clap(
short = 'f',
long = "file",
help = "use a different configuration file from the current one"
)]
file: Option<String>,
#[clap(
short = 'j',
long = "json",
help = "Print the command result in JSON format"
)]
json: bool,
},
#[clap(name = "cluster", about = "cluster management")]
Cluster {
#[clap(subcommand)]
cmd: ClusterCmd,
},
#[clap(name = "backend", about = "backend management")]
Backend {
#[clap(subcommand)]
cmd: BackendCmd,
},
#[clap(name = "frontend", about = "frontend management")]
Frontend {
#[clap(subcommand)]
cmd: FrontendCmd,
},
#[clap(name = "listener", about = "listener management")]
Listener {
#[clap(subcommand)]
cmd: ListenerCmd,
},
#[clap(name = "certificate", about = "certificate management")]
Certificate {
#[clap(subcommand)]
cmd: CertificateCmd,
},
#[clap(name = "query", about = "configuration state verification")]
Query {
#[clap(
short = 'j',
long = "json",
help = "Print the command result in JSON format",
global = true
)]
json: bool,
#[clap(subcommand)]
cmd: QueryCmd,
},
#[clap(name = "config", about = "configuration file management")]
Config {
#[clap(subcommand)]
cmd: ConfigCmd,
},
#[clap(name = "events", about = "receive sozu events")]
Events,
#[clap(
name = "acme",
next_line_help = false,
about = "Set up or renew Let's Encrypt certificates for sozu HTTP reverse proxy"
)]
Acme {
#[clap(long = "config", help = "Sets a custom config file")]
config: String,
#[clap(long = "domain", help = "the application's domain name")]
domain: String,
#[clap(long = "email", help = "registration email")]
email: String,
#[clap(long = "id", help = "cluster identifier (usually an application)")]
cluster_id: String,
#[clap(long = "old-cert", help = "path of the previous certificate (optional)")]
old_certificate_path: Option<String>,
#[clap(long = "new-cert", help = "where to write the new certificate")]
new_certificate_path: String,
#[clap(long = "chain", help = "where to write the certificate chain")]
certificate_chain_path: String,
#[clap(short = 'k', long = "key-path", help = "where to write the key")]
key_path: String,
#[clap(long = "http", help = "HTTP frontend address, in the format IP:port")]
http_frontend_address: String,
#[clap(long = "https", help = "HTTPS frontend address, in the format IP:port")]
https_frontend_address: String,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum MetricsCmd {
#[clap(name = "enable", about = "Enables local metrics collection")]
Enable,
#[clap(name = "disable", about = "Disables local metrics collection")]
Disable,
#[clap(name = "clear", about = "Deletes local metrics data")]
Clear,
#[clap(name = "get", about = "get all or filtered metrics")]
Get {
#[clap(short, long, help = "list the available metrics on the proxy level")]
list: bool,
#[clap(short, long, help = "refresh metrics results (in seconds)")]
refresh: Option<u32>,
#[clap(
short = 'n',
long = "names",
help = "Filter by metric names. Coma-separated list.",
use_value_delimiter = true
)]
names: Vec<String>,
#[clap(
short = 'k',
long = "clusters",
help = "list of cluster ids (= application id)",
use_value_delimiter = true
)]
clusters: Vec<String>,
#[clap(
short = 'b',
long="backends",
help="coma-separated list of backends, 'one_backend_id,other_backend_id'",
use_value_delimiter = true
// parse(try_from_str = split_slash)
)]
backends: Vec<String>,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum LoggingLevel {
#[clap(name = "trace", about = "Displays a LOT of logs")]
Trace,
#[clap(
name = "debug",
about = "Displays more logs about the inner workings of Sōzu"
)]
Debug,
#[clap(name = "error", about = "Displays occurring errors")]
Error,
#[clap(name = "warn", about = "Displays warnings about non-critical errors")]
Warn,
#[clap(name = "info", about = "Displays logs about normal behaviour of Sōzu")]
Info,
}
impl std::fmt::Display for LoggingLevel {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum StateCmd {
#[clap(name = "save", about = "Save state to that file")]
Save {
#[clap(short = 'f', long = "file")]
file: String,
},
#[clap(name = "load", about = "Load state from that file")]
Load {
#[clap(short = 'f', long = "file")]
file: String,
},
#[clap(name = "dump", about = "Dump current state to STDOUT")]
Dump {
#[clap(
short = 'j',
long = "json",
help = "Print the command result in JSON format"
)]
json: bool,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum ClusterCmd {
#[clap(name = "remove", about = "Remove a cluster")]
Remove {
#[clap(short = 'i', long = "id", help = "cluster id")]
id: String,
},
#[clap(name = "add", about = "Add a cluster")]
Add {
#[clap(short = 'i', long = "id", help = "cluster id")]
id: String,
#[clap(short = 's', long = "sticky-session")]
sticky_session: bool,
#[clap(short = 'r', long = "https-redirect")]
https_redirect: bool,
#[clap(
long = "send-proxy",
help = "Enforces use of the PROXY protocol version 2 over any connection established to this server."
)]
send_proxy: bool,
#[clap(
long = "expect-proxy",
help = "Configures the client-facing connection to receive a PROXY protocol header version 2"
)]
expect_proxy: bool,
#[clap(
long = "load-balancing-policy",
help = "Configures the load balancing policy. Possible values are 'roundrobin', 'random' or 'leastconnections'"
)]
load_balancing_policy: LoadBalancingAlgorithms,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum BackendCmd {
#[clap(name = "remove", about = "Remove a backend")]
Remove {
#[clap(short = 'i', long = "id")]
id: String,
#[clap(long = "backend-id")]
backend_id: String,
#[clap(
short = 'a',
long = "address",
help = "server address, format: IP:port"
)]
address: SocketAddr,
},
#[clap(name = "add", about = "Add a backend")]
Add {
#[clap(short = 'i', long = "id")]
id: String,
#[clap(long = "backend-id")]
backend_id: String,
#[clap(
short = 'a',
long = "address",
help = "server address, format: IP:port"
)]
address: SocketAddr,
#[clap(
short = 's',
long = "sticky-id",
help = "value for the sticky session cookie"
)]
sticky_id: Option<String>,
#[clap(short = 'b', long = "backup", help = "set backend as a backup backend")]
backup: Option<bool>,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum FrontendCmd {
#[clap(name = "http", about = "HTTP frontend management")]
Http {
#[clap(subcommand)]
cmd: HttpFrontendCmd,
},
#[clap(name = "https", about = "HTTPS frontend management")]
Https {
#[clap(subcommand)]
cmd: HttpFrontendCmd,
},
#[clap(name = "tcp", about = "TCP frontend management")]
Tcp {
#[clap(subcommand)]
cmd: TcpFrontendCmd,
},
#[clap(name = "list", about = "List frontends using filters")]
List {
#[clap(long = "http", help = "filter for http frontends")]
http: bool,
#[clap(long = "https", help = "filter for https frontends")]
https: bool,
#[clap(long = "tcp", help = "filter for tcp frontends")]
tcp: bool,
#[clap(
short = 'd',
long = "domain",
help = "filter by domain name (for http & https frontends)"
)]
domain: Option<String>,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum Route {
Id {
id: String,
},
Deny,
}
#[allow(clippy::from_over_into)]
impl std::convert::Into<sozu_command_lib::proxy::Route> for Route {
fn into(self) -> sozu_command_lib::proxy::Route {
match self {
Route::Deny => sozu_command_lib::proxy::Route::Deny,
Route::Id { id } => sozu_command_lib::proxy::Route::ClusterId(id),
}
}
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum HttpFrontendCmd {
#[clap(name = "add")]
Add {
#[clap(
short = 'a',
long = "address",
help = "frontend address, format: IP:port"
)]
address: SocketAddr,
#[clap(subcommand, name = "route")]
route: Route,
#[clap(long = "hostname", aliases = &["host"])]
hostname: String,
#[clap(short = 'p', long = "path-prefix", help = "URL prefix of the frontend")]
path_prefix: Option<String>,
#[clap(
long = "path-regex",
help = "the frontend URL path should match this regex"
)]
path_regex: Option<String>,
#[clap(
long = "path-equals",
help = "the frontend URL path should equal this regex"
)]
path_equals: Option<String>,
#[clap(short = 'm', long = "method", help = "HTTP method")]
method: Option<String>,
#[clap(long = "tags", help = "Specify tag (key-value pair) to apply on front-end (example: 'key=value, other-key=other-value')", value_parser = parse_tags)]
tags: Option<BTreeMap<String, String>>,
},
#[clap(name = "remove")]
Remove {
#[clap(
short = 'a',
long = "address",
help = "frontend address, format: IP:port"
)]
address: SocketAddr,
#[clap(subcommand, name = "route")]
route: Route,
#[clap(long = "hostname", aliases = &["host"])]
hostname: String,
#[clap(short = 'p', long = "path-prefix", help = "URL prefix of the frontend")]
path_prefix: Option<String>,
#[clap(
long = "path-regex",
help = "the frontend URL path should match this regex"
)]
path_regex: Option<String>,
#[clap(
long = "path-equals",
help = "the frontend URL path should equal this regex"
)]
path_equals: Option<String>,
#[clap(short = 'm', long = "method", help = "HTTP method")]
method: Option<String>,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum TcpFrontendCmd {
#[clap(name = "add")]
Add {
#[clap(
short = 'i',
long = "id",
help = "the id of the cluster to which the frontend belongs"
)]
id: String,
#[clap(
short = 'a',
long = "address",
help = "frontend address, format: IP:port"
)]
address: SocketAddr,
#[clap(
long = "tags",
help = "Specify tag (key-value pair) to apply on front-end (example: 'key=value, other-key=other-value')",
value_parser = parse_tags
)]
tags: Option<BTreeMap<String, String>>,
},
#[clap(name = "remove")]
Remove {
#[clap(
short = 'i',
long = "id",
help = "the id of the cluster to which the frontend belongs"
)]
id: String,
#[clap(
short = 'a',
long = "address",
help = "frontend address, format: IP:port"
)]
address: SocketAddr,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum ListenerCmd {
#[clap(name = "http", about = "HTTP listener management")]
Http {
#[clap(subcommand)]
cmd: HttpListenerCmd,
},
#[clap(name = "https", about = "HTTPS listener management")]
Https {
#[clap(subcommand)]
cmd: HttpsListenerCmd,
},
#[clap(name = "tcp", about = "TCP listener management")]
Tcp {
#[clap(subcommand)]
cmd: TcpListenerCmd,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum HttpListenerCmd {
#[clap(name = "add")]
Add {
#[clap(short = 'a')]
address: SocketAddr,
#[clap(
long = "public-address",
help = "a different IP than the one the socket sees, for logs and forwarded headers"
)]
public_address: Option<SocketAddr>,
#[clap(
long = "answer-404",
help = "path to file of the 404 answer sent to the client when a frontend is not found"
)]
answer_404: Option<String>,
#[clap(
long = "answer-503",
help = "path to file of the 503 answer sent to the client when a cluster has no backends available"
)]
answer_503: Option<String>,
#[clap(
long = "expect-proxy",
help = "Configures the client socket to receive a PROXY protocol header"
)]
expect_proxy: bool,
#[clap(long = "sticky-name", help = "sticky session cookie name")]
sticky_name: Option<String>,
#[clap(long = "front-timeout", help = "Set front timeout")]
front_timeout: Option<u32>,
#[clap(long = "back-timeout", help = "Set back timeout")]
back_timeout: Option<u32>,
#[clap(long = "request-timeout", help = "Set request timeout")]
request_timeout: Option<u32>,
#[clap(long = "connect-timeout", help = "Set connect timeout")]
connect_timeout: Option<u32>,
},
#[clap(name = "remove")]
Remove {
#[clap(
short = 'a',
long = "address",
help = "listener address, format: IP:port"
)]
address: SocketAddr,
},
#[clap(name = "activate")]
Activate {
#[clap(
short = 'a',
long = "address",
help = "listener address, format: IP:port"
)]
address: SocketAddr,
},
#[clap(name = "deactivate")]
Deactivate {
#[clap(
short = 'a',
long = "address",
help = "listener address, format: IP:port"
)]
address: SocketAddr,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum HttpsListenerCmd {
#[clap(name = "add")]
Add {
#[clap(short = 'a')]
address: SocketAddr,
#[clap(
long = "public-address",
help = "a different IP than the one the socket sees, for logs and forwarded headers"
)]
public_address: Option<SocketAddr>,
#[clap(
long = "answer-404",
help = "path to file of the 404 answer sent to the client when a frontend is not found"
)]
answer_404: Option<String>,
#[clap(
long = "answer-503",
help = "path to file of the 503 answer sent to the client when a cluster has no backends available"
)]
answer_503: Option<String>,
#[clap(long = "tls-versions", help = "list of TLS versions to use")]
tls_versions: Vec<TlsVersion>,
#[clap(
long = "tls-cipher-list",
help = "List of TLS cipher list to use (TLSv1.2 and TLSv1.3)"
)]
cipher_list: Option<Vec<String>>,
#[clap(
long = "expect-proxy",
help = "Configures the client socket to receive a PROXY protocol header"
)]
expect_proxy: bool,
#[clap(long = "sticky-name", help = "sticky session cookie name")]
sticky_name: Option<String>,
#[clap(long = "front-timeout", help = "Set front timeout")]
front_timeout: Option<u32>,
#[clap(long = "back-timeout", help = "Set back timeout")]
back_timeout: Option<u32>,
#[clap(long = "request-timeout", help = "Set request timeout")]
request_timeout: Option<u32>,
#[clap(long = "connect-timeout", help = "Set connect timeout")]
connect_timeout: Option<u32>,
},
#[clap(name = "remove")]
Remove {
#[clap(
short = 'a',
long = "address",
help = "listener address, format: IP:port"
)]
address: SocketAddr,
},
#[clap(name = "activate")]
Activate {
#[clap(
short = 'a',
long = "address",
help = "listener address, format: IP:port"
)]
address: SocketAddr,
},
#[clap(name = "deactivate")]
Deactivate {
#[clap(
short = 'a',
long = "address",
help = "listener address, format: IP:port"
)]
address: SocketAddr,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum TcpListenerCmd {
#[clap(name = "add")]
Add {
#[clap(
short = 'a',
long = "address",
help = "listener address, format: IP:port"
)]
address: SocketAddr,
#[clap(
long = "public-address",
help = "a different IP than the one the socket sees, for logs and forwarded headers"
)]
public_address: Option<SocketAddr>,
#[clap(
long = "expect-proxy",
help = "Configures the client socket to receive a PROXY protocol header"
)]
expect_proxy: bool,
},
#[clap(name = "remove")]
Remove {
#[clap(
short = 'a',
long = "address",
help = "listener address, format: IP:port"
)]
address: SocketAddr,
},
#[clap(name = "activate")]
Activate {
#[clap(
short = 'a',
long = "address",
help = "listener address, format: IP:port"
)]
address: SocketAddr,
},
#[clap(name = "deactivate")]
Deactivate {
#[clap(
short = 'a',
long = "address",
help = "listener address, format: IP:port"
)]
address: SocketAddr,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum CertificateCmd {
#[clap(name = "add", about = "Add a certificate")]
Add {
#[clap(
short = 'a',
long = "address",
help = "listener address, format: IP:port"
)]
address: SocketAddr,
#[clap(long = "certificate", help = "path to the certificate")]
certificate: String,
#[clap(long = "certificate-chain", help = "path to the certificate chain")]
chain: String,
#[clap(long = "key", help = "path to the key")]
key: String,
#[clap(long = "tls-versions", help = "accepted TLS versions for this certificate",
value_parser = parse_tls_versions)]
tls_versions: Vec<TlsVersion>,
},
#[clap(name = "remove", about = "Remove a certificate")]
Remove {
#[clap(
short = 'a',
long = "address",
help = "listener address, format: IP:port"
)]
address: SocketAddr,
#[clap(aliases = &["cert"], long = "certificate", help = "path to the certificate")]
certificate: Option<String>,
#[clap(short = 'f', long = "fingerprint", help = "certificate fingerprint")]
fingerprint: Option<String>,
},
#[clap(name = "replace", about = "Replace an existing certificate")]
Replace {
#[clap(
short = 'a',
long = "address",
help = "listener address, format: IP:port"
)]
address: SocketAddr,
#[clap(long = "new-certificate", help = "path to the new certificate")]
certificate: String,
#[clap(
long = "new-certificate-chain",
help = "path to the new certificate chain"
)]
chain: String,
#[clap(long = "new-key", help = "path to the new key")]
key: String,
#[clap(
aliases = &["old-cert"],
long = "old-certificate",
help = "path to the old certificate"
)]
old_certificate: Option<String>,
#[clap(
short = 'f',
long = "fingerprint",
help = "old certificate fingerprint"
)]
old_fingerprint: Option<String>,
#[clap(long = "tls-versions", help = "accepted TLS versions for this certificate",
value_parser = parse_tls_versions)]
tls_versions: Vec<TlsVersion>,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum QueryCmd {
#[clap(name = "clusters", about = "Query clusters matching a specific filter")]
Clusters {
#[clap(short = 'i', long = "id", help = "cluster identifier")]
id: Option<String>,
#[clap(short = 'd', long = "domain", help = "cluster domain name")]
domain: Option<String>,
},
#[clap(
name = "certificates",
about = "Query certificates matching a specific filter"
)]
Certificates {
#[clap(short = 'f', long = "fingerprint", help = "certificate fingerprint")]
fingerprint: Option<String>,
#[clap(short = 'd', long = "domain", help = "domain name")]
domain: Option<String>,
},
}
#[derive(Subcommand, PartialEq, Eq, Clone, Debug)]
pub enum ConfigCmd {
#[clap(name = "check", about = "check configuration file syntax and exit")]
Check {},
}
fn parse_tls_versions(i: &str) -> Result<TlsVersion, String> {
match i {
"TLSv1" => Ok(TlsVersion::TLSv1_0),
"TLSv1.1" => Ok(TlsVersion::TLSv1_1),
"TLSv1.2" => Ok(TlsVersion::TLSv1_2),
"TLSv1.3" => Ok(TlsVersion::TLSv1_2),
s => Err(format!("unrecognized TLS version: {}", s)),
}
}
fn parse_tags(string_to_parse: &str) -> Result<BTreeMap<String, String>, String> {
let mut tags: BTreeMap<String, String> = BTreeMap::new();
for s in string_to_parse.split(',') {
if let Some((key, value)) = s.trim().split_once('=') {
tags.insert(key.to_owned(), value.to_owned());
} else {
return Err(format!(
"something went wrong while parsing the tags '{}'",
string_to_parse
));
}
}
Ok(tags)
}
#[cfg(test)]
mod tests {
#[test]
fn parse_tags_from_string() {
use super::*;
let tags_to_parse =
"owner=John ,uuid=0dd8d7b1-a50a-461a-b1f9-5211a5f45a83=, hexkey=#846e84";
assert_eq!(
Ok(BTreeMap::from([
("owner".to_owned(), "John".to_owned()),
(
"uuid".to_owned(),
"0dd8d7b1-a50a-461a-b1f9-5211a5f45a83=".to_owned(),
),
("hexkey".to_owned(), "#846e84".to_owned())
])),
parse_tags(tags_to_parse)
);
}
}