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 struct PgConString {
72 addr: String,
73 port: u16,
74 db: String,
75 user: String,
76 pwd: String,
77 admin: AdminUser,
78 pub session_age: Duration,
79}
80
81impl PgConString {
82 pub fn new(
83 addr: &str,
84 port: u16,
85 db: &str,
86 user: &str,
87 pwd: &str,
88 admin: AdminUser,
89 session_age: Duration,
90 ) -> Self {
91 Self {
92 addr: addr.to_owned(),
93 port,
94 db: db.to_owned(),
95 user: user.to_owned(),
96 pwd: pwd.to_owned(),
97 admin,
98 session_age,
99 }
100 }
101}
102
103impl From<&Settings> for PgConString {
104 fn from(s: &Settings) -> Self {
105 Self {
106 addr: s.postgresql.address.clone(),
107 port: s.postgresql.port,
108 db: s.postgresql.db.clone(),
109 user: s.postgresql.user.clone(),
110 pwd: s.postgresql.pwd.clone(),
111 admin: AdminUser {
112 pwd: s.setup.admin_pwd.clone(),
113 token: s.setup.admin_token.clone(),
114 salt: generate_salt(),
115 },
116 session_age: Duration::from_secs(s.registry.session_age_seconds),
117 }
118 }
119}
120
121const URL_COMPONENT: &AsciiSet = &NON_ALPHANUMERIC
124 .remove(b'-')
125 .remove(b'.')
126 .remove(b'_')
127 .remove(b'~');
128
129impl Display for PgConString {
130 fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
131 write!(
132 f,
133 "postgres://{}:{}@{}:{}/{}",
134 utf8_percent_encode(&self.user, URL_COMPONENT),
135 utf8_percent_encode(&self.pwd, URL_COMPONENT),
136 self.addr,
137 self.port,
138 utf8_percent_encode(&self.db, URL_COMPONENT)
139 )
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Hash)]
144pub struct SqliteConString {
145 pub path: PathBuf,
146 pub salt: String,
147 pub admin_pwd: String,
148 pub admin_token: Option<String>,
149 pub session_age: Duration,
150}
151
152impl SqliteConString {
153 pub fn new(
154 path: &Path,
155 salt: &str,
156 admin_pwd: &str,
157 admin_token: Option<String>,
158 session_age: Duration,
159 ) -> Self {
160 Self {
161 path: path.to_owned(),
162 salt: salt.to_owned(),
163 admin_pwd: admin_pwd.to_owned(),
164 admin_token,
165 session_age,
166 }
167 }
168}
169
170impl From<&Settings> for SqliteConString {
171 fn from(settings: &Settings) -> Self {
172 Self {
173 path: settings.sqlite_path(),
174 salt: generate_salt(),
175 admin_pwd: settings.setup.admin_pwd.clone(),
176 admin_token: settings.setup.admin_token.clone(),
177 session_age: Duration::from_secs(settings.registry.session_age_seconds),
178 }
179 }
180}
181
182impl Display for SqliteConString {
183 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
184 if self.path.to_str() == Some(":memory:") {
185 write!(f, "sqlite::memory:")
186 } else {
187 write!(f, "sqlite://{}?mode=rwc", self.path.display())
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use std::str::FromStr;
195
196 use percent_encoding::percent_decode_str;
197 use sea_orm::sqlx::postgres::PgConnectOptions;
198 use url::Url;
199
200 use super::*;
201
202 fn pg_con_string(user: &str, pwd: &str, db: &str) -> PgConString {
203 PgConString::new(
204 "localhost",
205 5432,
206 db,
207 user,
208 pwd,
209 AdminUser::new("admin".to_string(), None, "salt".to_string()),
210 Duration::from_mins(1),
211 )
212 }
213
214 #[test]
215 fn pg_con_string_plain_credentials_are_unchanged() {
216 let con = pg_con_string("kellnr", "secret", "kellnr-db");
217
218 assert_eq!(
219 con.to_string(),
220 "postgres://kellnr:secret@localhost:5432/kellnr-db"
221 );
222 }
223
224 #[test]
225 fn pg_con_string_encodes_special_characters() {
226 let con = pg_con_string("us@er", "a/b@c:d%e#f?g", "my db");
227
228 assert_eq!(
229 con.to_string(),
230 "postgres://us%40er:a%2Fb%40c%3Ad%25e%23f%3Fg@localhost:5432/my%20db"
231 );
232 }
233
234 #[test]
235 fn pg_con_string_round_trips_through_sqlx() {
236 let user = "us@er:name";
237 let pwd = "cDlJH15F/Xj@:%2F#?&=+ รครถ";
238 let db = "kellnr/db";
239 let con = pg_con_string(user, pwd, db);
240
241 let opts = PgConnectOptions::from_str(&con.to_string()).unwrap();
242 let url = Url::parse(&con.to_string()).unwrap();
244 let decoded_pwd = percent_decode_str(url.password().unwrap())
245 .decode_utf8()
246 .unwrap();
247
248 assert_eq!(opts.get_username(), user);
249 assert_eq!(decoded_pwd, pwd);
250 assert_eq!(opts.get_host(), "localhost");
251 assert_eq!(opts.get_port(), 5432);
252 assert_eq!(opts.get_database(), Some(db));
253 }
254}