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: 4 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# ignore commits from showing up on git diffs.

# === large formatting commits ===
# TODO :: add commit once merged into mainline
6 changes: 1 addition & 5 deletions src/webserver/database/sqlpage_functions/function_traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,9 @@ impl<'a, T: IntoCow<'a>> IntoCow<'a> for Option<T> {
}
}

/// Declares the listed function modules and builds the [`SqlPageFunctionName`] dispatch enum from
/// them.
/// Builds the [`SqlPageFunctionName`] dispatch enum from the listed function modules.
macro_rules! sqlpage_functions {
($($func:ident),* $(,)?) => {
$(
mod $func;
)*

/// One variant per built-in `sqlpage.*` function.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
Expand Down
46 changes: 42 additions & 4 deletions src/webserver/database/sqlpage_functions/functions.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,53 @@
//! Built-in `SQLPage` SQL functions.
//!
//! Every function is a plain `async fn` in its own module under [`functions/`](self). To add one,
//! create `functions/<name>.rs` with an `async fn <name>` and add it to the
//! [`sqlpage_functions!`](super::function_traits::sqlpage_functions) call below. The macro declares
//! the module and adds it to the dispatch enum. Argument conversion and
//! dispatch are handled generically in [`super::function_traits`].
//! create `functions/<name>.rs` with an `async fn <name>`, declare the module below and add it to
//! the [`sqlpage_functions!`](super::function_traits::sqlpage_functions) call. Argument conversion
//! and dispatch are handled generically in [`super::function_traits`].

use std::fmt::Write;

use super::function_traits::sqlpage_functions;

mod basic_auth_password;
mod basic_auth_username;
mod client_ip;
mod configuration_directory;
mod cookie;
mod current_working_directory;
mod environment_variable;
mod exec;
mod fetch;
mod fetch_with_meta;
mod hash_password;
mod header;
mod headers;
mod hmac;
mod link;
mod oidc_logout_url;
mod path;
mod persist_uploaded_file;
mod protocol;
mod random_string;
mod read_file_as_data_url;
mod read_file_as_text;
mod regex_match;
mod request_body;
mod request_body_base64;
mod request_method;
mod run_sql;
mod send_mail;
mod set_variable;
mod uploaded_file_mime_type;
mod uploaded_file_name;
mod uploaded_file_path;
mod url_encode;
mod user_info;
mod user_info_token;
mod variables;
mod version;
mod web_root;

sqlpage_functions! {
basic_auth_password,
basic_auth_username,
Expand Down
5 changes: 4 additions & 1 deletion src/webserver/database/sqlpage_functions/functions/cookie.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ use std::borrow::Cow;

use crate::webserver::{http_request_info::RequestInfo, single_or_vec::SingleOrVec};

pub(super) async fn cookie<'a>(request: &'a RequestInfo, name: Cow<'a, str>) -> Option<Cow<'a, str>> {
pub(super) async fn cookie<'a>(
request: &'a RequestInfo,
name: Cow<'a, str>,
) -> Option<Cow<'a, str>> {
request.cookies.get(&*name).map(SingleOrVec::as_json_str)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use std::borrow::Cow;
use anyhow::Context;

/// Returns the value of an environment variable.
pub(super) async fn environment_variable(name: Cow<'_, str>) -> anyhow::Result<Option<Cow<'_, str>>> {
pub(super) async fn environment_variable(
name: Cow<'_, str>,
) -> anyhow::Result<Option<Cow<'_, str>>> {
match std::env::var(&*name) {
Ok(value) => Ok(Some(Cow::Owned(value))),
Err(std::env::VarError::NotPresent) if name.contains(['=', '\0']) => anyhow::bail!(
Expand Down
7 changes: 3 additions & 4 deletions src/webserver/database/sqlpage_functions/functions/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ use tracing::Instrument;

use crate::webserver::{
database::sqlpage_functions::http_fetch_request::HttpFetchRequest,
http_client::make_http_client,
http_request_info::RequestInfo,
http_client::make_http_client, http_request_info::RequestInfo,
};

pub(super) fn build_request<'a>(
Expand Down Expand Up @@ -94,8 +93,8 @@ pub(super) async fn fetch(

async {
let response_result = send_request(request, &http_request)?.await;
let mut response = response_result
.map_err(|e| anyhow!("Unable to fetch {}: {e}", http_request.url))?;
let mut response =
response_result.map_err(|e| anyhow!("Unable to fetch {}: {e}", http_request.url))?;

tracing::Span::current().record(
otel::HTTP_RESPONSE_STATUS_CODE,
Expand Down
5 changes: 4 additions & 1 deletion src/webserver/database/sqlpage_functions/functions/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use std::borrow::Cow;

use crate::webserver::{http_request_info::RequestInfo, single_or_vec::SingleOrVec};

pub(super) async fn header<'a>(request: &'a RequestInfo, name: Cow<'a, str>) -> Option<Cow<'a, str>> {
pub(super) async fn header<'a>(
request: &'a RequestInfo,
name: Cow<'a, str>,
) -> Option<Cow<'a, str>> {
let lower_name = name.to_ascii_lowercase();
request
.headers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ pub(super) async fn persist_uploaded_file<'a>(
}

#[cfg(unix)]
pub(super) async fn set_file_mode(path: &std::path::Path, mode: Option<&str>) -> anyhow::Result<()> {
pub(super) async fn set_file_mode(
path: &std::path::Path,
mode: Option<&str>,
) -> anyhow::Result<()> {
use std::os::unix::fs::PermissionsExt;
let mode = if let Some(mode) = mode {
u32::from_str_radix(mode, 8)
Expand All @@ -87,6 +90,9 @@ pub(super) async fn set_file_mode(path: &std::path::Path, mode: Option<&str>) ->
}

#[cfg(not(unix))]
pub(super) async fn set_file_mode(_path: &std::path::Path, _mode: Option<&str>) -> anyhow::Result<()> {
pub(super) async fn set_file_mode(
_path: &std::path::Path,
_mode: Option<&str>,
) -> anyhow::Result<()> {
Ok(())
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

/// Returns a random string of the specified length.
pub(super) async fn random_string(len: usize) -> anyhow::Result<String> {
// OsRng can block on Linux, so we run this on a blocking thread.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ use anyhow::Context;
use crate::{
filesystem::FileAccess,
webserver::{
database::blob_to_data_url::vec_to_data_uri_with_mime,
http_request_info::RequestInfo,
database::blob_to_data_url::vec_to_data_uri_with_mime, http_request_info::RequestInfo,
},
};

use super::uploaded_file_mime_type::{mime_from_upload_path, mime_guess_from_filename};

pub(super) async fn read_file_bytes(request: &RequestInfo, path_str: &str) -> Result<Vec<u8>, anyhow::Error> {
pub(super) async fn read_file_bytes(
request: &RequestInfo,
path_str: &str,
) -> Result<Vec<u8>, anyhow::Error> {
let path = std::path::Path::new(path_str);
// If the path is relative, it's relative to the web root, not the current working directory,
// and it can be fetched from the on-database filesystem table
Expand Down
103 changes: 57 additions & 46 deletions src/webserver/database/sqlpage_functions/functions/send_mail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,7 @@ enum SendMailError {
InvalidEmailCc { address: Option<String> },
InvalidEmailReplyTo { address: String },
InvalidAttachmentFilename { index: usize },
InvalidAttachment {
index: usize,
reason: anyhow::Error,
},
InvalidAttachment { index: usize, reason: anyhow::Error },
SmtpTlsFailed(anyhow::Error),
SmtpTimeout(anyhow::Error),
SmtpRejected(anyhow::Error),
Expand Down Expand Up @@ -136,10 +133,11 @@ impl SendMailError {
impl fmt::Display for SendMailError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidMessage(reason) => write_reason(formatter, "Invalid email message", reason),
Self::SmtpNotConfigured => formatter.write_str(
"sqlpage.send_mail() requires the smtp_host configuration option",
),
Self::InvalidMessage(reason) => {
write_reason(formatter, "Invalid email message", reason)
}
Self::SmtpNotConfigured => formatter
.write_str("sqlpage.send_mail() requires the smtp_host configuration option"),
Self::MissingEmailFrom => formatter.write_str(
"Email has no from address; set its from property or configure smtp_from",
),
Expand All @@ -153,14 +151,22 @@ impl fmt::Display for SendMailError {
write_invalid_recipient(formatter, "cc", address.as_deref())
}
Self::InvalidEmailReplyTo { address } => {
write!(formatter, "'{address}' is not a valid reply_to email address")
write!(
formatter,
"'{address}' is not a valid reply_to email address"
)
}
Self::InvalidAttachmentFilename { index } => {
write!(formatter, "Attachment filename at index {index} must not be empty")
}
Self::InvalidAttachment { index, reason } => {
write_reason(formatter, &format!("Invalid attachment at index {index}"), reason)
write!(
formatter,
"Attachment filename at index {index} must not be empty"
)
}
Self::InvalidAttachment { index, reason } => write_reason(
formatter,
&format!("Invalid attachment at index {index}"),
reason,
),
Self::SmtpTlsFailed(reason) => {
write_reason(formatter, "Unable to establish SMTP TLS", reason)
}
Expand All @@ -170,9 +176,11 @@ impl fmt::Display for SendMailError {
Self::SmtpRejected(reason) => {
write_reason(formatter, "SMTP server rejected the email", reason)
}
Self::SmtpConnectionFailed(reason) => {
write_reason(formatter, "Unable to communicate with the SMTP server", reason)
}
Self::SmtpConnectionFailed(reason) => write_reason(
formatter,
"Unable to communicate with the SMTP server",
reason,
),
}
}
}
Expand All @@ -185,7 +193,10 @@ fn write_invalid_recipient(
address: Option<&str>,
) -> fmt::Result {
match address {
Some(address) => write!(formatter, "'{address}' is not a valid {field} email address"),
Some(address) => write!(
formatter,
"'{address}' is not a valid {field} email address"
),
None => write!(formatter, "{field} must contain at least one email address"),
}
}
Expand Down Expand Up @@ -219,10 +230,7 @@ fn send_mail_result_json(result: SendMailResult<()>) -> String {
Ok(()) => serde_json::json!({ "status": "accepted" }).to_string(),
Err(error) => {
let message = format!("{error:#}");
log::warn!(
"sqlpage.send_mail failed with {}: {message}",
error.code()
);
log::warn!("sqlpage.send_mail failed with {}: {message}", error.code());
serde_json::json!({
"status": "error",
"error_code": error.code(),
Expand Down Expand Up @@ -300,12 +308,13 @@ fn resolve_bodies(

let html_body = if let Some(markdown_src) = body_md {
Some(
crate::template_helpers::render_markdown_to_html(config, markdown_src)
.map_err(|reason| {
crate::template_helpers::render_markdown_to_html(config, markdown_src).map_err(
|reason| {
SendMailError::InvalidMessage(anyhow::anyhow!(
"Failed to render body_md as HTML: {reason}"
))
})?,
},
)?,
)
} else {
body_html.map(std::string::ToString::to_string)
Expand Down Expand Up @@ -334,7 +343,8 @@ fn build_email(config: &AppConfig, request: MailRequest<'_>) -> SendMailResult<M
attachments,
} = request;

let (text_body, html_body) = resolve_bodies(config, body, body_html.as_ref(), body_md.as_ref())?;
let (text_body, html_body) =
resolve_bodies(config, body, body_html.as_ref(), body_md.as_ref())?;

let sender = from
.as_deref()
Expand All @@ -345,9 +355,7 @@ fn build_email(config: &AppConfig, request: MailRequest<'_>) -> SendMailResult<M
.map_err(|_| SendMailError::InvalidEmailFrom {
address: sender.to_string(),
})?;
let mut email = Message::builder()
.from(sender)
.subject(subject.as_ref());
let mut email = Message::builder().from(sender).subject(subject.as_ref());
for recipient in to.parse(RecipientField::To)? {
email = email.to(recipient);
}
Expand All @@ -357,11 +365,12 @@ fn build_email(config: &AppConfig, request: MailRequest<'_>) -> SendMailResult<M
}
}
if let Some(reply_to) = reply_to {
let parsed_reply_to = reply_to.parse::<Mailbox>().map_err(|_| {
SendMailError::InvalidEmailReplyTo {
address: reply_to.to_string(),
}
})?;
let parsed_reply_to =
reply_to
.parse::<Mailbox>()
.map_err(|_| SendMailError::InvalidEmailReplyTo {
address: reply_to.to_string(),
})?;
email = email.reply_to(parsed_reply_to);
}
if attachments.is_empty() {
Expand Down Expand Up @@ -412,10 +421,9 @@ fn build_email(config: &AppConfig, request: MailRequest<'_>) -> SendMailResult<M
let content_type = ContentType::parse(media_type)
.with_context(|| format!("Invalid attachment media type at index {index}"))
.map_err(|reason| SendMailError::InvalidAttachment { index, reason })?;
multipart = multipart.singlepart(Attachment::new(attachment.filename.into_owned()).body(
bytes,
content_type,
));
multipart = multipart.singlepart(
Attachment::new(attachment.filename.into_owned()).body(bytes, content_type),
);
}
email
.multipart(multipart)
Expand Down Expand Up @@ -461,9 +469,7 @@ mod tests {
thread,
};

use super::{
SendMailError, SmtpTlsMode, send_mail_result_json, send_mail_with_config,
};
use super::{SendMailError, SmtpTlsMode, send_mail_result_json, send_mail_with_config};
use crate::app_config::tests::test_config;

#[tokio::test]
Expand Down Expand Up @@ -693,7 +699,11 @@ mod tests {
.await
.unwrap_err();
assert!(matches!(&error, SendMailError::InvalidMessage(_)));
assert!(error.to_string().contains("cannot combine 'body_md' with 'body_html'"));
assert!(
error
.to_string()
.contains("cannot combine 'body_md' with 'body_html'")
);
}

#[tokio::test]
Expand All @@ -711,7 +721,11 @@ mod tests {
.await
.unwrap_err();
assert!(matches!(&error, SendMailError::InvalidMessage(_)));
assert!(error.to_string().contains("requires either 'body' or 'body_md'"));
assert!(
error
.to_string()
.contains("requires either 'body' or 'body_md'")
);
}

#[tokio::test]
Expand Down Expand Up @@ -773,10 +787,7 @@ mod tests {
)
.await
.unwrap_err();
assert!(matches!(
&error,
SendMailError::InvalidAttachment { .. }
));
assert!(matches!(&error, SendMailError::InvalidAttachment { .. }));
assert!(format!("{error:#}").contains("Decoded data exceeds the limit of 1 bytes"));
}

Expand Down
Loading