Skip to content

Commit

Permalink
add support for using the system's root ca certificates in sqlpage.fetch
Browse files Browse the repository at this point in the history
fixes #507
  • Loading branch information
lovasoa committed Aug 5, 2024
1 parent 6cc4cce commit e8ba798
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
63 changes: 63 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions src/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// 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 {
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/webserver/content_security_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
}
}
Expand Down
29 changes: 25 additions & 4 deletions src/webserver/database/sqlpage_functions/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ super::function_definition_macro::sqlpage_functions! {
environment_variable(name: Cow<str>);
exec((&RequestInfo), program_name: Cow<str>, args: Vec<Cow<str>>);

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

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

async fn fetch(
request: &RequestInfo,
http_request: super::http_fetch_request::HttpFetchRequest<'_>,
) -> anyhow::Result<String> {
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 {
Expand Down Expand Up @@ -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<String>) -> anyhow::Result<Option<String>> {
let Some(password) = password else {
return Ok(None);
Expand Down
10 changes: 5 additions & 5 deletions src/webserver/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit e8ba798

Please sign in to comment.