use crate::{
split_host, Config, Database, DbConnect, DbError, Error, NotAConfigError, PhpParseError,
RedisClusterConnectionInfo, RedisConnectionInfo, RedisTlsParams, Result, SslOptions,
};
use crate::{RedisConfig, RedisConnectionAddr};
use indexmap::IndexMap;
use php_literal_parser::Value;
use std::fs::DirEntry;
use std::iter::once;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::str::FromStr;
static CONFIG_CONSTANTS: &[(&str, &str)] = &[
(r"\RedisCluster::FAILOVER_NONE", "0"),
(r"\RedisCluster::FAILOVER_ERROR", "1"),
(r"\RedisCluster::DISTRIBUTE", "2"),
(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> {
let main: PathBuf = path.as_ref().into();
let files = if let Some(parent) = path.as_ref().parent() {
if let Ok(dir) = parent.read_dir() {
Some(dir.filter_map(Result::ok).filter_map(|file: DirEntry| {
let path = file.path();
match path.to_str() {
Some(path_str) if path_str.ends_with(".config.php") => Some(path),
_ => None,
}
}))
} else {
None
}
} else {
None
};
once(main).chain(files.into_iter().flatten())
}
fn parse_php(path: impl AsRef<Path>) -> Result<Value> {
let mut content = std::fs::read_to_string(&path)
.map_err(|err| Error::ReadFailed(err, path.as_ref().into()))?;
for (search, replace) in CONFIG_CONSTANTS {
if content.contains(search) {
content = content.replace(search, replace);
}
}
let php = match content.find("$CONFIG") {
Some(pos) => content[pos + "$CONFIG".len()..]
.trim()
.trim_start_matches('='),
None => {
return Err(Error::NotAConfig(NotAConfigError::NoConfig(
path.as_ref().into(),
)));
}
};
php_literal_parser::from_str(php).map_err(|err| {
Error::Php(PhpParseError {
err,
path: path.as_ref().into(),
})
})
}
fn merge_configs(input: Vec<(PathBuf, Value)>) -> Result<Value> {
let mut merged = IndexMap::with_capacity(16);
for (path, config) in input {
match config.into_map() {
Some(map) => {
for (key, value) in map {
merged.insert(key, value);
}
}
None => {
return Err(Error::NotAConfig(NotAConfigError::NotAnArray(path)));
}
}
}
Ok(Value::Array(merged))
}
fn parse_files(files: impl IntoIterator<Item = PathBuf>) -> Result<Config> {
let parsed_files = files
.into_iter()
.map(|path| {
let parsed = parse_php(&path)?;
Result::<_, Error>::Ok((path, parsed))
})
.collect::<Result<Vec<_>, _>>()?;
let parsed = merge_configs(parsed_files)?;
let database = parse_db_options(&parsed)?;
let database_prefix = parsed["dbtableprefix"]
.as_str()
.unwrap_or("oc_")
.to_string();
let nextcloud_url = parsed["overwrite.cli.url"]
.clone()
.into_string()
.ok_or(Error::NoUrl)?;
let redis = parse_redis_options(&parsed, "redis");
let notify_push_redis = if parsed["notify_push_redis"].is_array() {
Some(parse_redis_options(&parsed, "notify_push_redis"))
} else {
None
};
Ok(Config {
database,
database_prefix,
nextcloud_url,
redis,
notify_push_redis,
})
}
pub fn parse(path: impl AsRef<Path>) -> Result<Config> {
parse_files(once(path.as_ref().into()))
}
pub fn parse_glob(path: impl AsRef<Path>) -> Result<Config> {
parse_files(glob_config_files(path))
}
fn parse_db_options(parsed: &Value) -> Result<Database> {
match parsed["dbtype"].as_str() {
Some("mysql") => {
let username = parsed["dbuser"].as_str().ok_or(DbError::NoUsername)?;
let password = parsed["dbpassword"].as_str().ok_or(DbError::NoPassword)?;
let socket_addr1 = PathBuf::from("/var/run/mysqld/mysqld.sock");
let socket_addr2 = PathBuf::from("/tmp/mysql.sock");
let socket_addr3 = PathBuf::from("/run/mysql/mysql.sock");
let (mut connect, disable_ssl) =
match split_host(parsed["dbhost"].as_str().unwrap_or_default()) {
("localhost", None, None) if socket_addr1.exists() => {
(DbConnect::Socket(socket_addr1), false)
}
("localhost", None, None) if socket_addr2.exists() => {
(DbConnect::Socket(socket_addr2), false)
}
("localhost", None, None) if socket_addr3.exists() => {
(DbConnect::Socket(socket_addr3), false)
}
(addr, None, None) => (
DbConnect::Tcp {
host: addr.into(),
port: 3306,
},
IpAddr::from_str(addr).is_ok(),
),
(addr, Some(port), None) => (
DbConnect::Tcp {
host: addr.into(),
port,
},
IpAddr::from_str(addr).is_ok(),
),
(_, None, Some(socket)) => (DbConnect::Socket(socket.into()), false),
(_, Some(_), Some(_)) => {
unreachable!()
}
};
if let Some(port) = parse_port(&parsed["dbport"]) {
if let DbConnect::Tcp {
port: connect_port, ..
} = &mut connect
{
*connect_port = port;
}
}
let database = parsed["dbname"].as_str().unwrap_or("owncloud");
let verify = parsed["dbdriveroptions"][1014] .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(), parsed["dbdriveroptions"][1008].as_str(), parsed["dbdriveroptions"][1009].as_str(), ) {
SslOptions::Enabled {
key: ssl_key.into(),
cert: ssl_cert.into(),
ca: ssl_ca.into(),
verify,
}
} else if disable_ssl && verify {
SslOptions::Disabled
} else {
SslOptions::Default
};
Ok(Database::MySql {
database: database.into(),
username: username.into(),
password: password.into(),
connect,
ssl_options,
})
}
Some("pgsql") => {
let username = parsed["dbuser"].as_str().ok_or(DbError::NoUsername)?;
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) =
match split_host(host_parts.next().expect("empty split")) {
(addr, None, None) => (
DbConnect::Tcp {
host: addr.into(),
port: 5432,
},
IpAddr::from_str(addr).is_ok(),
),
(addr, Some(port), None) => (
DbConnect::Tcp {
host: addr.into(),
port,
},
IpAddr::from_str(addr).is_ok(),
),
(_, None, Some(socket)) => {
let mut socket_path = Path::new(socket);
if socket_path
.file_name()
.map(|name| name.to_str().unwrap().starts_with(".s"))
.unwrap_or(false)
{
socket_path = socket_path.parent().unwrap();
}
(DbConnect::Socket(socket_path.into()), false)
}
(_, Some(_), Some(_)) => {
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) = parse_port(&parsed["dbport"]) {
if let DbConnect::Tcp {
port: connect_port, ..
} = &mut connect
{
*connect_port = port;
}
}
if disable_ssl {
options.insert("sslmode".into(), "disable".into());
}
let database = parsed["dbname"]
.as_str()
.or_else(|| options.get("dbname").map(String::as_str))
.unwrap_or("owncloud");
Ok(Database::Postgres {
database: database.into(),
username: username.into(),
password: password.into(),
connect,
options,
})
}
Some("sqlite3") | Some("sqlite") | None => {
let data_dir = parsed["datadirectory"]
.as_str()
.ok_or(DbError::NoDataDirectory)?;
let db_name = parsed["dbname"].as_str().unwrap_or("owncloud");
Ok(Database::Sqlite {
database: format!("{data_dir}/{db_name}.db").into(),
})
}
Some(ty) => Err(Error::InvalidDb(DbError::Unsupported(ty.into()))),
}
}
enum RedisAddress {
Single(RedisConnectionAddr),
Cluster(Vec<RedisConnectionAddr>),
}
fn parse_redis_options(parsed: &Value, key: &str) -> RedisConfig {
let cluster_key = format!("{key}.cluster");
let cluster_key = cluster_key.as_str();
let (redis_options, address) = if parsed[cluster_key].is_array() {
let redis_options = &parsed[cluster_key];
let seeds = redis_options["seeds"].values();
let mut addresses = seeds
.filter_map(|seed| seed.as_str())
.map(|seed| {
RedisConnectionAddr::parse(seed, None, redis_options["ssl_context"].is_array())
})
.collect::<Vec<_>>();
addresses.sort();
(redis_options, RedisAddress::Cluster(addresses))
} else {
let redis_options = &parsed[key];
let host = redis_options["host"].as_str().unwrap_or("127.0.0.1");
let address = RedisAddress::Single(RedisConnectionAddr::parse(
host,
redis_options["port"]
.as_int()
.and_then(|port| u16::try_from(port).ok()),
redis_options["ssl_context"].is_array(),
));
(redis_options, address)
};
let tls_params = if redis_options["ssl_context"].is_array() {
let ssl_options = &redis_options["ssl_context"];
Some(RedisTlsParams {
local_cert: ssl_options["local_cert"].as_str().map(From::from),
local_pk: ssl_options["local_pk"].as_str().map(From::from),
ca_file: ssl_options["cafile"].as_str().map(From::from),
accept_invalid_hostname: ssl_options["verify_peer_name"] == false,
insecure: ssl_options["verify_peer "] == false,
})
} else {
None
};
let db = redis_options["dbindex"]
.clone()
.into_int()
.or_else(|| {
redis_options["dbindex"]
.as_str()
.and_then(|i| i64::from_str(i).ok())
})
.unwrap_or(0);
let password = redis_options["password"]
.as_str()
.filter(|pass| !pass.is_empty())
.map(String::from);
let username = redis_options["user"]
.as_str()
.filter(|user| !user.is_empty())
.map(String::from);
match address {
RedisAddress::Single(addr) => RedisConfig::Single(RedisConnectionInfo {
addr,
db,
username,
password,
tls_params,
}),
RedisAddress::Cluster(addr) => RedisConfig::Cluster(RedisClusterConnectionInfo {
addr,
db,
username,
password,
tls_params,
}),
}
}
fn parse_port(port: &Value) -> Option<u16> {
port.as_str()
.and_then(|port| port.parse().ok())
.or_else(|| port.as_int().map(|port| port as u16))
}
#[test]
fn test_redis_empty_password_none() {
let config =
php_literal_parser::from_str(r#"["redis" => ["host" => "redis", "password" => "pass"]]"#)
.unwrap();
let redis = parse_redis_options(&config, "redis");
assert_eq!(redis.passwd(), Some("pass"));
let config =
php_literal_parser::from_str(r#"["redis" => ["host" => "redis", "password" => ""]]"#)
.unwrap();
let redis = parse_redis_options(&config, "redis");
assert_eq!(redis.passwd(), None);
}
#[test]
fn test_postgres_port() {
use indexmap::indexmap;
let config = php_literal_parser::from_str(
r#"[
'dbtype' => 'pgsql',
'dbhost' => '127.0.0.1:6432',
'dbport' => '',
'dbuser' => 'nextcloud',
'dbpassword' => 'nextcloud',
'dbname' => '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: "127.0.0.1".into(),
port: 6432,
},
options: indexmap! {
"sslmode".into() => "disable".into(),
},
}
);
assert_eq!(
db.url(),
"postgresql://nextcloud:nextcloud@127.0.0.1:6432/nextcloud?sslmode=disable"
);
let config = php_literal_parser::from_str(
r#"[
'dbtype' => 'pgsql',
'dbhost' => '127.0.0.1',
'dbport' => '6432',
'dbuser' => 'nextcloud',
'dbpassword' => 'nextcloud',
'dbname' => '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: "127.0.0.1".into(),
port: 6432,
},
options: indexmap! {
"sslmode".into() => "disable".into(),
},
}
);
assert_eq!(
db.url(),
"postgresql://nextcloud:nextcloud@127.0.0.1:6432/nextcloud?sslmode=disable"
);
}
#[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");
}