sqlx_postgres/options/
pgpass.rs

1use std::borrow::Cow;
2use std::env::var_os;
3use std::fs::File;
4use std::io::{BufRead, BufReader};
5use std::path::PathBuf;
6
7/// try to load a password from the various pgpass file locations
8pub fn load_password(
9    host: &str,
10    port: u16,
11    username: &str,
12    database: Option<&str>,
13) -> Option<String> {
14    let custom_file = var_os("PGPASSFILE");
15    if let Some(file) = custom_file {
16        if let Some(password) =
17            load_password_from_file(PathBuf::from(file), host, port, username, database)
18        {
19            return Some(password);
20        }
21    }
22
23    #[cfg(not(target_os = "windows"))]
24    let default_file = home::home_dir().map(|path| path.join(".pgpass"));
25    #[cfg(target_os = "windows")]
26    let default_file = {
27        use etcetera::BaseStrategy;
28
29        etcetera::base_strategy::Windows::new()
30            .ok()
31            .map(|basedirs| basedirs.data_dir().join("postgres").join("pgpass.conf"))
32    };
33    load_password_from_file(default_file?, host, port, username, database)
34}
35
36/// try to extract a password from a pgpass file
37fn load_password_from_file(
38    path: PathBuf,
39    host: &str,
40    port: u16,
41    username: &str,
42    database: Option<&str>,
43) -> Option<String> {
44    let file = File::open(&path)
45        .map_err(|e| {
46            match e.kind() {
47                std::io::ErrorKind::NotFound => {
48                    tracing::debug!(
49                        path = %path.display(),
50                        "`.pgpass` file not found",
51                    );
52                }
53                _ => {
54                    tracing::warn!(
55                        path = %path.display(),
56                        "Failed to open `.pgpass` file: {e:?}",
57                    );
58                }
59            };
60        })
61        .ok()?;
62
63    #[cfg(target_os = "linux")]
64    {
65        use std::os::unix::fs::PermissionsExt;
66
67        // check file permissions on linux
68
69        let metadata = file.metadata().ok()?;
70        let permissions = metadata.permissions();
71        let mode = permissions.mode();
72        if mode & 0o77 != 0 {
73            tracing::warn!(
74                path = %path.display(),
75                permissions = format!("{mode:o}"),
76                "Ignoring path. Permissions are not strict enough",
77            );
78            return None;
79        }
80    }
81
82    let reader = BufReader::new(file);
83    load_password_from_reader(reader, host, port, username, database)
84}
85
86fn load_password_from_reader(
87    mut reader: impl BufRead,
88    host: &str,
89    port: u16,
90    username: &str,
91    database: Option<&str>,
92) -> Option<String> {
93    let mut line = String::new();
94
95    // https://stackoverflow.com/a/55041833
96    fn trim_newline(s: &mut String) {
97        if s.ends_with('\n') {
98            s.pop();
99            if s.ends_with('\r') {
100                s.pop();
101            }
102        }
103    }
104
105    while let Ok(n) = reader.read_line(&mut line) {
106        if n == 0 {
107            break;
108        }
109
110        if line.starts_with('#') {
111            // comment, do nothing
112        } else {
113            // try to load password from line
114            trim_newline(&mut line);
115            if let Some(password) = load_password_from_line(&line, host, port, username, database) {
116                return Some(password);
117            }
118        }
119
120        line.clear();
121    }
122
123    None
124}
125
126/// try to check all fields & extract the password
127fn load_password_from_line(
128    mut line: &str,
129    host: &str,
130    port: u16,
131    username: &str,
132    database: Option<&str>,
133) -> Option<String> {
134    let whole_line = line;
135
136    // Pgpass line ordering: hostname, port, database, username, password
137    // See: https://www.postgresql.org/docs/9.3/libpq-pgpass.html
138    match line.trim_start().chars().next() {
139        None | Some('#') => None,
140        _ => {
141            matches_next_field(whole_line, &mut line, host)?;
142            matches_next_field(whole_line, &mut line, &port.to_string())?;
143            matches_next_field(whole_line, &mut line, database.unwrap_or_default())?;
144            matches_next_field(whole_line, &mut line, username)?;
145            Some(line.to_owned())
146        }
147    }
148}
149
150/// check if the next field matches the provided value
151fn matches_next_field(whole_line: &str, line: &mut &str, value: &str) -> Option<()> {
152    let field = find_next_field(line);
153    match field {
154        Some(field) => {
155            if field == "*" || field == value {
156                Some(())
157            } else {
158                None
159            }
160        }
161        None => {
162            tracing::warn!(line = whole_line, "Malformed line in pgpass file");
163            None
164        }
165    }
166}
167
168/// extract the next value from a line in a pgpass file
169///
170/// `line` will get updated to point behind the field and delimiter
171fn find_next_field<'a>(line: &mut &'a str) -> Option<Cow<'a, str>> {
172    let mut escaping = false;
173    let mut escaped_string = None;
174    let mut last_added = 0;
175
176    let char_indicies = line.char_indices();
177    for (idx, c) in char_indicies {
178        if c == ':' && !escaping {
179            let (field, rest) = line.split_at(idx);
180            *line = &rest[1..];
181
182            if let Some(mut escaped_string) = escaped_string {
183                escaped_string += &field[last_added..];
184                return Some(Cow::Owned(escaped_string));
185            } else {
186                return Some(Cow::Borrowed(field));
187            }
188        } else if c == '\\' {
189            let s = escaped_string.get_or_insert_with(String::new);
190
191            if escaping {
192                s.push('\\');
193            } else {
194                *s += &line[last_added..idx];
195            }
196
197            escaping = !escaping;
198            last_added = idx + 1;
199        } else {
200            escaping = false;
201        }
202    }
203
204    None
205}
206
207#[cfg(test)]
208mod tests {
209    use super::{find_next_field, load_password_from_line, load_password_from_reader};
210    use std::borrow::Cow;
211
212    #[test]
213    fn test_find_next_field() {
214        fn test_case<'a>(mut input: &'a str, result: Option<Cow<'a, str>>, rest: &str) {
215            assert_eq!(find_next_field(&mut input), result);
216            assert_eq!(input, rest);
217        }
218
219        // normal field
220        test_case("foo:bar:baz", Some(Cow::Borrowed("foo")), "bar:baz");
221        // \ escaped
222        test_case(
223            "foo\\\\:bar:baz",
224            Some(Cow::Owned("foo\\".to_owned())),
225            "bar:baz",
226        );
227        // : escaped
228        test_case(
229            "foo\\::bar:baz",
230            Some(Cow::Owned("foo:".to_owned())),
231            "bar:baz",
232        );
233        // unnecessary escape
234        test_case(
235            "foo\\a:bar:baz",
236            Some(Cow::Owned("fooa".to_owned())),
237            "bar:baz",
238        );
239        // other text after escape
240        test_case(
241            "foo\\\\a:bar:baz",
242            Some(Cow::Owned("foo\\a".to_owned())),
243            "bar:baz",
244        );
245        // double escape
246        test_case(
247            "foo\\\\\\\\a:bar:baz",
248            Some(Cow::Owned("foo\\\\a".to_owned())),
249            "bar:baz",
250        );
251        // utf8 support
252        test_case("🦀:bar:baz", Some(Cow::Borrowed("🦀")), "bar:baz");
253
254        // missing delimiter (eof)
255        test_case("foo", None, "foo");
256        // missing delimiter after escape
257        test_case("foo\\:", None, "foo\\:");
258        // missing delimiter after unused trailing escape
259        test_case("foo\\", None, "foo\\");
260    }
261
262    #[test]
263    fn test_load_password_from_line() {
264        // normal
265        assert_eq!(
266            load_password_from_line(
267                "localhost:5432:bar:foo:baz",
268                "localhost",
269                5432,
270                "foo",
271                Some("bar")
272            ),
273            Some("baz".to_owned())
274        );
275        // wildcard
276        assert_eq!(
277            load_password_from_line("*:5432:bar:foo:baz", "localhost", 5432, "foo", Some("bar")),
278            Some("baz".to_owned())
279        );
280        // accept wildcard with missing db
281        assert_eq!(
282            load_password_from_line("localhost:5432:*:foo:baz", "localhost", 5432, "foo", None),
283            Some("baz".to_owned())
284        );
285
286        // doesn't match
287        assert_eq!(
288            load_password_from_line(
289                "thishost:5432:bar:foo:baz",
290                "thathost",
291                5432,
292                "foo",
293                Some("bar")
294            ),
295            None
296        );
297        // malformed entry
298        assert_eq!(
299            load_password_from_line(
300                "localhost:5432:bar:foo",
301                "localhost",
302                5432,
303                "foo",
304                Some("bar")
305            ),
306            None
307        );
308    }
309
310    #[test]
311    fn test_load_password_from_reader() {
312        let file = b"\
313            localhost:5432:bar:foo:baz\n\
314            # mixed line endings (also a comment!)\n\
315            *:5432:bar:foo:baz\r\n\
316            # trailing space, comment with CRLF! \r\n\
317            thishost:5432:bar:foo:baz \n\
318            # malformed line \n\
319            thathost:5432:foobar:foo\n\
320            # missing trailing newline\n\
321            localhost:5432:*:foo:baz
322        ";
323
324        // normal
325        assert_eq!(
326            load_password_from_reader(&mut &file[..], "localhost", 5432, "foo", Some("bar")),
327            Some("baz".to_owned())
328        );
329        // wildcard
330        assert_eq!(
331            load_password_from_reader(&mut &file[..], "localhost", 5432, "foo", Some("foobar")),
332            Some("baz".to_owned())
333        );
334        // accept wildcard with missing db
335        assert_eq!(
336            load_password_from_reader(&mut &file[..], "localhost", 5432, "foo", None),
337            Some("baz".to_owned())
338        );
339
340        // doesn't match
341        assert_eq!(
342            load_password_from_reader(&mut &file[..], "thathost", 5432, "foo", Some("foobar")),
343            None
344        );
345        // malformed entry
346        assert_eq!(
347            load_password_from_reader(&mut &file[..], "thathost", 5432, "foo", Some("foobar")),
348            None
349        );
350    }
351}