1use std::fmt::{Display, Formatter};
2use std::path::{Path, PathBuf};
3use std::time::Duration;
4
5use kellnr_settings::Settings;
6use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};
7
8use crate::password::generate_salt;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub enum ConString {
12 Postgres(PgConString),
13 Sqlite(SqliteConString),
14}
15
16impl Display for ConString {
17 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
18 let con_string = match self {
19 ConString::Postgres(p) => p.to_string(),
20 ConString::Sqlite(s) => s.to_string(),
21 };
22 write!(f, "{con_string}")
23 }
24}
25
26impl ConString {
27 pub fn admin_pwd(&self) -> String {
28 match self {
29 ConString::Postgres(p) => p.admin.pwd.clone(),
30 ConString::Sqlite(s) => s.admin_pwd.clone(),
31 }
32 }
33
34 pub fn salt(&self) -> String {
35 match self {
36 ConString::Postgres(p) => p.admin.salt.clone(),
37 ConString::Sqlite(s) => s.salt.clone(),
38 }
39 }
40
41 pub fn admin_token(&self) -> Option<String> {
42 match self {
43 ConString::Postgres(p) => p.admin.token.clone(),
44 ConString::Sqlite(s) => s.admin_token.clone(),
45 }
46 }
47
48 pub fn session_age(&self) -> Duration {
50 match self {
51 ConString::Postgres(p) => p.session_age,
52 ConString::Sqlite(s) => s.session_age,
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub struct AdminUser {
59 pub pwd: String,
60 pub token: Option<String>,
61 pub salt: String,
62}
63
64impl AdminUser {
65 pub fn new(pwd: String, token: Option<String>, salt: String) -> Self {
66 Self { pwd, token, salt }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Hash)]
71pub enum PgConStringInner {
72 Structured {
74 addr: String,
75 port: u16,
76 db: String,
77 user: String,
78 pwd: String,
79 },
80 Raw(String),
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Hash)]
85pub struct PgConString {
86 inner: PgConStringInner,
87 admin: AdminUser,
88 pub session_age: Duration,
89}
90
91impl PgConString {
92 pub fn new(
93 addr: &str,
94 port: u16,
95 db: &str,
96 user: &str,
97 pwd: &str,
98 admin: AdminUser,
99 session_age: Duration,
100 ) -> Self {
101 Self {
102 inner: PgConStringInner::Structured {
103 addr: addr.to_owned(),
104 port,
105 db: db.to_owned(),
106 user: user.to_owned(),
107 pwd: pwd.to_owned(),
108 },
109 admin,
110 session_age,
111 }
112 }
113}
114
115impl From<&Settings> for PgConString {
116 fn from(s: &Settings) -> Self {
117 let inner = if let Some(string) = s.postgresql.connection_string.clone() {
118 PgConStringInner::Raw(string)
119 } else {
120 PgConStringInner::Structured {
121 addr: s.postgresql.address.clone(),
122 port: s.postgresql.port,
123 db: s.postgresql.db.clone(),
124 user: s.postgresql.user.clone(),
125 pwd: s.postgresql.pwd.clone(),
126 }
127 };
128
129 Self {
130 inner,
131 admin: AdminUser {
132 pwd: s.setup.admin_pwd.clone(),
133 token: s.setup.admin_token.clone(),
134 salt: generate_salt(),
135 },
136 session_age: Duration::from_secs(s.registry.session_age_seconds),
137 }
138 }
139}
140
141const URL_COMPONENT: &AsciiSet = &NON_ALPHANUMERIC
144 .remove(b'-')
145 .remove(b'.')
146 .remove(b'_')
147 .remove(b'~');
148
149impl Display for PgConString {
150 fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
151 match &self.inner {
152 PgConStringInner::Raw(s) => write!(f, "{s}"),
153 PgConStringInner::Structured {
154 user,
155 pwd,
156 addr,
157 port,
158 db,
159 } => {
160 write!(
161 f,
162 "postgres://{}:{}@{}:{}/{}",
163 utf8_percent_encode(user, URL_COMPONENT),
164 utf8_percent_encode(pwd, URL_COMPONENT),
165 addr,
166 port,
167 utf8_percent_encode(db, URL_COMPONENT)
168 )
169 }
170 }
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Hash)]
175pub struct SqliteConString {
176 pub path: PathBuf,
177 pub salt: String,
178 pub admin_pwd: String,
179 pub admin_token: Option<String>,
180 pub session_age: Duration,
181}
182
183impl SqliteConString {
184 pub fn new(
185 path: &Path,
186 salt: &str,
187 admin_pwd: &str,
188 admin_token: Option<String>,
189 session_age: Duration,
190 ) -> Self {
191 Self {
192 path: path.to_owned(),
193 salt: salt.to_owned(),
194 admin_pwd: admin_pwd.to_owned(),
195 admin_token,
196 session_age,
197 }
198 }
199}
200
201impl From<&Settings> for SqliteConString {
202 fn from(settings: &Settings) -> Self {
203 Self {
204 path: settings.sqlite_path(),
205 salt: generate_salt(),
206 admin_pwd: settings.setup.admin_pwd.clone(),
207 admin_token: settings.setup.admin_token.clone(),
208 session_age: Duration::from_secs(settings.registry.session_age_seconds),
209 }
210 }
211}
212
213impl Display for SqliteConString {
214 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
215 if self.path.to_str() == Some(":memory:") {
216 write!(f, "sqlite::memory:")
217 } else {
218 write!(f, "sqlite://{}?mode=rwc", self.path.display())
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use std::str::FromStr;
226
227 use percent_encoding::percent_decode_str;
228 use sea_orm::sqlx::postgres::PgConnectOptions;
229 use url::Url;
230
231 use super::*;
232
233 fn pg_con_string(user: &str, pwd: &str, db: &str) -> PgConString {
234 PgConString::new(
235 "localhost",
236 5432,
237 db,
238 user,
239 pwd,
240 AdminUser::new("admin".to_string(), None, "salt".to_string()),
241 Duration::from_mins(1),
242 )
243 }
244
245 #[test]
246 fn pg_con_string_plain_credentials_are_unchanged() {
247 let con = pg_con_string("kellnr", "secret", "kellnr-db");
248
249 assert_eq!(
250 con.to_string(),
251 "postgres://kellnr:secret@localhost:5432/kellnr-db"
252 );
253 }
254
255 #[test]
256 fn pg_con_string_encodes_special_characters() {
257 let con = pg_con_string("us@er", "a/b@c:d%e#f?g", "my db");
258
259 assert_eq!(
260 con.to_string(),
261 "postgres://us%40er:a%2Fb%40c%3Ad%25e%23f%3Fg@localhost:5432/my%20db"
262 );
263 }
264
265 #[test]
266 fn pg_con_string_round_trips_through_sqlx() {
267 let user = "us@er:name";
268 let pwd = "cDlJH15F/Xj@:%2F#?&=+ รครถ";
269 let db = "kellnr/db";
270 let con = pg_con_string(user, pwd, db);
271
272 let opts = PgConnectOptions::from_str(&con.to_string()).unwrap();
273 let url = Url::parse(&con.to_string()).unwrap();
275 let decoded_pwd = percent_decode_str(url.password().unwrap())
276 .decode_utf8()
277 .unwrap();
278
279 assert_eq!(opts.get_username(), user);
280 assert_eq!(decoded_pwd, pwd);
281 assert_eq!(opts.get_host(), "localhost");
282 assert_eq!(opts.get_port(), 5432);
283 assert_eq!(opts.get_database(), Some(db));
284 }
285}