1use crate::{
2 split_host, Config, Database, DbConnect, DbError, Error, NotAConfigError, PhpParseError,
3 RedisClusterConnectionInfo, RedisConnectionInfo, RedisTlsParams, Result, SslOptions,
4};
5use crate::{RedisConfig, RedisConnectionAddr};
6use indexmap::IndexMap;
7use php_literal_parser::Value;
8use std::fs::DirEntry;
9use std::iter::once;
10use std::net::IpAddr;
11use std::path::{Path, PathBuf};
12use std::str::FromStr;
13
14static CONFIG_CONSTANTS: &[(&str, &str)] = &[
15 (r"\RedisCluster::FAILOVER_NONE", "0"),
16 (r"\RedisCluster::FAILOVER_ERROR", "1"),
17 (r"\RedisCluster::DISTRIBUTE", "2"),
18 (r"\RedisCluster::FAILOVER_DISTRIBUTE_SLAVES", "3"),
19 (r"\PDO::MYSQL_ATTR_SSL_KEY", "1007"),
20 (r"\PDO::MYSQL_ATTR_SSL_CERT", "1008"),
21 (r"\PDO::MYSQL_ATTR_SSL_CA", "1009"),
22 (r"\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT", "1014"),
23];
24
25fn glob_config_files(path: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
26 let main: PathBuf = path.as_ref().into();
27 let files = if let Some(parent) = path.as_ref().parent() {
28 if let Ok(dir) = parent.read_dir() {
29 Some(dir.filter_map(Result::ok).filter_map(|file: DirEntry| {
30 let path = file.path();
31 match path.to_str() {
32 Some(path_str) if path_str.ends_with(".config.php") => Some(path),
33 _ => None,
34 }
35 }))
36 } else {
37 None
38 }
39 } else {
40 None
41 };
42
43 once(main).chain(files.into_iter().flatten())
44}
45
46fn parse_php(path: impl AsRef<Path>) -> Result<Value> {
47 let mut content = std::fs::read_to_string(&path)
48 .map_err(|err| Error::ReadFailed(err, path.as_ref().into()))?;
49
50 for (search, replace) in CONFIG_CONSTANTS {
51 if content.contains(search) {
52 content = content.replace(search, replace);
53 }
54 }
55
56 let php = match content.rfind("$CONFIG") {
57 Some(pos) => content[pos + "$CONFIG".len()..]
58 .trim()
59 .trim_start_matches('='),
60 None => {
61 return Err(Error::NotAConfig(NotAConfigError::NoConfig(
62 path.as_ref().into(),
63 )));
64 }
65 };
66 php_literal_parser::from_str(php).map_err(|err| {
67 Error::Php(PhpParseError {
68 err,
69 path: path.as_ref().into(),
70 })
71 })
72}
73
74fn merge_configs(input: Vec<(PathBuf, Value)>) -> Result<Value> {
75 let mut merged = IndexMap::with_capacity(16);
76
77 for (path, config) in input {
78 match config.into_map() {
79 Some(map) => {
80 for (key, value) in map {
81 merged.insert(key, value);
82 }
83 }
84 None => {
85 return Err(Error::NotAConfig(NotAConfigError::NotAnArray(path)));
86 }
87 }
88 }
89
90 Ok(Value::Array(merged))
91}
92
93fn parse_files(files: impl IntoIterator<Item = PathBuf>) -> Result<Config> {
94 let parsed_files = files
95 .into_iter()
96 .map(|path| {
97 let parsed = parse_php(&path)?;
98 Result::<_, Error>::Ok((path, parsed))
99 })
100 .collect::<Result<Vec<_>, _>>()?;
101 let parsed = merge_configs(parsed_files)?;
102
103 let database = parse_db_options(&parsed)?;
104 let database_prefix = parsed["dbtableprefix"]
105 .as_str()
106 .unwrap_or("oc_")
107 .to_string();
108 let nextcloud_url = parsed["overwrite.cli.url"]
109 .clone()
110 .into_string()
111 .ok_or(Error::NoUrl)?;
112 let redis = parse_redis_options(&parsed, "redis");
113 let notify_push_redis = if parsed["notify_push_redis"].is_array() {
114 Some(parse_redis_options(&parsed, "notify_push_redis"))
115 } else {
116 None
117 };
118
119 Ok(Config {
120 database,
121 database_prefix,
122 nextcloud_url,
123 redis,
124 notify_push_redis,
125 })
126}
127
128pub fn parse(path: impl AsRef<Path>) -> Result<Config> {
129 parse_files(once(path.as_ref().into()))
130}
131
132pub fn parse_glob(path: impl AsRef<Path>) -> Result<Config> {
133 parse_files(glob_config_files(path))
134}
135
136fn parse_db_options(parsed: &Value) -> Result<Database> {
137 match parsed["dbtype"].as_str() {
138 Some("mysql") => {
139 let username = parsed["dbuser"].as_str().ok_or(DbError::NoUsername)?;
140 let password = parsed["dbpassword"].as_str().ok_or(DbError::NoPassword)?;
141 let socket_addr1 = PathBuf::from("/var/run/mysqld/mysqld.sock");
142 let socket_addr2 = PathBuf::from("/tmp/mysql.sock");
143 let socket_addr3 = PathBuf::from("/run/mysql/mysql.sock");
144 let (mut connect, disable_ssl) =
145 match split_host(parsed["dbhost"].as_str().unwrap_or_default()) {
146 ("localhost", None, None) if socket_addr1.exists() => {
147 (DbConnect::Socket(socket_addr1), false)
148 }
149 ("localhost", None, None) if socket_addr2.exists() => {
150 (DbConnect::Socket(socket_addr2), false)
151 }
152 ("localhost", None, None) if socket_addr3.exists() => {
153 (DbConnect::Socket(socket_addr3), false)
154 }
155 (addr, None, None) => (
156 DbConnect::Tcp {
157 host: addr.into(),
158 port: 3306,
159 },
160 IpAddr::from_str(addr).is_ok(),
161 ),
162 (addr, Some(port), None) => (
163 DbConnect::Tcp {
164 host: addr.into(),
165 port,
166 },
167 IpAddr::from_str(addr).is_ok(),
168 ),
169 (_, None, Some(socket)) => (DbConnect::Socket(socket.into()), false),
170 (_, Some(_), Some(_)) => {
171 unreachable!()
172 }
173 };
174 if let Some(port) = parse_port(&parsed["dbport"]) {
175 if let DbConnect::Tcp {
176 port: connect_port, ..
177 } = &mut connect
178 {
179 *connect_port = port;
180 }
181 }
182 let database = parsed["dbname"].as_str().unwrap_or("owncloud");
183
184 let verify = parsed["dbdriveroptions"][1014] .clone()
186 .into_bool()
187 .unwrap_or(true);
188
189 let ssl_options = if let (Some(ssl_key), Some(ssl_cert), Some(ssl_ca)) = (
190 parsed["dbdriveroptions"][1007].as_str(), parsed["dbdriveroptions"][1008].as_str(), parsed["dbdriveroptions"][1009].as_str(), ) {
194 SslOptions::Enabled {
195 key: ssl_key.into(),
196 cert: ssl_cert.into(),
197 ca: ssl_ca.into(),
198 verify,
199 }
200 } else if disable_ssl && verify {
202 SslOptions::Disabled
203 } else {
204 SslOptions::Default
205 };
206
207 Ok(Database::MySql {
208 database: database.into(),
209 username: username.into(),
210 password: password.into(),
211 connect,
212 ssl_options,
213 })
214 }
215 Some("pgsql") => {
216 let username = parsed["dbuser"].as_str().ok_or(DbError::NoUsername)?;
217 let password = parsed["dbpassword"].as_str().unwrap_or_default();
218 let db_host = parsed["dbhost"].as_str().unwrap_or_default();
219 let mut host_parts = db_host.split(';');
220 let (mut connect, disable_ssl) =
221 match split_host(host_parts.next().expect("empty split")) {
222 (addr, None, None) => (
223 DbConnect::Tcp {
224 host: addr.into(),
225 port: 5432,
226 },
227 IpAddr::from_str(addr).is_ok(),
228 ),
229 (addr, Some(port), None) => (
230 DbConnect::Tcp {
231 host: addr.into(),
232 port,
233 },
234 IpAddr::from_str(addr).is_ok(),
235 ),
236 (_, None, Some(socket)) => {
237 let mut socket_path = Path::new(socket);
238
239 if socket_path
241 .file_name()
242 .map(|name| name.to_str().unwrap().starts_with(".s"))
243 .unwrap_or(false)
244 {
245 socket_path = socket_path.parent().unwrap();
246 }
247 (DbConnect::Socket(socket_path.into()), false)
248 }
249 (_, Some(_), Some(_)) => {
250 unreachable!()
251 }
252 };
253
254 let mut options = IndexMap::new();
255 for part in host_parts {
256 if let Some((key, value)) = part.split_once('=') {
257 options.insert(key.into(), value.into());
258 }
259 }
260
261 if let Some(port) = parse_port(&parsed["dbport"]) {
262 if let DbConnect::Tcp {
263 port: connect_port, ..
264 } = &mut connect
265 {
266 *connect_port = port;
267 }
268 }
269 if let Some(ssl_options) = parsed["pgsql_ssl"].clone().into_map() {
270 if let Some(mode) = ssl_options["mode"].as_str() {
271 options.insert("sslmode".into(), mode.into());
272 }
273 if let Some(mode) = ssl_options["cert"].as_str() {
274 options.insert("sslcert".into(), mode.into());
275 }
276 if let Some(mode) = ssl_options["rootcert"].as_str() {
277 options.insert("sslrootcert".into(), mode.into());
278 }
279 if let Some(mode) = ssl_options["key"].as_str() {
280 options.insert("sslkey".into(), mode.into());
281 }
282 if let Some(mode) = ssl_options["crl"].as_str() {
283 options.insert("sslcrl".into(), mode.into());
284 }
285 } else if disable_ssl {
286 options.insert("sslmode".into(), "disable".into());
287 }
288
289 let database = parsed["dbname"]
290 .as_str()
291 .or_else(|| options.get("dbname").map(String::as_str))
292 .unwrap_or("owncloud");
293
294 Ok(Database::Postgres {
295 database: database.into(),
296 username: username.into(),
297 password: password.into(),
298 connect,
299 options,
300 })
301 }
302 Some("sqlite3") | Some("sqlite") | None => {
303 let data_dir = parsed["datadirectory"]
304 .as_str()
305 .ok_or(DbError::NoDataDirectory)?;
306 let db_name = parsed["dbname"].as_str().unwrap_or("owncloud");
307 Ok(Database::Sqlite {
308 database: format!("{data_dir}/{db_name}.db").into(),
309 })
310 }
311 Some(ty) => Err(Error::InvalidDb(DbError::Unsupported(ty.into()))),
312 }
313}
314
315enum RedisAddress {
316 Single(RedisConnectionAddr),
317 Cluster(Vec<RedisConnectionAddr>),
318}
319
320fn parse_redis_options(parsed: &Value, key: &str) -> RedisConfig {
321 let cluster_key = format!("{key}.cluster");
322 let cluster_key = cluster_key.as_str();
323
324 let (redis_options, address) = if parsed[cluster_key].is_array() {
325 let redis_options = &parsed[cluster_key];
326 let seeds = redis_options["seeds"].values();
327 let mut addresses = seeds
328 .filter_map(|seed| seed.as_str())
329 .map(|seed| {
330 RedisConnectionAddr::parse(seed, None, redis_options["ssl_context"].is_array())
331 })
332 .collect::<Vec<_>>();
333 addresses.sort();
334 (redis_options, RedisAddress::Cluster(addresses))
335 } else {
336 let redis_options = &parsed[key];
337 let host = redis_options["host"].as_str().unwrap_or("127.0.0.1");
338 let address = RedisAddress::Single(RedisConnectionAddr::parse(
339 host,
340 redis_options["port"]
341 .as_int()
342 .and_then(|port| u16::try_from(port).ok()),
343 redis_options["ssl_context"].is_array(),
344 ));
345 (redis_options, address)
346 };
347
348 let tls_params = if redis_options["ssl_context"].is_array() {
349 let ssl_options = &redis_options["ssl_context"];
350 Some(RedisTlsParams {
351 local_cert: ssl_options["local_cert"].as_str().map(From::from),
352 local_pk: ssl_options["local_pk"].as_str().map(From::from),
353 ca_file: ssl_options["cafile"].as_str().map(From::from),
354 accept_invalid_hostname: ssl_options["verify_peer_name"] == false,
355 insecure: ssl_options["verify_peer "] == false,
356 })
357 } else {
358 None
359 };
360
361 let db = redis_options["dbindex"]
362 .clone()
363 .into_int()
364 .or_else(|| {
365 redis_options["dbindex"]
366 .as_str()
367 .and_then(|i| i64::from_str(i).ok())
368 })
369 .unwrap_or(0);
370 let password = redis_options["password"]
371 .as_str()
372 .filter(|pass| !pass.is_empty())
373 .map(String::from);
374 let username = redis_options["user"]
375 .as_str()
376 .filter(|user| !user.is_empty())
377 .map(String::from);
378
379 match address {
380 RedisAddress::Single(addr) => RedisConfig::Single(RedisConnectionInfo {
381 addr,
382 db,
383 username,
384 password,
385 tls_params,
386 }),
387 RedisAddress::Cluster(addr) => RedisConfig::Cluster(RedisClusterConnectionInfo {
388 addr,
389 db,
390 username,
391 password,
392 tls_params,
393 }),
394 }
395}
396
397fn parse_port(port: &Value) -> Option<u16> {
398 port.as_str()
399 .and_then(|port| port.parse().ok())
400 .or_else(|| port.as_int().map(|port| port as u16))
401}
402
403#[test]
404fn test_redis_empty_password_none() {
405 let config =
406 php_literal_parser::from_str(r#"["redis" => ["host" => "redis", "password" => "pass"]]"#)
407 .unwrap();
408 let redis = parse_redis_options(&config, "redis");
409 assert_eq!(redis.passwd(), Some("pass"));
410
411 let config =
412 php_literal_parser::from_str(r#"["redis" => ["host" => "redis", "password" => ""]]"#)
413 .unwrap();
414 let redis = parse_redis_options(&config, "redis");
415 assert_eq!(redis.passwd(), None);
416}
417
418#[test]
419fn test_postgres_port() {
420 use indexmap::indexmap;
421
422 let config = php_literal_parser::from_str(
423 r#"[
424 'dbtype' => 'pgsql',
425 'dbhost' => '127.0.0.1:6432',
426 'dbport' => '',
427 'dbuser' => 'nextcloud',
428 'dbpassword' => 'nextcloud',
429 'dbname' => 'nextcloud',
430 ]"#,
431 )
432 .unwrap();
433 let db = parse_db_options(&config).unwrap();
434 assert_eq!(
435 db,
436 Database::Postgres {
437 database: "nextcloud".to_string(),
438 username: "nextcloud".to_string(),
439 password: "nextcloud".to_string(),
440 connect: DbConnect::Tcp {
441 host: "127.0.0.1".into(),
442 port: 6432,
443 },
444 options: indexmap! {
445 "sslmode".into() => "disable".into(),
446 },
447 }
448 );
449 assert_eq!(
450 db.url(),
451 "postgresql://nextcloud:nextcloud@127.0.0.1:6432/nextcloud?sslmode=disable"
452 );
453
454 let config = php_literal_parser::from_str(
455 r#"[
456 'dbtype' => 'pgsql',
457 'dbhost' => '127.0.0.1',
458 'dbport' => '6432',
459 'dbuser' => 'nextcloud',
460 'dbpassword' => 'nextcloud',
461 'dbname' => 'nextcloud',
462 ]"#,
463 )
464 .unwrap();
465 let db = parse_db_options(&config).unwrap();
466 assert_eq!(
467 db,
468 Database::Postgres {
469 database: "nextcloud".to_string(),
470 username: "nextcloud".to_string(),
471 password: "nextcloud".to_string(),
472 connect: DbConnect::Tcp {
473 host: "127.0.0.1".into(),
474 port: 6432,
475 },
476 options: indexmap! {
477 "sslmode".into() => "disable".into(),
478 },
479 }
480 );
481 assert_eq!(
482 db.url(),
483 "postgresql://nextcloud:nextcloud@127.0.0.1:6432/nextcloud?sslmode=disable"
484 );
485}
486
487#[test]
488fn test_postgres_options() {
489 use indexmap::indexmap;
490
491 let config =
492 php_literal_parser::from_str(r#"[
493 'dbtype' => 'pgsql',
494 'dbhost' => 'db.example.org;sslmode=verify-ca;sslrootcert=/etc/ssl/certs/ca-certificates.crt;dbname=nextcloud',
495 'dbuser' => 'nextcloud',
496 'dbpassword' => 'nextcloud',
497 ]"#)
498 .unwrap();
499 let db = parse_db_options(&config).unwrap();
500 assert_eq!(
501 db,
502 Database::Postgres {
503 database: "nextcloud".to_string(),
504 username: "nextcloud".to_string(),
505 password: "nextcloud".to_string(),
506 connect: DbConnect::Tcp {
507 host: "db.example.org".into(),
508 port: 5432,
509 },
510 options: indexmap! {
511 "sslmode".into() => "verify-ca".into(),
512 "sslrootcert".into() => "/etc/ssl/certs/ca-certificates.crt".into(),
513 "dbname".into() => "nextcloud".into(),
514 },
515 }
516 );
517 assert_eq!(db.url(), "postgresql://nextcloud:nextcloud@db.example.org/nextcloud?sslmode=verify-ca&sslrootcert=/etc/ssl/certs/ca-certificates.crt&dbname=nextcloud");
518}