improve handling of mysql ssl options

This commit is contained in:
Robin Appelman 2021-08-03 17:23:44 +02:00
commit 4b8c9530fb
6 changed files with 226 additions and 20 deletions

View file

@ -1,7 +1,7 @@
[package] [package]
name = "nextcloud-config-parser" name = "nextcloud-config-parser"
description = "Rust parser for nextcloud config files" description = "Rust parser for nextcloud config files"
version = "0.3.0" version = "0.4.0"
authors = ["Robin Appelman <robin@icewind.nl>"] authors = ["Robin Appelman <robin@icewind.nl>"]
edition = "2018" edition = "2018"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View file

@ -127,6 +127,18 @@ pub enum NotAConfigError {
NotAnArray(PathBuf), NotAnArray(PathBuf),
} }
#[derive(Debug)]
pub enum SslOptions {
Enabled {
key: String,
cert: String,
ca: String,
verify: bool,
},
Disabled,
Default,
}
#[derive(Debug)] #[derive(Debug)]
pub enum Database { pub enum Database {
Sqlite { Sqlite {
@ -137,14 +149,14 @@ pub enum Database {
username: String, username: String,
password: String, password: String,
connect: DbConnect, connect: DbConnect,
disable_ssl: bool, ssl_options: SslOptions,
}, },
Postgres { Postgres {
database: String, database: String,
username: String, username: String,
password: String, password: String,
connect: DbConnect, connect: DbConnect,
disable_ssl: bool, ssl_options: SslOptions,
}, },
} }
@ -172,14 +184,25 @@ impl From<Database> for sqlx::any::AnyConnectOptions {
username, username,
password, password,
connect, connect,
disable_ssl, ssl_options,
} => { } => {
let mut options = MySqlConnectOptions::default() let mut options = MySqlConnectOptions::default()
.database(&database) .database(&database)
.username(&username) .username(&username)
.password(&password); .password(&password);
if disable_ssl { match ssl_options {
options = options.ssl_mode(MySqlSslMode::Disabled); SslOptions::Enabled { ca, verify, .. } => {
options = options.ssl_ca(ca);
options = options.ssl_mode(if verify {
MySqlSslMode::VerifyIdentity
} else {
MySqlSslMode::VerifyCa
});
}
SslOptions::Disabled => {
options = options.ssl_mode(MySqlSslMode::Disabled);
}
SslOptions::Default => {}
} }
match connect { match connect {
DbConnect::Socket(socket) => { DbConnect::Socket(socket) => {
@ -196,13 +219,13 @@ impl From<Database> for sqlx::any::AnyConnectOptions {
username, username,
password, password,
connect, connect,
disable_ssl, ssl_options,
} => { } => {
let mut options = PgConnectOptions::default() let mut options = PgConnectOptions::default()
.database(&database) .database(&database)
.username(&username) .username(&username)
.password(&password); .password(&password);
if disable_ssl { if matches!(ssl_options, SslOptions::Disabled) {
options = options.ssl_mode(PgSslMode::Disable); options = options.ssl_mode(PgSslMode::Disable);
} }
match connect { match connect {

147
src/nc.rs
View file

@ -1,6 +1,8 @@
#[cfg(feature = "redis-connect")] #[cfg(feature = "redis-connect")]
use crate::RedisConfig; use crate::RedisConfig;
use crate::{Config, Database, DbConnect, DbError, Error, NotAConfigError, PhpParseError, Result}; use crate::{
Config, Database, DbConnect, DbError, Error, NotAConfigError, PhpParseError, Result, SslOptions,
};
use php_literal_parser::Value; use php_literal_parser::Value;
#[cfg(feature = "redis-connect")] #[cfg(feature = "redis-connect")]
use redis::{ConnectionAddr, ConnectionInfo, RedisConnectionInfo}; use redis::{ConnectionAddr, ConnectionInfo, RedisConnectionInfo};
@ -16,6 +18,10 @@ static CONFIG_CONSTANTS: &[(&str, &str)] = &[
(r"\RedisCluster::FAILOVER_ERROR", "1"), (r"\RedisCluster::FAILOVER_ERROR", "1"),
(r"\RedisCluster::DISTRIBUTE", "2"), (r"\RedisCluster::DISTRIBUTE", "2"),
(r"\RedisCluster::FAILOVER_DISTRIBUTE_SLAVES", "3"), (r"\RedisCluster::FAILOVER_DISTRIBUTE_SLAVES", "3"),
(r"\PDO::MYSQL_ATTR_SSL_KEY", "1007"),
(r"\PDO::MYSQL_ATTR_SSL_CERT", "1008"),
(r"\PDO::MYSQL_ATTR_SSL_CA", "1009"),
(r"\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT", "1014"),
]; ];
fn glob_config_files(path: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> { fn glob_config_files(path: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
@ -174,12 +180,35 @@ fn parse_db_options(parsed: &Value) -> Result<Database> {
} }
let database = parsed["dbname"].as_str().ok_or(DbError::NoName)?; let database = parsed["dbname"].as_str().ok_or(DbError::NoName)?;
let verify = parsed["dbdriveroptions"][1014] // MYSQL_ATTR_SSL_VERIFY_SERVER_CERT
.clone()
.into_bool()
.unwrap_or(true);
let ssl_options = if let (Some(ssl_key), Some(ssl_cert), Some(ssl_ca)) = (
parsed["dbdriveroptions"][1007].as_str(), // MYSQL_ATTR_SSL_KEY
parsed["dbdriveroptions"][1008].as_str(), // MYSQL_ATTR_SSL_CERT
parsed["dbdriveroptions"][1009].as_str(), // MYSQL_ATTR_SSL_CA
) {
SslOptions::Enabled {
key: ssl_key.into(),
cert: ssl_cert.into(),
ca: ssl_ca.into(),
verify,
}
// if MYSQL_ATTR_SSL_VERIFY_SERVER_CERT is disabled, we should be able to use ssl even with raw ip
} else if disable_ssl && verify {
SslOptions::Disabled
} else {
SslOptions::Default
};
Ok(Database::MySql { Ok(Database::MySql {
database: database.into(), database: database.into(),
username: username.into(), username: username.into(),
password: password.into(), password: password.into(),
connect, connect,
disable_ssl, ssl_options,
}) })
} }
Some("pgsql") => { Some("pgsql") => {
@ -228,12 +257,18 @@ fn parse_db_options(parsed: &Value) -> Result<Database> {
} }
let database = parsed["dbname"].as_str().ok_or(DbError::NoName)?; let database = parsed["dbname"].as_str().ok_or(DbError::NoName)?;
let ssl_options = if disable_ssl {
SslOptions::Disabled
} else {
SslOptions::Default
};
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,
disable_ssl, ssl_options,
}) })
} }
Some("sqlite3") => { Some("sqlite3") => {
@ -375,7 +410,7 @@ fn test_parse_config_basic() {
host: "127.0.0.1".to_string(), host: "127.0.0.1".to_string(),
port: 3306, port: 3306,
}, },
disable_ssl: true, ssl_options: SslOptions::Disabled,
}, },
&config.database, &config.database,
); );
@ -444,7 +479,7 @@ fn test_parse_comment_whitespace() {
host: "127.0.0.1".to_string(), host: "127.0.0.1".to_string(),
port: 3306, port: 3306,
}, },
disable_ssl: true, ssl_options: SslOptions::Disabled,
}, },
&config.database, &config.database,
); );
@ -475,7 +510,7 @@ fn test_parse_port_in_host() {
host: "127.0.0.1".to_string(), host: "127.0.0.1".to_string(),
port: 1234, port: 1234,
}, },
disable_ssl: true, ssl_options: SslOptions::Disabled,
}, },
&config.database, &config.database,
); );
@ -498,7 +533,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()),
disable_ssl: false, ssl_options: SslOptions::Default,
}, },
&config.database, &config.database,
); );
@ -524,7 +559,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()),
disable_ssl: false, ssl_options: SslOptions::Default,
}, },
&config.database, &config.database,
); );
@ -574,7 +609,7 @@ fn test_parse_config_multiple() {
host: "127.0.0.1".to_string(), host: "127.0.0.1".to_string(),
port: 3306, port: 3306,
}, },
disable_ssl: true, ssl_options: SslOptions::Disabled,
}, },
&config.database, &config.database,
); );
@ -628,7 +663,7 @@ fn test_parse_config_mysql_fqdn() {
host: "db.example.com".to_string(), host: "db.example.com".to_string(),
port: 3306, port: 3306,
}, },
disable_ssl: false, ssl_options: SslOptions::Default,
}, },
&config.database, &config.database,
); );
@ -642,6 +677,94 @@ fn test_parse_config_mysql_fqdn() {
); );
} }
#[test]
fn test_parse_config_mysql_ip_no_verify() {
let config = config_from_file("tests/configs/mysql_ip_no_verify.php");
assert_debug_equal(
&Database::MySql {
database: "nextcloud".to_string(),
username: "nextcloud".to_string(),
password: "secret".to_string(),
connect: DbConnect::Tcp {
host: "1.2.3.4".to_string(),
port: 3306,
},
ssl_options: SslOptions::Default,
},
&config.database,
);
#[cfg(feature = "db-sqlx")]
assert_debug_equal(
AnyConnectOptions::from_str(
"mysql://nextcloud:secret@1.2.3.4/nextcloud?ssl-mode=preferred",
)
.unwrap(),
config.database.into(),
);
}
#[test]
fn test_parse_config_mysql_ssl_ca() {
let config = config_from_file("tests/configs/mysql_ssl_ca.php");
assert_debug_equal(
&Database::MySql {
database: "nextcloud".to_string(),
username: "nextcloud".to_string(),
password: "secret".to_string(),
connect: DbConnect::Tcp {
host: "db.example.com".to_string(),
port: 3306,
},
ssl_options: SslOptions::Enabled {
key: "/ssl-key.pem".into(),
cert: "/ssl-cert.pem".into(),
ca: "/ca-cert.pem".into(),
verify: true,
},
},
&config.database,
);
#[cfg(feature = "db-sqlx")]
assert_debug_equal(
AnyConnectOptions::from_str(
"mysql://nextcloud:secret@db.example.com/nextcloud?ssl-mode=verify_identity&ssl-ca=/ca-cert.pem",
)
.unwrap(),
config.database.into(),
);
}
#[test]
fn test_parse_config_mysql_ssl_ca_no_verify() {
let config = config_from_file("tests/configs/mysql_ssl_ca_no_verify.php");
assert_debug_equal(
&Database::MySql {
database: "nextcloud".to_string(),
username: "nextcloud".to_string(),
password: "secret".to_string(),
connect: DbConnect::Tcp {
host: "db.example.com".to_string(),
port: 3306,
},
ssl_options: SslOptions::Enabled {
key: "/ssl-key.pem".into(),
cert: "/ssl-cert.pem".into(),
ca: "/ca-cert.pem".into(),
verify: false,
},
},
&config.database,
);
#[cfg(feature = "db-sqlx")]
assert_debug_equal(
AnyConnectOptions::from_str(
"mysql://nextcloud:secret@db.example.com/nextcloud?ssl-mode=verify_ca&ssl-ca=/ca-cert.pem",
)
.unwrap(),
config.database.into(),
);
}
#[test] #[test]
fn test_parse_postgres_ip() { fn test_parse_postgres_ip() {
let config = config_from_file("tests/configs/postgres_ip.php"); let config = config_from_file("tests/configs/postgres_ip.php");
@ -654,7 +777,7 @@ fn test_parse_postgres_ip() {
host: "1.2.3.4".to_string(), host: "1.2.3.4".to_string(),
port: 5432, port: 5432,
}, },
disable_ssl: true, ssl_options: SslOptions::Disabled,
}, },
&config.database, &config.database,
); );
@ -684,7 +807,7 @@ fn test_parse_postgres_fqdn() {
host: "pg.example.com".to_string(), host: "pg.example.com".to_string(),
port: 5432, port: 5432,
}, },
disable_ssl: false, ssl_options: SslOptions::Default,
}, },
&config.database, &config.database,
); );

