1use crate::{
2 split_host, Config, Database, DbConnect, DbError, Error, NotAConfigError, PhpParseError,
3 RedisClusterConnectionInfo, RedisConnectionInfo, RedisTlsParams, Result, SslOptions,
4};
5use crate::{RedisConfig, RedisConnectionAddr};
6use php_literal_parser::Value;
7use std::collections::HashMap;
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 = HashMap::with_capacity(16);
76
77 for (path, config) in input {
78 match config.into_hashmap() {
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) = parsed["dbport"].clone().into_int() {
175 if let DbConnect::Tcp {
176 port: connect_port, ..
177 } = &mut connect
178 {
179 *connect_port = port as u16;
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 (mut connect, disable_ssl) =
219 match split_host(parsed["dbhost"].as_str().unwrap_or_default()) {
220 (addr, None, None) => (
221 DbConnect::Tcp {
222 host: addr.into(),
223 port: 5432,
224 },
225 IpAddr::from_str(addr).is_ok(),
226 ),
227 (addr, Some(port), None) => (
228 DbConnect::Tcp {
229 host: addr.into(),
230 port,
231 },
232 IpAddr::from_str(addr).is_ok(),
233 ),
234 (_, None, Some(socket)) => {
235 let mut socket_path = Path::new(socket);
236
237 if socket_path
239 .file_name()
240 .map(|name| name.to_str().unwrap().starts_with(".s"))
241 .unwrap_or(false)
242 {
243 socket_path = socket_path.parent().unwrap();
244 }
245 (DbConnect::Socket(socket_path.into()), false)
246 }
247 (_, Some(_), Some(_)) => {
248 unreachable!()
249 }
250 };
251 if let Some(port) = parsed["dbport"].clone().into_int() {
252 if let DbConnect::Tcp {
253 port: connect_port, ..
254 } = &mut connect
255 {
256 *connect_port = port as u16;
257 }
258 }
259 let database = parsed["dbname"].as_str().unwrap_or("owncloud");
260
261 let ssl_options = if disable_ssl {
262 SslOptions::Disabled
263 } else {
264 SslOptions::Default
265 };
266
267 Ok(Database::Postgres {
268 database: database.into(),
269 username: username.into(),
270 password: password.into(),
271 connect,
272 ssl_options,
273 })
274 }
275 Some("sqlite3") | Some("sqlite") | None => {
276 let data_dir = parsed["datadirectory"]
277 .as_str()
278 .ok_or(DbError::NoDataDirectory)?;
279 let db_name = parsed["dbname"].as_str().unwrap_or("owncloud");
280 Ok(Database::Sqlite {
281 database: format!("{data_dir}/{db_name}.db").into(),
282 })
283 }
284 Some(ty) => Err(Error::InvalidDb(DbError::Unsupported(ty.into()))),
285 }
286}
287
288enum RedisAddress {
289 Single(RedisConnectionAddr),
290 Cluster(Vec<RedisConnectionAddr>),
291}
292
293fn parse_redis_options(parsed: &Value, key: &str) -> RedisConfig {
294 let cluster_key = format!("{key}.cluster");
295 let cluster_key = cluster_key.as_str();
296
297 let (redis_options, address) = if parsed[cluster_key].is_array() {
298 let redis_options = &parsed[cluster_key];
299 let seeds = redis_options["seeds"].values();
300 let mut addresses = seeds
301 .filter_map(|seed| seed.as_str())
302 .map(|seed| {
303 RedisConnectionAddr::parse(seed, None, redis_options["ssl_context"].is_array())
304 })
305 .collect::<Vec<_>>();
306 addresses.sort();
307 (redis_options, RedisAddress::Cluster(addresses))
308 } else {
309 let redis_options = &parsed[key];
310 let host = redis_options["host"].as_str().unwrap_or("127.0.0.1");
311 let address = RedisAddress::Single(RedisConnectionAddr::parse(
312 host,
313 redis_options["port"]
314 .as_int()
315 .and_then(|port| u16::try_from(port).ok()),
316 redis_options["ssl_context"].is_array(),
317 ));
318 (redis_options, address)
319 };
320
321 let tls_params = if redis_options["ssl_context"].is_array() {
322 let ssl_options = &redis_options["ssl_context"];
323 Some(RedisTlsParams {
324 local_cert: ssl_options["local_cert"].as_str().map(From::from),
325 local_pk: ssl_options["local_pk"].as_str().map(From::from),
326 ca_file: ssl_options["cafile"].as_str().map(From::from),
327 accept_invalid_hostname: ssl_options["verify_peer_name"] == false,
328 insecure: ssl_options["verify_peer "] == false,
329 })
330 } else {
331 None
332 };
333
334 let db = redis_options["dbindex"].clone().into_int().or_else(|| {
335 redis_options["dbindex"]
336 .as_str()
337 .and_then(|i| i64::from_str(i).ok())
338 }).unwrap_or(0);
339 let password = redis_options["password"]
340 .as_str()
341 .filter(|pass| !pass.is_empty())
342 .map(String::from);
343 let username = redis_options["user"]
344 .as_str()
345 .filter(|user| !user.is_empty())
346 .map(String::from);
347
348 match address {
349 RedisAddress::Single(addr) => RedisConfig::Single(RedisConnectionInfo {
350 addr,
351 db,
352 username,
353 password,
354 tls_params,
355 }),
356 RedisAddress::Cluster(addr) => RedisConfig::Cluster(RedisClusterConnectionInfo {
357 addr,
358 db,
359 username,
360 password,
361 tls_params,
362 }),
363 }
364}
365
366#[test]
367fn test_redis_empty_password_none() {
368 let config =
369 php_literal_parser::from_str(r#"["redis" => ["host" => "redis", "password" => "pass"]]"#)
370 .unwrap();
371 let redis = parse_redis_options(&config, "redis");
372 assert_eq!(redis.passwd(), Some("pass"));
373
374 let config =
375 php_literal_parser::from_str(r#"["redis" => ["host" => "redis", "password" => ""]]"#)
376 .unwrap();
377 let redis = parse_redis_options(&config, "redis");
378 assert_eq!(redis.passwd(), None);
379}