1mod nc;
2
3use form_urlencoded::Serializer;
4use itertools::Either;
5use miette::Diagnostic;
6use std::iter::once;
7use std::path::PathBuf;
8use std::str::FromStr;
9use thiserror::Error;
10
11pub use nc::{parse, parse_glob};
12
13#[derive(Debug)]
14#[non_exhaustive]
15pub struct Config {
16 pub database: Database,
17 pub database_prefix: String,
18 pub redis: RedisConfig,
19 pub notify_push_redis: Option<RedisConfig>,
20 pub nextcloud_url: String,
21}
22
23#[derive(Debug)]
24pub enum RedisConfig {
25 Single(RedisConnectionInfo),
26 Cluster(RedisClusterConnectionInfo),
27}
28
29impl RedisConfig {
30 pub fn as_single(&self) -> Option<RedisConnectionInfo> {
31 match self {
32 RedisConfig::Single(single) => Some(single.clone()),
33 RedisConfig::Cluster(cluster) => cluster.iter().next(),
34 }
35 }
36}
37
38#[derive(Clone, Debug, PartialOrd, PartialEq, Ord, Eq)]
39pub enum RedisConnectionAddr {
40 Tcp { host: String, port: u16, tls: bool },
41 Unix { path: PathBuf },
42}
43
44impl RedisConnectionAddr {
45 fn parse(mut host: &str, port: Option<u16>, tls: bool) -> Self {
46 if host.starts_with("/") {
47 RedisConnectionAddr::Unix { path: host.into() }
48 } else {
49 let tls = if host.starts_with("tls://") || host.starts_with("rediss://") {
50 host = host.split_once("://").unwrap().1;
51 true
52 } else {
53 tls
54 };
55 if host == "localhost" {
56 host = "127.0.0.1";
57 }
58 let (host, port, _) = if let Some(port) = port {
59 (host, Some(port), None)
60 } else {
61 split_host(host)
62 };
63 RedisConnectionAddr::Tcp {
64 host: host.into(),
65 port: port.unwrap_or(6379),
66 tls,
67 }
68 }
69 }
70}
71
72#[derive(Clone, Debug)]
73pub struct RedisClusterConnectionInfo {
74 pub addr: Vec<RedisConnectionAddr>,
75 pub db: i64,
76 pub username: Option<String>,
77 pub password: Option<String>,
78 pub tls_params: Option<RedisTlsParams>,
79}
80
81impl RedisClusterConnectionInfo {
82 pub fn iter(&self) -> impl Iterator<Item = RedisConnectionInfo> + '_ {
83 self.addr.iter().cloned().map(|addr| RedisConnectionInfo {
84 addr,
85 db: self.db,
86 username: self.username.clone(),
87 password: self.password.clone(),
88 tls_params: self.tls_params.clone(),
89 })
90 }
91}
92
93#[derive(Clone, Debug)]
94pub struct RedisConnectionInfo {
95 pub addr: RedisConnectionAddr,
96 pub db: i64,
97 pub username: Option<String>,
98 pub password: Option<String>,
99 pub tls_params: Option<RedisTlsParams>,
100}
101
102#[derive(Clone, Debug, Default)]
103pub struct RedisTlsParams {
104 pub local_cert: Option<PathBuf>,
105 pub local_pk: Option<PathBuf>,
106 pub ca_file: Option<PathBuf>,
107 pub accept_invalid_hostname: bool,
108 pub insecure: bool,
109}
110
111impl RedisConfig {
112 pub fn addr(&self) -> impl Iterator<Item = &RedisConnectionAddr> {
113 match self {
114 RedisConfig::Single(conn) => Either::Left(once(&conn.addr)),
115 RedisConfig::Cluster(cluster) => Either::Right(cluster.addr.iter()),
116 }
117 }
118
119 pub fn db(&self) -> i64 {
120 match self {
121 RedisConfig::Single(conn) => conn.db,
122 RedisConfig::Cluster(cluster) => cluster.db,
123 }
124 }
125
126 pub fn username(&self) -> Option<&str> {
127 match self {
128 RedisConfig::Single(conn) => conn.username.as_deref(),
129 RedisConfig::Cluster(cluster) => cluster.username.as_deref(),
130 }
131 }
132
133 pub fn passwd(&self) -> Option<&str> {
134 match self {
135 RedisConfig::Single(conn) => conn.password.as_deref(),
136 RedisConfig::Cluster(cluster) => cluster.password.as_deref(),
137 }
138 }
139
140 pub fn is_empty(&self) -> bool {
141 match self {
142 RedisConfig::Single(_) => false,
143 RedisConfig::Cluster(cluster) => cluster.addr.is_empty(),
144 }
145 }
146}
147
148type Result<T, E = Error> = std::result::Result<T, E>;
149
150#[derive(Debug, Error, Diagnostic)]
151pub enum Error {
152 #[error(transparent)]
153 #[diagnostic(transparent)]
154 Php(PhpParseError),
155 #[error("Provided config file doesn't seem to be a nextcloud config file: {0:#}")]
156 NotAConfig(#[from] NotAConfigError),
157 #[error("Failed to read config file")]
158 ReadFailed(std::io::Error, PathBuf),
159 #[error("invalid database configuration: {0}")]
160 InvalidDb(#[from] DbError),
161 #[error("Invalid redis configuration")]
162 Redis,
163 #[error("`overwrite.cli.url` not set`")]
164 NoUrl,
165}
166
167#[derive(Debug, Error, Diagnostic)]
168#[error("Error while parsing '{path}':\n{err}")]
169#[diagnostic(forward(err))]
170pub struct PhpParseError {
171 err: php_literal_parser::ParseError,
172 path: PathBuf,
173}
174
175#[derive(Debug, Error)]
176pub enum DbError {
177 #[error("unsupported database type {0}")]
178 Unsupported(String),
179 #[error("no username set")]
180 NoUsername,
181 #[error("no password set")]
182 NoPassword,
183 #[error("no data directory")]
184 NoDataDirectory,
185}
186
187#[derive(Debug, Error)]
188pub enum NotAConfigError {
189 #[error("$CONFIG not found in file")]
190 NoConfig(PathBuf),
191 #[error("$CONFIG is not an array")]
192 NotAnArray(PathBuf),
193}
194
195#[derive(Debug, Clone)]
196pub enum SslOptions {
197 Enabled {
198 key: String,
199 cert: String,
200 ca: String,
201 verify: bool,
202 },
203 Disabled,
204 Default,
205}
206
207#[derive(Debug, Clone)]
208pub enum Database {
209 Sqlite {
210 database: PathBuf,
211 },
212 MySql {
213 database: String,
214 username: String,
215 password: String,
216 connect: DbConnect,
217 ssl_options: SslOptions,
218 },
219 Postgres {
220 database: String,
221 username: String,
222 password: String,
223 connect: DbConnect,
224 ssl_options: SslOptions,
225 },
226}
227
228#[derive(Debug, Clone)]
229pub enum DbConnect {
230 Tcp { host: String, port: u16 },
231 Socket(PathBuf),
232}
233
234impl Database {
235 pub fn url(&self) -> String {
236 match self {
237 Database::Sqlite { database } => {
238 format!("sqlite://{}", database.display())
239 }
240 Database::MySql {
241 database,
242 username,
243 password,
244 connect,
245 ssl_options,
246 } => {
247 let mut params = Serializer::new(String::new());
248 match ssl_options {
249 SslOptions::Default => {}
250 SslOptions::Disabled => {
251 params.append_pair("ssl-mode", "disabled");
252 }
253 SslOptions::Enabled { ca, verify, .. } => {
254 params.append_pair(
255 "ssl-mode",
256 if *verify {
257 "verify_identity"
258 } else {
259 "verify_ca"
260 },
261 );
262 params.append_pair("ssl-ca", ca.as_str());
263 }
264 }
265 let (host, port) = match connect {
266 DbConnect::Socket(socket) => {
267 params.append_pair("socket", &socket.to_string_lossy());
268 ("localhost", 3306) }
270 DbConnect::Tcp { host, port } => (host.as_str(), *port),
271 };
272 let params = params.finish().replace("%2F", "/");
273 let params_start = if params.is_empty() { "" } else { "?" };
274
275 if port == 3306 {
276 format!(
277 "mysql://{}:{}@{}/{}{}{}",
278 urlencoding::encode(username),
279 urlencoding::encode(password),
280 host,
281 database,
282 params_start,
283 params
284 )
285 } else {
286 format!(
287 "mysql://{}:{}@{}:{}/{}{}{}",
288 urlencoding::encode(username),
289 urlencoding::encode(password),
290 host,
291 port,
292 database,
293 params_start,
294 params
295 )
296 }
297 }
298 Database::Postgres {
299 database,
300 username,
301 password,
302 connect,
303 ssl_options,
304 } => {
305 let mut params = Serializer::new(String::new());
306 match ssl_options {
307 SslOptions::Default => {}
308 SslOptions::Disabled => {
309 params.append_pair("sslmode", "disable");
310 }
311 SslOptions::Enabled { ca, verify, .. } => {
312 params.append_pair(
313 "ssl-mode",
314 if *verify { "verify-full" } else { "verify-ca" },
315 );
316 params.append_pair("sslrootcert", ca.as_str());
317 }
318 }
319 let (host, port) = match connect {
320 DbConnect::Socket(socket) => {
321 params.append_pair("host", &socket.to_string_lossy());
322 ("localhost", 5432) }
324 DbConnect::Tcp { host, port } => (host.as_str(), *port),
325 };
326 let params = params.finish().replace("%2F", "/");
327 let params_start = if params.is_empty() { "" } else { "?" };
328
329 if port == 5432 {
330 format!(
331 "postgresql://{}:{}@{}/{}{}{}",
332 urlencoding::encode(username),
333 urlencoding::encode(password),
334 host,
335 database,
336 params_start,
337 params
338 )
339 } else {
340 format!(
341 "postgresql://{}:{}@{}:{}/{}{}{}",
342 urlencoding::encode(username),
343 urlencoding::encode(password),
344 host,
345 port,
346 database,
347 params_start,
348 params
349 )
350 }
351 }
352 }
353 }
354}
355
356fn split_host(host: &str) -> (&str, Option<u16>, Option<&str>) {
357 if host.starts_with('/') {
358 return ("localhost", None, Some(host));
359 }
360 let mut parts = host.split(':');
361 let host = parts.next().unwrap();
362 match parts
363 .next()
364 .map(|port_or_socket| u16::from_str(port_or_socket).map_err(|_| port_or_socket))
365 {
366 Some(Ok(port)) => (host, Some(port), None),
367 Some(Err(socket)) => (host, None, Some(socket)),
368 None => (host, None, None),
369 }
370}