View file

@ -0,0 +1,18 @@
<?php
$CONFIG = [
'overwrite.cli.url' => 'https://cloud.example.com',
'dbtype' => 'mysql',
'dbname' => 'nextcloud',
'dbhost' => '1.2.3.4',
'dbport' => '',
'dbtableprefix' => 'oc_',
'dbuser' => 'nextcloud',
'dbpassword' => 'secret',
'redis' => [
'host' => 'localhost'
],
'dbdriveroptions' => [
\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false,
],
];

View file

@ -0,0 +1,21 @@
<?php
$CONFIG = [
'overwrite.cli.url' => 'https://cloud.example.com',
'dbtype' => 'mysql',
'dbname' => 'nextcloud',
'dbhost' => 'db.example.com',
'dbport' => '',
'dbtableprefix' => 'oc_',
'dbuser' => 'nextcloud',
'dbpassword' => 'secret',
'redis' => [
'host' => 'localhost'
],
'dbdriveroptions' => [
\PDO::MYSQL_ATTR_SSL_KEY => '/ssl-key.pem',
\PDO::MYSQL_ATTR_SSL_CERT => '/ssl-cert.pem',
\PDO::MYSQL_ATTR_SSL_CA => '/ca-cert.pem',
\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => true,
],
];

View file

@ -0,0 +1,21 @@
<?php
$CONFIG = [
'overwrite.cli.url' => 'https://cloud.example.com',
'dbtype' => 'mysql',
'dbname' => 'nextcloud',
'dbhost' => 'db.example.com',
'dbport' => '',
'dbtableprefix' => 'oc_',
'dbuser' => 'nextcloud',
'dbpassword' => 'secret',
'redis' => [
'host' => 'localhost'
],
'dbdriveroptions' => [
\PDO::MYSQL_ATTR_SSL_KEY => '/ssl-key.pem',
\PDO::MYSQL_ATTR_SSL_CERT => '/ssl-cert.pem',
\PDO::MYSQL_ATTR_SSL_CA => '/ca-cert.pem',
\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false,
],
];