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