From b6c5edac71acd8d9359f05652200b580a4974be5 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Mon, 3 Nov 2025 16:32:13 +0200 Subject: [PATCH 1/4] psq-server: don't apply default on read error Especially when dialing up log levels, it can be easily missed if the explicitly specified config file does not exist. Instead of having a String with a default path, make it an Option<_>, so we know later if the user explicitly specified a config or not. In case they didn't, we can fallback to Config::create_default(). Also, make the types a bit more idiomatic - use a PathBuf, and make Config::read_from_file take any AsRef, which includes &str, &Path, PathBuf and String. --- src/bin/psq-server.rs | 29 ++++++++++++++--------------- src/server/config.rs | 4 +++- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/bin/psq-server.rs b/src/bin/psq-server.rs index cb7a050..e4a1db4 100644 --- a/src/bin/psq-server.rs +++ b/src/bin/psq-server.rs @@ -2,7 +2,10 @@ extern crate log; use clap::Parser; -use std::net::SocketAddr; +use std::{ + net::SocketAddr, + path::{Path, PathBuf}, +}; use pasque::{server::Config, PsqServer}; @@ -11,16 +14,12 @@ async fn main() { env_logger::builder().format_timestamp_nanos().init(); let args = Args::new(); - let config = match Config::read_from_file(args.config()) { - Ok(c) => c, - Err(e) => { - warn!( - "Could not read config '{}': {}. Applying default configuration.", - args.config(), - e, - ); - Config::create_default() - } + + let config = if let Some(config_path) = args.config_path() { + Config::read_from_file(config_path).expect("unable to read config file") + } else { + warn!("No config specified, using default configuration."); + Config::create_default() }; let mut psqserver = PsqServer::start(&args.address(), &config).await.unwrap(); @@ -40,8 +39,8 @@ pub struct Args { address: Vec, /// Configuration file to read. - #[arg(short, long, default_value = "src/bin/server-example.json")] - config: String, + #[arg(short, long)] + config: Option, } impl Args { @@ -55,7 +54,7 @@ impl Args { &self.address } - pub fn config(&self) -> &String { - &self.config + pub fn config_path(&self) -> Option<&Path> { + self.config.as_ref().map(PathBuf::as_path) } } diff --git a/src/server/config.rs b/src/server/config.rs index 5847e8a..f08fa00 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -64,7 +64,9 @@ enum Endpoint { impl Config { /// Read JSON-formatted configuration from given configuration file - pub fn read_from_file(filename: &str) -> core::result::Result { + pub fn read_from_file( + filename: impl AsRef, + ) -> core::result::Result { let file = match File::open(filename) { Ok(f) => f, Err(e) => { From 89695b97c2b5477a9a0fffa5fc65c6678c076fa2 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Mon, 3 Nov 2025 16:44:41 +0200 Subject: [PATCH 2/4] PsqServer: config: use server-example.json for impl Default We can deduplicate the config. It also avoids having server-example.json to be present on the host this is running on. --- README.md | 7 ++++--- src/bin/psq-server.rs | 2 +- src/server/config.rs | 37 +++++++++++++++++++++++-------------- src/stream/iptunnel.rs | 3 ++- tests/integration.rs | 6 +++--- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 3d371a1..aa0f739 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,10 @@ to bind both IPv4 and IPv6 addresses, for example: `-a 0.0.0.0:443 -a [::]:443`. The server needs a JSON configuration file that gives links to files containing TLS certificate and private key are given in a JSON configuration file. The -configuration file is given with `-c` option. By default, an example -configuration file **[server-example.json](src/bin/server-example.json)** is used, -that contains link to an invalid certificate, but can be used for development +configuration file is given with `-c` option. +If no config is specified, the example configuration file from +**[server-example.json](src/bin/server-example.json)** (at build time) is used. +It points to an invalid certificate, but can be used for development and testing, if certificate validation is disabled at client. **Starting the example client:** diff --git a/src/bin/psq-server.rs b/src/bin/psq-server.rs index e4a1db4..978d81f 100644 --- a/src/bin/psq-server.rs +++ b/src/bin/psq-server.rs @@ -19,7 +19,7 @@ async fn main() { Config::read_from_file(config_path).expect("unable to read config file") } else { warn!("No config specified, using default configuration."); - Config::create_default() + Config::default() }; let mut psqserver = PsqServer::start(&args.address(), &config).await.unwrap(); diff --git a/src/server/config.rs b/src/server/config.rs index f08fa00..6584937 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -3,7 +3,7 @@ //! Typically read from a JSON file. Includes certificate parameters and //! configurations for different kinds of endpoints. -use std::{fs::File, io::BufReader}; +use std::{fs::File, io::BufReader, sync::LazyLock}; use serde::Deserialize; @@ -17,7 +17,7 @@ use super::PsqServer; /// are configured and what fields they have. /// /// [server-example.json]: https://github.com/PasiSa/pasque/blob/main/src/bin/server-example.json -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct Config { cert_file: String, key_file: String, @@ -26,12 +26,24 @@ pub struct Config { endpoints: Vec, } +static DEFAULT_CONFIG: LazyLock = LazyLock::new(|| { + serde_json::from_slice(include_bytes!("../bin/server-example.json")) + .expect("example config must parse") +}); + +impl Default for Config { + fn default() -> Self { + DEFAULT_CONFIG.clone() + } +} + +/// This is used if jwt_secret is not present in the configuration. fn default_jwt_secret() -> String { "not-secret".to_string() } /// Common attributes to different endpoint types. -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct Common { /// Path to this endpoint. pub path: String, @@ -41,7 +53,7 @@ pub struct Common { pub permission: Option, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] #[serde(tag = "type")] enum Endpoint { IpEndpoint { @@ -89,16 +101,6 @@ impl Config { Ok(conf) } - /// Create a default configuration. Can be applied if configuration file cannot be read. - pub fn create_default() -> Config { - Config { - cert_file: "src/bin/cert.crt".to_string(), - key_file: "src/bin/cert.key".to_string(), - jwt_secret: "not-secret".to_string(), - endpoints: Vec::new(), - } - } - /// File path that contains the PEM formatted TLS certificate. pub fn cert_file(&self) -> &String { &self.cert_file @@ -114,6 +116,13 @@ impl Config { &self.jwt_secret } + /// Return a config without endpoints configured. Used in tests. + pub fn default_without_endpoints() -> Self { + let mut config = Self::default(); + config.endpoints.clear(); + config + } + /// Apply server endpoint settings from configuration. /// See [server-example.json] for an example configuration /// with endpoints. diff --git a/src/stream/iptunnel.rs b/src/stream/iptunnel.rs index bf514b2..72259cb 100644 --- a/src/stream/iptunnel.rs +++ b/src/stream/iptunnel.rs @@ -904,7 +904,8 @@ mod tests { let (tunnel, mut tester) = UnixStream::pair().unwrap(); let server = tokio::spawn(async move { - let config = Config::create_default(); + let config = Config::default_without_endpoints(); + let mut psqserver = PsqServer::start(&vec![SocketAddr::from_str(addr).unwrap()], &config) .await diff --git a/tests/integration.rs b/tests/integration.rs index b5dcae4..ddc43d8 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -19,7 +19,7 @@ use pasque::{ async fn test_get_request() { init_logger(); let addr = "127.0.0.1:8888"; - let config = Config::create_default(); + let config = Config::default_without_endpoints(); let server = tokio::spawn(async move { let mut psqserver = PsqServer::start(&vec![SocketAddr::from_str(addr).unwrap()], &config) .await @@ -72,7 +72,7 @@ async fn test_get_request() { } async fn run_server(addr: &str, shutdown: Arc) { - let config = Config::create_default(); + let config = Config::default_without_endpoints(); let mut psqserver = PsqServer::start(&vec![SocketAddr::from_str(addr).unwrap()], &config) .await .unwrap(); @@ -186,7 +186,7 @@ async fn tunnel_closing() { init_logger(); let addr = "127.0.0.1:9003"; let server = tokio::spawn(async move { - let config = Config::create_default(); + let config = Config::default_without_endpoints(); let mut psqserver = PsqServer::start(&vec![SocketAddr::from_str(addr).unwrap()], &config) .await .unwrap(); From 2e50af872bf9ccaef41328b6b606f73013ce374e Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Mon, 3 Nov 2025 16:52:51 +0200 Subject: [PATCH 3/4] README: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa0f739..2134727 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ fields defining the server operation: - **key_file**: path to the private key needed with the certificate. - **jwt_secret**: Secret that is used to decode the JWT tokens. This should - actually be sufficiently long randomly generated string. + actually be a sufficiently long randomly generated string. After the global parameters, there are configurations for different endpoints that the server operates: IP tunnel endpoint (type: `IpEndpoint`), UDP proxy From 051c20b5a1146c9dc6eb9c80932ea8157dbb0216 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Mon, 3 Nov 2025 16:57:24 +0200 Subject: [PATCH 4/4] PsqServer: drop jwt_secret deserialization default Force the user to explicitly provide a jwt_secret in the configuration. It's better than silently falling back to "not-secret", in case there's a typo in their config key, for example. --- src/server/config.rs | 6 ------ tests/endpoints.json | 3 ++- tests/testconfig1.json | 3 ++- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/server/config.rs b/src/server/config.rs index 6584937..447d509 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -21,7 +21,6 @@ use super::PsqServer; pub struct Config { cert_file: String, key_file: String, - #[serde(default = "default_jwt_secret")] jwt_secret: String, endpoints: Vec, } @@ -37,11 +36,6 @@ impl Default for Config { } } -/// This is used if jwt_secret is not present in the configuration. -fn default_jwt_secret() -> String { - "not-secret".to_string() -} - /// Common attributes to different endpoint types. #[derive(Clone, Debug, Deserialize)] pub struct Common { diff --git a/tests/endpoints.json b/tests/endpoints.json index 4451f5f..2964a13 100644 --- a/tests/endpoints.json +++ b/tests/endpoints.json @@ -18,5 +18,6 @@ "type": "Files", "root": "." } - ] + ], + "jwt_secret": "not-secret" } diff --git a/tests/testconfig1.json b/tests/testconfig1.json index 6fe49e8..60f467f 100644 --- a/tests/testconfig1.json +++ b/tests/testconfig1.json @@ -1,5 +1,6 @@ { "cert_file": "src/bin/cert.crt", "key_file": "src/bin/cert.keys", - "endpoints": [ ] + "endpoints": [ ], + "jwt_secret": "not-secret" }