sofie_core/
local_database.rs1use std::net::IpAddr;
2
3use crate::encoding::percent;
4
5#[must_use]
8pub fn is_local_database(url: &str) -> bool {
9 if url.starts_with("sqlite://") || url == "sqlite::memory:" {
10 return true;
11 }
12 let Some(rest) = url
13 .strip_prefix("postgres://")
14 .or_else(|| url.strip_prefix("postgresql://"))
15 else {
16 return false;
17 };
18 let rest = rest.split_once('@').map_or(rest, |(_, after)| after);
20 let end = rest.find(['/', '?']).unwrap_or(rest.len());
21 let (hosts, path) = rest.split_at(end);
22 let parameters = path.split_once('?').map_or("", |(_, query)| query);
23 let Some(parameter_hosts) = parameter_hosts(parameters) else {
24 return false;
25 };
26 hosts
27 .split(',')
28 .map(without_port)
29 .chain(parameter_hosts.iter().map(String::as_str))
30 .all(is_local_host)
31}
32
33fn parameter_hosts(parameters: &str) -> Option<Vec<String>> {
36 let mut hosts = Vec::new();
37 for parameter in parameters.split('&') {
38 let Some((key, value)) = parameter.split_once('=') else {
39 continue;
40 };
41 match (decoded(key)?.as_str(), decoded(value)?) {
42 ("host", value) => hosts.extend(value.split(',').map(str::to_owned)),
43 ("hostaddr", addresses) if !addresses.split(',').all(is_loopback) => return None,
44 ("service", _) => return None,
46 _ => {}
47 }
48 }
49 Some(hosts)
50}
51
52fn decoded(text: &str) -> Option<String> {
53 String::from_utf8(percent::decode(text.as_bytes())?).ok()
54}
55
56fn is_loopback(address: &str) -> bool {
57 address
58 .parse::<IpAddr>()
59 .is_ok_and(|address| address.is_loopback())
60}
61
62fn without_port(host: &str) -> &str {
63 if let Some(bracketed) = host.strip_prefix('[') {
64 return bracketed.split(']').next().unwrap_or_default();
65 }
66 host.split(':').next().unwrap_or_default()
67}
68
69fn is_local_host(host: &str) -> bool {
71 host.is_empty()
72 || host.starts_with('/')
73 || host.to_ascii_lowercase().starts_with("%2f")
74 || matches!(host, "localhost" | "127.0.0.1" | "::1")
75}
76
77#[cfg(test)]
78mod tests {
79 use super::is_local_database;
80
81 #[test]
82 fn a_database_on_this_machine_is_local() {
83 for url in [
84 "postgres://sofie:sofie@127.0.0.1:25432/sofie",
85 "postgres://sofie:p%40ss@localhost/shop",
86 "postgres://localhost/shop?hostaddr=127.0.0.1",
87 "postgresql://[::1]:5432/shop",
88 "postgres:///shop",
89 "postgres://sofie@/shop?host=/var/run/postgresql",
90 "postgres://sofie@%2Fvar%2Frun%2Fpostgresql/shop",
91 "sqlite://app.db",
92 "sqlite::memory:",
93 ] {
94 assert!(is_local_database(url), "{url}");
95 }
96 }
97
98 #[test]
99 fn any_other_host_is_not() {
100 for url in [
101 "postgres://sofie:sofie@db.example.com/shop",
102 "postgres://sofie:sofie@10.0.0.5:5432/shop",
103 "postgres://localhost,db.example.com/shop",
104 "postgres://localhost/shop?host=db.example.com",
105 "postgres://sofie:sofie@localhost.example.com/shop",
106 "host=localhost dbname=shop",
107 "sqlite:app.db",
108 "postgres://localhost/shop?hostaddr=10.0.0.5",
109 "postgres://localhost/shop?hostaddr=127.0.0.1,10.0.0.5",
110 "postgres://localhost/shop?application_name=@db.example.com",
111 "postgres://sofie:p@ss@localhost/shop",
112 "postgres://localhost/shop?service=production",
113 "postgres://localhost/shop?host=/tmp,db.example.com",
114 "postgres://localhost/shop?host=%2Ftmp%2Cdb.example.com",
115 "postgres://localhost/shop?h%6Fstaddr=10.0.0.5",
116 "postgres://localhost/shop?%68ost=db.example.com",
117 ] {
118 assert!(!is_local_database(url), "{url}");
119 }
120 }
121}