From e8ba798f82988fdddc4d7778fb364f279b6fe889 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Mon, 5 Aug 2024 18:34:35 +0200 Subject: [PATCH] add support for using the system's root ca certificates in sqlpage.fetch fixes https://github.com/lovasoa/SQLpage/issues/507 --- CHANGELOG.md | 1 + Cargo.lock | 63 +++++++++++++++++++ Cargo.toml | 2 + configuration.md | 1 + src/app_config.rs | 10 +++ src/webserver/content_security_policy.rs | 4 +- .../database/sqlpage_functions/functions.rs | 29 +++++++-- src/webserver/http.rs | 10 +-- 8 files changed, 109 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93c682d1..f5ca470a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - reduce the margin at the botton of forms to make them more compact. - fix [datagrid](https://sql.ophir.dev/documentation.sql?component=datagrid#component) color pills display when they contain long text. - fix the "started successfully" message being displayed before the error message when the server failed to start. + - 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. ## 0.25.0 (2024-07-13) diff --git a/Cargo.lock b/Cargo.lock index b3e8a2ed..f51b6ff1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -912,6 +912,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -2106,6 +2116,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "option-ext" version = "0.2.0" @@ -2614,6 +2630,19 @@ dependencies = [ "x509-parser", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "2.1.3" @@ -2647,12 +2676,44 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.23" @@ -2848,7 +2909,9 @@ dependencies = [ "password-hash", "percent-encoding", "rand", + "rustls", "rustls-acme", + "rustls-native-certs", "serde", "serde_json", "sqlparser", diff --git a/Cargo.toml b/Cargo.toml index 6b038f7f..836013e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,8 @@ base64 = "0.22" rustls-acme = "0.9.2" dotenvy = "0.15.7" csv-async = { version = "1.2.6", features = ["tokio"] } +rustls = { version = "0.22.0" } # keep in sync with actix-web, awc, rustls-acme, and sqlx +rustls-native-certs = "0.7.0" awc = { version = "3", features = ["rustls-0_22-webpki-roots"] } [build-dependencies] diff --git a/configuration.md b/configuration.md index 8c3d4089..18658f71 100644 --- a/configuration.md +++ b/configuration.md @@ -31,6 +31,7 @@ Here are the available configuration options and their default values: | `https_acme_directory_url` | https://acme-v02.api.letsencrypt.org/directory | The URL of the ACME directory to use when requesting a certificate. | | `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. | | `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. | +| `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. | Multiple configuration file formats are supported: 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. diff --git a/src/app_config.rs b/src/app_config.rs index 95e87d5e..daf20e43 100644 --- a/src/app_config.rs +++ b/src/app_config.rs @@ -100,6 +100,12 @@ pub struct AppConfig { /// 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 pub content_security_policy: Option, + + /// Whether `sqlpage.fetch` should load trusted certificates from the operating system's certificate store + /// By default, it loads Mozilla's root certificates that are embedded in the `SQLPage` binary, or the ones pointed to by the + /// `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables. + #[serde(default = "default_system_root_ca_certificates")] + pub system_root_ca_certificates: bool, } impl AppConfig { @@ -322,6 +328,10 @@ fn default_compress_responses() -> bool { true } +fn default_system_root_ca_certificates() -> bool { + std::env::var("SSL_CERT_FILE").is_ok() || std::env::var("SSL_CERT_DIR").is_ok() +} + #[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, Eq, Default)] #[serde(rename_all = "lowercase")] pub enum DevOrProd { diff --git a/src/webserver/content_security_policy.rs b/src/webserver/content_security_policy.rs index dab636c1..988ad2d8 100644 --- a/src/webserver/content_security_policy.rs +++ b/src/webserver/content_security_policy.rs @@ -8,8 +8,8 @@ pub struct ContentSecurityPolicy { pub nonce: u64, } -impl ContentSecurityPolicy { - pub fn new() -> Self { +impl Default for ContentSecurityPolicy { + fn default() -> Self { Self { nonce: random() } } } diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index 6ece4b9f..95be8682 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -22,7 +22,7 @@ super::function_definition_macro::sqlpage_functions! { environment_variable(name: Cow); exec((&RequestInfo), program_name: Cow, args: Vec>); - fetch(http_request: SqlPageFunctionParam>); + fetch((&RequestInfo), http_request: SqlPageFunctionParam>); hash_password(password: Option); header((&RequestInfo), name: Cow); @@ -129,12 +129,12 @@ async fn exec<'a>( } async fn fetch( + request: &RequestInfo, http_request: super::http_fetch_request::HttpFetchRequest<'_>, ) -> anyhow::Result { use awc::http::Method; - let client = awc::Client::builder() - .add_default_header((awc::http::header::USER_AGENT, env!("CARGO_PKG_NAME"))) - .finish(); + let client = make_http_client(&request.app_state.config); + let method = if let Some(method) = http_request.method { Method::from_str(&method)? } else { @@ -174,6 +174,27 @@ async fn fetch( Ok(response_str) } +fn make_http_client(config: &crate::app_config::AppConfig) -> awc::Client { + let connector = if config.system_root_ca_certificates { + let mut roots = rustls::RootCertStore::empty(); + for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") + { + roots.add(cert).unwrap(); + } + let tls_conf = rustls::ClientConfig::builder() + .with_root_certificates(roots) + .with_no_client_auth(); + + awc::Connector::new().rustls_0_22(std::sync::Arc::new(tls_conf)) + } else { + awc::Connector::new() + }; + awc::Client::builder() + .connector(connector) + .add_default_header((awc::http::header::USER_AGENT, env!("CARGO_PKG_NAME"))) + .finish() +} + pub(crate) async fn hash_password(password: Option) -> anyhow::Result> { let Some(password) = password else { return Ok(None); diff --git a/src/webserver/http.rs b/src/webserver/http.rs index ed445fc0..3d913fba 100644 --- a/src/webserver/http.rs +++ b/src/webserver/http.rs @@ -254,7 +254,7 @@ async fn render_sql( actix_web::rt::spawn(async move { let request_context = RequestContext { is_embedded: req_param.get_variables.contains_key("_sqlpage_embed"), - content_security_policy: ContentSecurityPolicy::new(), + content_security_policy: ContentSecurityPolicy::default(), }; let mut conn = None; let database_entries_stream = @@ -598,15 +598,15 @@ pub async fn run_server(config: &AppConfig, state: AppState) -> anyhow::Result<( } } - let (r, _) = tokio::join!(server.run(), log_welcome_message(&config)); - r.with_context(|| "Unable to start the application") + log_welcome_message(config); + server.run().await.with_context(|| "Unable to start the application") } -async fn log_welcome_message(config: &AppConfig) { +fn log_welcome_message(config: &AppConfig) { let address_message = if let Some(unix_socket) = &config.unix_socket { format!("unix socket {unix_socket:?}") } else if let Some(domain) = &config.https_domain { - format!("https://{}", domain) + format!("https://{domain}") } else { use std::fmt::Write; let listen_on = config.listen_on();