Skip to content

Commit e8ba798

Browse files
committed
add support for using the system's root ca certificates in sqlpage.fetch
fixes #507
1 parent 6cc4cce commit e8ba798

File tree

8 files changed

+109
-11
lines changed

8 files changed

+109
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- reduce the margin at the botton of forms to make them more compact.
1717
- fix [datagrid](https://sql.ophir.dev/documentation.sql?component=datagrid#component) color pills display when they contain long text.
1818
- fix the "started successfully" message being displayed before the error message when the server failed to start.
19+
- add support for using the system's native SSL Certificate Authority (CA) store in `sqlpage.fetch`. See the new `system_root_ca_certificates` configuration option.
1920

2021
## 0.25.0 (2024-07-13)
2122

Cargo.lock

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ base64 = "0.22"
5858
rustls-acme = "0.9.2"
5959
dotenvy = "0.15.7"
6060
csv-async = { version = "1.2.6", features = ["tokio"] }
61+
rustls = { version = "0.22.0" } # keep in sync with actix-web, awc, rustls-acme, and sqlx
62+
rustls-native-certs = "0.7.0"
6163
awc = { version = "3", features = ["rustls-0_22-webpki-roots"] }
6264

6365
[build-dependencies]

configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Here are the available configuration options and their default values:
3131
| `https_acme_directory_url` | https://acme-v02.api.letsencrypt.org/directory | The URL of the ACME directory to use when requesting a certificate. |
3232
| `environment` | development | The environment in which SQLPage is running. Can be either `development` or `production`. In `production` mode, SQLPage will hide error messages and stack traces from the user, and will cache sql files in memory to avoid reloading them from disk. |
3333
| `content_security_policy` | | The [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) to set in the HTTP headers. If you get CSP errors in the browser console, you can set this to the empty string to disable CSP. |
34+
| `system_root_ca_certificates` | false | Whether to use the system root CA certificates to validate SSL certificates when making http requests with `sqlpage.fetch`. If set to false, SQLPage will use its own set of root CA certificates. If the `SSL_CERT_FILE` or `SSL_CERT_DIR` environment variables are set, they will be used instead of the system root CA certificates. |
3435

3536
Multiple configuration file formats are supported:
3637
you can use a [`.json5`](https://json5.org/) file, a [`.toml`](https://toml.io/) file, or a [`.yaml`](https://en.wikipedia.org/wiki/YAML#Syntax) file.

src/app_config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ pub struct AppConfig {
100100

101101
/// Content-Security-Policy header to send to the client. If not set, a default policy allowing scripts from the same origin is used and from jsdelivr.net
102102
pub content_security_policy: Option<String>,
103+
104+
/// Whether `sqlpage.fetch` should load trusted certificates from the operating system's certificate store
105+
/// By default, it loads Mozilla's root certificates that are embedded in the `SQLPage` binary, or the ones pointed to by the
106+
/// `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables.
107+
#[serde(default = "default_system_root_ca_certificates")]
108+
pub system_root_ca_certificates: bool,
103109
}
104110

105111
impl AppConfig {
@@ -322,6 +328,10 @@ fn default_compress_responses() -> bool {
322328
true
323329
}
324330

331+
fn default_system_root_ca_certificates() -> bool {
332+
std::env::var("SSL_CERT_FILE").is_ok() || std::env::var("SSL_CERT_DIR").is_ok()
333+
}
334+
325335
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, Eq, Default)]
326336
#[serde(rename_all = "lowercase")]
327337
pub enum DevOrProd {

src/webserver/content_security_policy.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ pub struct ContentSecurityPolicy {
88
pub nonce: u64,
99
}
1010

11-
impl ContentSecurityPolicy {
12-
pub fn new() -> Self {
11+
impl Default for ContentSecurityPolicy {
12+
fn default() -> Self {
1313
Self { nonce: random() }
1414
}
1515
}

src/webserver/database/sqlpage_functions/functions.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ super::function_definition_macro::sqlpage_functions! {
2222
environment_variable(name: Cow<str>);
2323
exec((&RequestInfo), program_name: Cow<str>, args: Vec<Cow<str>>);
2424

25-
fetch(http_request: SqlPageFunctionParam<super::http_fetch_request::HttpFetchRequest<'_>>);
25+
fetch((&RequestInfo), http_request: SqlPageFunctionParam<super::http_fetch_request::HttpFetchRequest<'_>>);
2626

2727
hash_password(password: Option<String>);
2828
header((&RequestInfo), name: Cow<str>);
@@ -129,12 +129,12 @@ async fn exec<'a>(
129129
}
130130

131131
async fn fetch(
132+
request: &RequestInfo,
132133
http_request: super::http_fetch_request::HttpFetchRequest<'_>,
133134
) -> anyhow::Result<String> {
134135
use awc::http::Method;
135-
let client = awc::Client::builder()
136-
.add_default_header((awc::http::header::USER_AGENT, env!("CARGO_PKG_NAME")))
137-
.finish();
136+
let client = make_http_client(&request.app_state.config);
137+
138138
let method = if let Some(method) = http_request.method {
139139
Method::from_str(&method)?
140140
} else {
@@ -174,6 +174,27 @@ async fn fetch(
174174
Ok(response_str)
175175
}
176176

177+
fn make_http_client(config: &crate::app_config::AppConfig) -> awc::Client {
178+
let connector = if config.system_root_ca_certificates {
179+
let mut roots = rustls::RootCertStore::empty();
180+
for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs")
181+
{
182+
roots.add(cert).unwrap();
183+
}
184+
let tls_conf = rustls::ClientConfig::builder()
185+
.with_root_certificates(roots)
186+
.with_no_client_auth();
187+
188+
awc::Connector::new().rustls_0_22(std::sync::Arc::new(tls_conf))
189+
} else {
190+
awc::Connector::new()
191+
};
192+
awc::Client::builder()
193+
.connector(connector)
194+
.add_default_header((awc::http::header::USER_AGENT, env!("CARGO_PKG_NAME")))
195+
.finish()
196+
}
197+
177198
pub(crate) async fn hash_password(password: Option<String>) -> anyhow::Result<Option<String>> {
178199
let Some(password) = password else {
179200
return Ok(None);

src/webserver/http.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ async fn render_sql(
254254
actix_web::rt::spawn(async move {
255255
let request_context = RequestContext {
256256
is_embedded: req_param.get_variables.contains_key("_sqlpage_embed"),
257-
content_security_policy: ContentSecurityPolicy::new(),
257+
content_security_policy: ContentSecurityPolicy::default(),
258258
};
259259
let mut conn = None;
260260
let database_entries_stream =
@@ -598,15 +598,15 @@ pub async fn run_server(config: &AppConfig, state: AppState) -> anyhow::Result<(
598598
}
599599
}
600600

601-
let (r, _) = tokio::join!(server.run(), log_welcome_message(&config));
602-
r.with_context(|| "Unable to start the application")
601+
log_welcome_message(config);
602+
server.run().await.with_context(|| "Unable to start the application")
603603
}
604604

605-
async fn log_welcome_message(config: &AppConfig) {
605+
fn log_welcome_message(config: &AppConfig) {
606606
let address_message = if let Some(unix_socket) = &config.unix_socket {
607607
format!("unix socket {unix_socket:?}")
608608
} else if let Some(domain) = &config.https_domain {
609-
format!("https://{}", domain)
609+
format!("https://{domain}")
610610
} else {
611611
use std::fmt::Write;
612612
let listen_on = config.listen_on();

0 commit comments

Comments
 (0)