Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions sqlx-postgres/src/connection/establish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ impl PgConnection {

stream
.send(Password::Cleartext(
options.password.as_deref().unwrap_or_default(),
&options.get_password().unwrap_or_default(),
))
.await?;
}
Expand All @@ -91,7 +91,7 @@ impl PgConnection {
stream
.send(Password::Md5 {
username: &options.username,
password: options.password.as_deref().unwrap_or_default(),
password: &options.get_password().unwrap_or_default(),
salt: body.salt,
})
.await?;
Expand Down
2 changes: 1 addition & 1 deletion sqlx-postgres/src/connection/sasl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub(crate) async fn authenticate(

// SaltedPassword := Hi(Normalize(password), salt, i)
let salted_password = hi(
options.password.as_deref().unwrap_or_default(),
&options.get_password().unwrap_or_default(),
&cont.salt,
cont.iterations,
)?;
Expand Down
34 changes: 26 additions & 8 deletions sqlx-postgres/src/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};

pub use ssl_mode::PgSslMode;

use crate::options::pgpass::PGPassFile;
use crate::{connection::LogSettings, net::tls::CertificateInput};

mod connect;
Expand All @@ -20,6 +21,7 @@ pub struct PgConnectOptions {
pub(crate) socket: Option<PathBuf>,
pub(crate) username: String,
pub(crate) password: Option<String>,
pub(crate) passfile: PGPassFile,
pub(crate) database: Option<String>,
pub(crate) ssl_mode: PgSslMode,
pub(crate) ssl_root_cert: Option<CertificateInput>,
Expand Down Expand Up @@ -74,6 +76,7 @@ impl PgConnectOptions {
socket: None,
username,
password: var("PGPASSWORD").ok(),
passfile: PGPassFile::default(),
database,
ssl_root_cert: var("PGSSLROOTCERT").ok().map(CertificateInput::from),
ssl_client_cert: var("PGSSLCERT").ok().map(CertificateInput::from),
Expand All @@ -94,14 +97,7 @@ impl PgConnectOptions {
}

pub(crate) fn apply_pgpass(mut self) -> Self {
if self.password.is_none() {
self.password = pgpass::load_password(
&self.host,
self.port,
&self.username,
self.database.as_deref(),
);
}
self.passfile = PGPassFile::load().unwrap_or_default();

self
}
Expand Down Expand Up @@ -519,6 +515,28 @@ impl PgConnectOptions {
&self.username
}

/// Get the password.
///
/// ```rust
/// # use sqlx_postgres::PgConnectOptions;
/// let options = PgConnectOptions::new()
/// .password("53C237");
/// assert_eq!(options.get_password().as_deref(), Some("53C237"));
/// ```
pub fn get_password(&self) -> Option<Cow<'_, str>> {
if self.password.is_some() {
return self.password.as_deref().map(Cow::Borrowed);
}
self.passfile
.password_if_matching(
&self.host,
self.port,
self.database.as_deref(),
&self.username,
)
.map(Cow::Owned)
}

/// Get the current database name.
///
/// # Example
Expand Down
158 changes: 86 additions & 72 deletions sqlx-postgres/src/options/pgpass.rs
Original file line number Diff line number Diff line change
@@ -1,86 +1,100 @@
use std::borrow::Cow;
use std::env::var_os;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::io::{BufRead, Read};
use std::path::PathBuf;

/// try to load a password from the various pgpass file locations
pub fn load_password(
host: &str,
port: u16,
username: &str,
database: Option<&str>,
) -> Option<String> {
let custom_file = var_os("PGPASSFILE");
if let Some(file) = custom_file {
if let Some(password) =
load_password_from_file(PathBuf::from(file), host, port, username, database)
{
return Some(password);
/// PostgreSQL passfile content.
#[derive(Clone, Debug, Default)]
pub struct PGPassFile(String);

impl PGPassFile {
/// Loads the first valid passfile discovered.
///
/// Loading is attempted in the following order:
/// 1. Path given via the `PGPASSFILE` environment variable.
/// 2. Default path (`~/.pgpass` on Linux and
/// `%APPDATA%/postgres/pgpass.conf` on Windows)
///
/// If loading of any file fails, the function proceeds to the next.
/// Returns `None` in case no file can be loaded.
pub fn load() -> Option<Self> {
let custom_file = var_os("PGPASSFILE");
if let Some(file) = custom_file {
if let Some(password) = Self::load_from_file(PathBuf::from(file)) {
return Some(password);
}
}
}

#[cfg(not(target_os = "windows"))]
let default_file = home::home_dir().map(|path| path.join(".pgpass"));
#[cfg(target_os = "windows")]
let default_file = {
use etcetera::BaseStrategy;

etcetera::base_strategy::Windows::new()
.ok()
.map(|basedirs| basedirs.data_dir().join("postgres").join("pgpass.conf"))
};
load_password_from_file(default_file?, host, port, username, database)
}
#[cfg(not(target_os = "windows"))]
let default_file = home::home_dir().map(|path| path.join(".pgpass"));
#[cfg(target_os = "windows")]
let default_file = {
use etcetera::BaseStrategy;

etcetera::base_strategy::Windows::new()
.ok()
.map(|basedirs| basedirs.data_dir().join("postgres").join("pgpass.conf"))
};
Self::load_from_file(default_file?)
}

/// try to extract a password from a pgpass file
fn load_password_from_file(
path: PathBuf,
host: &str,
port: u16,
username: &str,
database: Option<&str>,
) -> Option<String> {
let file = File::open(&path)
.map_err(|e| {
match e.kind() {
std::io::ErrorKind::NotFound => {
tracing::debug!(
path = %path.display(),
"`.pgpass` file not found",
);
}
_ => {
tracing::warn!(
path = %path.display(),
"Failed to open `.pgpass` file: {e:?}",
);
}
};
})
.ok()?;

#[cfg(target_os = "linux")]
{
use std::os::unix::fs::PermissionsExt;

// check file permissions on linux

let metadata = file.metadata().ok()?;
let permissions = metadata.permissions();
let mode = permissions.mode();
if mode & 0o77 != 0 {
tracing::warn!(
path = %path.display(),
permissions = format!("{mode:o}"),
"Ignoring path. Permissions are not strict enough",
);
return None;
/// Returns the PostgreSQL passfile loaded from the given path.
fn load_from_file(path: PathBuf) -> Option<Self> {
let mut file = File::open(&path)
.map_err(|e| {
match e.kind() {
std::io::ErrorKind::NotFound => {
tracing::debug!(
path = %path.display(),
"`.pgpass` file not found",
);
}
_ => {
tracing::warn!(
path = %path.display(),
"Failed to open `.pgpass` file: {e:?}",
);
}
};
})
.ok()?;

#[cfg(target_os = "linux")]
{
use std::os::unix::fs::PermissionsExt;

// check file permissions on linux

let metadata = file.metadata().ok()?;
let permissions = metadata.permissions();
let mode = permissions.mode();
if mode & 0o77 != 0 {
tracing::warn!(
path = %path.display(),
permissions = format!("{mode:o}"),
"Ignoring path. Permissions are not strict enough",
);
return None;
}
}

let mut passfile = Self::default();
file.read_to_string(&mut passfile.0).ok()?;

Some(passfile)
}

let reader = BufReader::new(file);
load_password_from_reader(reader, host, port, username, database)
/// Returns the password matched by the given parameters.
pub fn password_if_matching(
&self,
hostname: &str,
port: u16,
database: Option<&str>,
username: &str,
) -> Option<String> {
load_password_from_reader(self.0.as_bytes(), hostname, port, username, database)
}
}

fn load_password_from_reader(
Expand Down
Loading