better handling of postgres options

This commit is contained in:
Robin Appelman 2026-03-02 22:14:44 +01:00
commit a8f4622c1a
5 changed files with 86 additions and 37 deletions

17
Cargo.lock generated
View file

@ -461,13 +461,19 @@ dependencies = [
"foldhash", "foldhash",
] ]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]] [[package]]
name = "hashlink" name = "hashlink"
version = "0.10.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [ dependencies = [
"hashbrown", "hashbrown 0.15.2",
] ]
[[package]] [[package]]
@ -650,12 +656,12 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.9.0" version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown 0.16.1",
] ]
[[package]] [[package]]
@ -839,6 +845,7 @@ name = "nextcloud-config-parser"
version = "0.14.1" version = "0.14.1"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"indexmap",
"itertools", "itertools",
"miette", "miette",
"php-literal-parser", "php-literal-parser",
@ -1393,7 +1400,7 @@ dependencies = [
"futures-intrusive", "futures-intrusive",
"futures-io", "futures-io",
"futures-util", "futures-util",
"hashbrown", "hashbrown 0.15.2",
"hashlink", "hashlink",
"indexmap", "indexmap",
"log", "log",

View file

@ -16,6 +16,7 @@ miette = "7.4.0"
urlencoding = "2.1.3" urlencoding = "2.1.3"
form_urlencoded = "1.2.1" form_urlencoded = "1.2.1"
itertools = "0.14.0" itertools = "0.14.0"
indexmap = "2.13.0"
[dev-dependencies] [dev-dependencies]
miette = { version = "7.4.0", features = ["fancy"] } miette = { version = "7.4.0", features = ["fancy"] }

View file

@ -1,6 +1,7 @@
mod nc; mod nc;
use form_urlencoded::Serializer; use form_urlencoded::Serializer;
use indexmap::IndexMap;
use itertools::Either; use itertools::Either;
use miette::Diagnostic; use miette::Diagnostic;
use std::iter::once; use std::iter::once;
@ -192,7 +193,7 @@ pub enum NotAConfigError {
NotAnArray(PathBuf), NotAnArray(PathBuf),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub enum SslOptions { pub enum SslOptions {
Enabled { Enabled {
key: String, key: String,
@ -204,7 +205,7 @@ pub enum SslOptions {
Default, Default,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub enum Database { pub enum Database {
Sqlite { Sqlite {
database: PathBuf, database: PathBuf,
@ -221,11 +222,11 @@ pub enum Database {
username: String, username: String,
password: String, password: String,
connect: DbConnect, connect: DbConnect,
ssl_options: SslOptions, options: IndexMap<String, String>,
}, },
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub enum DbConnect { pub enum DbConnect {
Tcp { host: String, port: u16 }, Tcp { host: String, port: u16 },
Socket(PathBuf), Socket(PathBuf),
@ -300,21 +301,11 @@ impl Database {
username, username,
password, password,
connect, connect,
ssl_options, options,
} => { } => {
let mut params = Serializer::new(String::new()); let mut params = Serializer::new(String::new());
match ssl_options { for (key, value) in options {
SslOptions::Default => {} params.append_pair(key.as_str(), value.as_str());
SslOptions::Disabled => {
params.append_pair("sslmode", "disable");
}
SslOptions::Enabled { ca, verify, .. } => {
params.append_pair(
"ssl-mode",
if *verify { "verify-full" } else { "verify-ca" },
);
params.append_pair("sslrootcert", ca.as_str());
}
} }
let (host, port) = match connect { let (host, port) = match connect {
DbConnect::Socket(socket) => { DbConnect::Socket(socket) => {

View file

@ -3,6 +3,7 @@ use crate::{
RedisClusterConnectionInfo, RedisConnectionInfo, RedisTlsParams, Result, SslOptions, RedisClusterConnectionInfo, RedisConnectionInfo, RedisTlsParams, Result, SslOptions,
}; };
use crate::{RedisConfig, RedisConnectionAddr}; use crate::{RedisConfig, RedisConnectionAddr};
use indexmap::IndexMap;
use php_literal_parser::Value; use php_literal_parser::Value;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::DirEntry; use std::fs::DirEntry;
@ -215,8 +216,10 @@ fn parse_db_options(parsed: &Value) -> Result<Database> {
Some("pgsql") => { Some("pgsql") => {
let username = parsed["dbuser"].as_str().ok_or(DbError::NoUsername)?; let username = parsed["dbuser"].as_str().ok_or(DbError::NoUsername)?;
let password = parsed["dbpassword"].as_str().unwrap_or_default(); let password = parsed["dbpassword"].as_str().unwrap_or_default();
let db_host = parsed["dbhost"].as_str().unwrap_or_default();
let mut host_parts = db_host.split(';');
let (mut connect, disable_ssl) = let (mut connect, disable_ssl) =
match split_host(parsed["dbhost"].as_str().unwrap_or_default()) { match split_host(host_parts.next().expect("empty split")) {
(addr, None, None) => ( (addr, None, None) => (
DbConnect::Tcp { DbConnect::Tcp {
host: addr.into(), host: addr.into(),
@ -248,6 +251,14 @@ fn parse_db_options(parsed: &Value) -> Result<Database> {
unreachable!() unreachable!()
} }
}; };
let mut options = IndexMap::new();
for part in host_parts {
if let Some((key, value)) = part.split_once('=') {
options.insert(key.into(), value.into());
}
}
if let Some(port) = parsed["dbport"].clone().into_int() { if let Some(port) = parsed["dbport"].clone().into_int() {
if let DbConnect::Tcp { if let DbConnect::Tcp {
port: connect_port, .. port: connect_port, ..
@ -256,20 +267,21 @@ fn parse_db_options(parsed: &Value) -> Result<Database> {
*connect_port = port as u16; *connect_port = port as u16;
} }
} }
let database = parsed["dbname"].as_str().unwrap_or("owncloud"); if disable_ssl {
options.insert("sslmode".into(), "disable".into());
}
let ssl_options = if disable_ssl { let database = parsed["dbname"]
SslOptions::Disabled .as_str()
} else { .or_else(|| options.get("dbname").map(String::as_str))
SslOptions::Default .unwrap_or("owncloud");
};
Ok(Database::Postgres { Ok(Database::Postgres {
database: database.into(), database: database.into(),
username: username.into(), username: username.into(),
password: password.into(), password: password.into(),
connect, connect,
ssl_options, options,
}) })
} }
Some("sqlite3") | Some("sqlite") | None => { Some("sqlite3") | Some("sqlite") | None => {
@ -377,3 +389,36 @@ fn test_redis_empty_password_none() {
let redis = parse_redis_options(&config, "redis"); let redis = parse_redis_options(&config, "redis");
assert_eq!(redis.passwd(), None); assert_eq!(redis.passwd(), None);
} }
#[test]
fn test_postgres_options() {
use indexmap::indexmap;
let config =
php_literal_parser::from_str(r#"[
'dbtype' => 'pgsql',
'dbhost' => 'db.example.org;sslmode=verify-ca;sslrootcert=/etc/ssl/certs/ca-certificates.crt;dbname=nextcloud',
'dbuser' => 'nextcloud',
'dbpassword' => 'nextcloud',
]"#)
.unwrap();
let db = parse_db_options(&config).unwrap();
assert_eq!(
db,
Database::Postgres {
database: "nextcloud".to_string(),
username: "nextcloud".to_string(),
password: "nextcloud".to_string(),
connect: DbConnect::Tcp {
host: "db.example.org".into(),
port: 5432,
},
options: indexmap! {
"sslmode".into() => "verify-ca".into(),
"sslrootcert".into() => "/etc/ssl/certs/ca-certificates.crt".into(),
"dbname".into() => "nextcloud".into(),
},
}
);
assert_eq!(db.url(), "postgresql://nextcloud:nextcloud@db.example.org/nextcloud?sslmode=verify-ca&sslrootcert=/etc/ssl/certs/ca-certificates.crt&dbname=nextcloud");
}

View file

@ -4,6 +4,7 @@ use nextcloud_config_parser::{
}; };
use std::fmt::Debug; use std::fmt::Debug;
use indexmap::{indexmap, IndexMap};
use redis::{ConnectionAddr, ConnectionInfo}; use redis::{ConnectionAddr, ConnectionInfo};
use sqlx::mysql::{MySqlConnectOptions, MySqlSslMode}; use sqlx::mysql::{MySqlConnectOptions, MySqlSslMode};
use sqlx::sqlite::SqliteConnectOptions; use sqlx::sqlite::SqliteConnectOptions;
@ -249,7 +250,7 @@ fn test_parse_postgres_socket() {
username: "redacted".to_string(), username: "redacted".to_string(),
password: "redacted".to_string(), password: "redacted".to_string(),
connect: DbConnect::Socket("/var/run/postgresql".into()), connect: DbConnect::Socket("/var/run/postgresql".into()),
ssl_options: SslOptions::Default, options: IndexMap::default(),
}, },
&config.database, &config.database,
); );
@ -279,7 +280,7 @@ fn test_parse_postgres_socket_empty_hostname() {
username: "nextcloud".to_string(), username: "nextcloud".to_string(),
password: "redacted".to_string(), password: "redacted".to_string(),
connect: DbConnect::Socket("/run/postgresql".into()), connect: DbConnect::Socket("/run/postgresql".into()),
ssl_options: SslOptions::Default, options: IndexMap::default(),
}, },
&config.database, &config.database,
); );
@ -309,7 +310,7 @@ fn test_parse_postgres_socket_no_pass() {
username: "redacted".to_string(), username: "redacted".to_string(),
password: "".to_string(), password: "".to_string(),
connect: DbConnect::Socket("/var/run/postgresql".into()), connect: DbConnect::Socket("/var/run/postgresql".into()),
ssl_options: SslOptions::Default, options: IndexMap::default(),
}, },
&config.database, &config.database,
); );
@ -337,7 +338,7 @@ fn test_parse_postgres_socket_folder() {
username: "redacted".to_string(), username: "redacted".to_string(),
password: "redacted".to_string(), password: "redacted".to_string(),
connect: DbConnect::Socket("/var/run/postgresql".into()), connect: DbConnect::Socket("/var/run/postgresql".into()),
ssl_options: SslOptions::Default, options: IndexMap::default(),
}, },
&config.database, &config.database,
); );
@ -590,7 +591,9 @@ fn test_parse_postgres_ip() {
host: "1.2.3.4".to_string(), host: "1.2.3.4".to_string(),
port: 5432, port: 5432,
}, },
ssl_options: SslOptions::Disabled, options: indexmap! {
"sslmode".into() => "disable".into()
},
}, },
&config.database, &config.database,
); );
@ -623,7 +626,7 @@ fn test_parse_postgres_fqdn() {
host: "pg.example.com".to_string(), host: "pg.example.com".to_string(),
port: 5432, port: 5432,
}, },
ssl_options: SslOptions::Default, options: IndexMap::default(),
}, },
&config.database, &config.database,
); );
@ -691,7 +694,9 @@ fn test_parse_postgres_escaped_credentials() {
host: "1.2.3.4".to_string(), host: "1.2.3.4".to_string(),
port: 5432, port: 5432,
}, },
ssl_options: SslOptions::Disabled, options: indexmap! {
"sslmode".into() => "disable".into()
},
}, },
&config.database, &config.database,
); );