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.find("$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 disable_ssl {
270 options.insert("sslmode".into(), "disable".into());
271 }
272
273 let database = parsed["dbname"]
274 .as_str()
275 .or_else(|| options.get("dbname").map(String::as_str))
276 .unwrap_or("owncloud");
277
278 Ok(Database::Postgres {
279 database: database.into(),
280 username: username.into(),
281 password: password.into(),
282 connect,
283 options,
284 })
285 }
286 Some("sqlite3") | Some("sqlite") | None => {
287 let data_dir = parsed["datadirectory"]
288 .as_str()
289 .ok_or(DbError::NoDataDirectory)?;
290 let db_name = parsed["dbname"].as_str().unwrap_or("owncloud");
291 Ok(Database::Sqlite {
292 database: format!("{data_dir}/{db_name}.db").into(),
293 })
294 }
295 Some(ty) => Err(Error::InvalidDb(DbError::Unsupported(ty.into()))),
296 }
297}
298
299enum RedisAddress {
300 Single(RedisConnectionAddr),
301 Cluster(Vec<RedisConnectionAddr>),
302}
303
304fn parse_redis_options(parsed: &Value, key: &str) -> RedisConfig {
305 let cluster_key = format!("{key}.cluster");
306 let cluster_key = cluster_key.as_str();
307
308 let (redis_options, address) = if parsed[cluster_key].is_array() {
309 let redis_options = &parsed[cluster_key];
310 let seeds = redis_options["seeds"].values();
311 let mut addresses = seeds
312 .filter_map(|seed| seed.as_str())
313 .map(|seed| {
314 RedisConnectionAddr::parse(seed, None, redis_options["ssl_context"].is_array())
315 })
316 .collect::<Vec<_>>();
317 addresses.sort();
318 (redis_options, RedisAddress::Cluster(addresses))
319 } else {
320 let redis_options = &parsed[key];
321 let host = redis_options["host"].as_str().unwrap_or("127.0.0.1");
322 let address = RedisAddress::Single(RedisConnectionAddr::parse(
323 host,
324 redis_options["port"]
325 .as_int()
326 .and_then(|port| u16::try_from(port).ok()),
327 redis_options["ssl_context"].is_array(),
328 ));
329 (redis_options, address)
330 };
331
332 let tls_params = if redis_options["ssl_context"].is_array() {
333 let ssl_options = &redis_options["ssl_context"];
334 Some(RedisTlsParams {
335 local_cert: ssl_options["local_cert"].as_str().map(From::from),
336 local_pk: ssl_options["local_pk"].as_str().map(From::from),
337 ca_file: ssl_options["cafile"].as_str().map(From::from),
338 accept_invalid_hostname: ssl_options["verify_peer_name"] == false,
339 insecure: ssl_options["verify_peer "] == false,
340 })
341 } else {
342 None
343 };
344
345 let db = redis_options["dbindex"]
346 .clone()
347 .into_int()
348 .or_else(|| {
349 redis_options["dbindex"]
350 .as_str()
351 .and_then(|i| i64::from_str(i).ok())
352 })
353 .unwrap_or(0);
354 let password = redis_options["password"]
355 .as_str()
356 .filter(|pass| !pass.is_empty())
357 .map(String::from);
358 let username = redis_options["user"]
359 .as_str()
360 .filter(|user| !user.is_empty())
361 .map(String::from);
362
363 match address {
364 RedisAddress::Single(addr) => RedisConfig::Single(RedisConnectionInfo {
365 addr,
366 db,
367 username,
368 password,
369 tls_params,
370 }),
371 RedisAddress::Cluster(addr) => RedisConfig::Cluster(RedisClusterConnectionInfo {
372 addr,
373 db,
374 username,
375 password,
376 tls_params,
377 }),
378 }
379}
380
381fn parse_port(port: &Value) -> Option<u16> {
382 port.as_str()
383 .and_then(|port| port.parse().ok())
384 .or_else(|| port.as_int().map(|port| port as u16))
385}
386
387#[test]
388fn test_redis_empty_password_none() {
389 let config =
390 php_literal_parser::from_str(r#"["redis" => ["host" => "redis", "password" => "pass"]]"#)
391 .unwrap();
392 let redis = parse_redis_options(&config, "redis");
393 assert_eq!(redis.passwd(), Some("pass"));
394
395 let config =
396 php_literal_parser::from_str(r#"["redis" => ["host" => "redis", "password" => ""]]"#)
397 .unwrap();
398 let redis = parse_redis_options(&config, "redis");
399 assert_eq!(redis.passwd(), None);
400}
401
402#[test]
403fn test_postgres_port() {
404 use indexmap::indexmap;
405
406 let config = php_literal_parser::from_str(
407 r#"[
408 'dbtype' => 'pgsql',
409 'dbhost' => '127.0.0.1:6432',
410 'dbport' => '',
411 'dbuser' => 'nextcloud',
412 'dbpassword' => 'nextcloud',
413 'dbname' => 'nextcloud',
414 ]"#,
415 )
416 .unwrap();
417 let db = parse_db_options(&config).unwrap();
418 assert_eq!(
419 db,
420 Database::Postgres {
421 database: "nextcloud".to_string(),
422 username: "nextcloud".to_string(),
423 password: "nextcloud".to_string(),
424 connect: DbConnect::Tcp {
425 host: "127.0.0.1".into(),
426 port: 6432,
427 },
428 options: indexmap! {
429 "sslmode".into() => "disable".into(),
430 },
431 }
432 );
433 assert_eq!(
434 db.url(),
435 "postgresql://nextcloud:nextcloud@127.0.0.1:6432/nextcloud?sslmode=disable"
436 );
437
438 let config = php_literal_parser::from_str(
439 r#"[
440 'dbtype' => 'pgsql',
441 'dbhost' => '127.0.0.1',
442 'dbport' => '6432',
443 'dbuser' => 'nextcloud',
444 'dbpassword' => 'nextcloud',
445 'dbname' => 'nextcloud',
446 ]"#,
447 )
448 .unwrap();
449 let db = parse_db_options(&config).unwrap();
450 assert_eq!(
451 db,
452 Database::Postgres {
453 database: "nextcloud".to_string(),
454 username: "nextcloud".to_string(),
455 password: "nextcloud".to_string(),
456 connect: DbConnect::Tcp {
457 host: "127.0.0.1".into(),
458 port: 6432,
459 },
460 options: indexmap! {
461 "sslmode".into() => "disable".into(),
462 },
463 }
464 );
465 assert_eq!(
466 db.url(),
467 "postgresql://nextcloud:nextcloud@127.0.0.1:6432/nextcloud?sslmode=disable"
468 );
469}
470
471#[test]
472fn test_postgres_options() {
473 use indexmap::indexmap;
474
475 let config =
476 php_literal_parser::from_str(r#"[
477 'dbtype' => 'pgsql',
478 'dbhost' => 'db.example.org;sslmode=verify-ca;sslrootcert=/etc/ssl/certs/ca-certificates.crt;dbname=nextcloud',
479 'dbuser' => 'nextcloud',
480 'dbpassword' => 'nextcloud',
481 ]"#)
482 .unwrap();
483 let db = parse_db_options(&config).unwrap();
484 assert_eq!(
485 db,
486 Database::Postgres {
487 database: "nextcloud".to_string(),
488 username: "nextcloud".to_string(),
489 password: "nextcloud".to_string(),
490 connect: DbConnect::Tcp {
491 host: "db.example.org".into(),
492 port: 5432,
493 },
494 options: indexmap! {
495 "sslmode".into() => "verify-ca".into(),
496 "sslrootcert".into() => "/etc/ssl/certs/ca-certificates.crt".into(),
497 "dbname".into() => "nextcloud".into(),
498 },
499 }
500 );
501 assert_eq!(db.url(), "postgresql://nextcloud:nextcloud@db.example.org/nextcloud?sslmode=verify-ca&sslrootcert=/etc/ssl/certs/ca-certificates.crt&dbname=nextcloud");
502}