diff --git a/Cargo.lock b/Cargo.lock index bd307b64..3307b07f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1486,6 +1486,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -1531,6 +1537,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "gloo-net" version = "0.2.6" @@ -2568,6 +2580,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -2683,6 +2704,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "ring" version = "0.17.8" @@ -2736,6 +2763,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rstest" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.96", + "unicode-ident", +] + [[package]] name = "rust-ini" version = "0.21.1" @@ -3115,6 +3172,7 @@ dependencies = [ "password-hash", "percent-encoding", "rand", + "rstest", "rustls", "rustls-acme", "rustls-native-certs", diff --git a/Cargo.toml b/Cargo.toml index e5a50acb..4a85c5eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,3 +69,6 @@ awc = { version = "3", features = ["rustls-0_22-webpki-roots"] } actix-rt = "2.8" libflate = "2" futures-util = "0.3.21" + +[dev-dependencies] +rstest = "0.24.0" diff --git a/configuration.md b/configuration.md index f7782f85..cb83063d 100644 --- a/configuration.md +++ b/configuration.md @@ -6,34 +6,35 @@ or a [JSON](https://en.wikipedia.org/wiki/JSON) file placed in `sqlpage/sqlpage. You can find an example configuration file in [`sqlpage/sqlpage.json`](./sqlpage/sqlpage.json). Here are the available configuration options and their default values: -| variable | default | description | -| --------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `listen_on` | 0.0.0.0:8080 | Interface and port on which the web server should listen | -| `database_url` | sqlite://sqlpage.db?mode=rwc | Database connection URL, in the form `dbengine://user:password@host:port/dbname`. Special characters in user and password should be [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding). | -| `database_password` | | Database password. If set, this will override any password specified in the `database_url`. This allows you to keep the password separate from the connection string for better security. | -| `port` | 8080 | Like listen_on, but specifies only the port. | -| `unix_socket` | | Path to a UNIX socket to listen on instead of the TCP port. If specified, SQLPage will accept HTTP connections only on this socket and not on any TCP port. This option is mutually exclusive with `listen_on` and `port`. -| `max_database_pool_connections` | PostgreSQL: 50
MySql: 75
SQLite: 16
MSSQL: 100 | How many simultaneous database connections to open at most | -| `database_connection_idle_timeout_seconds` | SQLite: None
All other: 30 minutes | Automatically close database connections after this period of inactivity | -| `database_connection_max_lifetime_seconds` | SQLite: None
All other: 60 minutes | Always close database connections after this amount of time | -| `database_connection_retries` | 6 | Database connection attempts before giving up. Retries will happen every 5 seconds. | -| `database_connection_acquire_timeout_seconds` | 10 | How long to wait when acquiring a database connection from the pool before giving up and returning an error. | -| `sqlite_extensions` | | An array of SQLite extensions to load, such as `mod_spatialite` | -| `web_root` | `.` | The root directory of the web server, where the `index.sql` file is located. | -| `site_prefix` | `/` | Base path of the site. If you want to host SQLPage at `https://example.com/sqlpage/`, set this to `/sqlpage/`. When using a reverse proxy, this allows hosting SQLPage together with other applications on the same subdomain. | +| variable | default | description | +| --------------------------------------------- |-------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `listen_on` | 0.0.0.0:8080 | Interface and port on which the web server should listen | +| `database_url` | sqlite://sqlpage.db?mode=rwc | Database connection URL, in the form `dbengine://user:password@host:port/dbname`. Special characters in user and password should be [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding). | +| `database_password` | | Database password. If set, this will override any password specified in the `database_url`. This allows you to keep the password separate from the connection string for better security. | +| `port` | 8080 | Like listen_on, but specifies only the port. | +| `unix_socket` | | Path to a UNIX socket to listen on instead of the TCP port. If specified, SQLPage will accept HTTP connections only on this socket and not on any TCP port. This option is mutually exclusive with `listen_on` and `port`. +| `max_database_pool_connections` | PostgreSQL: 50
MySql: 75
SQLite: 16
MSSQL: 100 | How many simultaneous database connections to open at most | +| `database_connection_idle_timeout_seconds` | SQLite: None
All other: 30 minutes | Automatically close database connections after this period of inactivity | +| `database_connection_max_lifetime_seconds` | SQLite: None
All other: 60 minutes | Always close database connections after this amount of time | +| `database_connection_retries` | 6 | Database connection attempts before giving up. Retries will happen every 5 seconds. | +| `database_connection_acquire_timeout_seconds` | 10 | How long to wait when acquiring a database connection from the pool before giving up and returning an error. | +| `sqlite_extensions` | | An array of SQLite extensions to load, such as `mod_spatialite` | +| `web_root` | `.` | The root directory of the web server, where the `index.sql` file is located. | +| `site_prefix` | `/` | Base path of the site. If you want to host SQLPage at `https://example.com/sqlpage/`, set this to `/sqlpage/`. When using a reverse proxy, this allows hosting SQLPage together with other applications on the same subdomain. | +| `suppress_file_extensions` | false | Supress the file extension when using the `sqlpage.link` function. Useful if behind a rewriting proxy and want to suppress urls containing `.sql`. | | `configuration_directory` | `./sqlpage/` | The directory where the `sqlpage.json` file is located. This is used to find the path to [`templates/`](https://sql-page.com/custom_components.sql), [`migrations/`](https://sql-page.com/your-first-sql-website/migrations.sql), and `on_connect.sql`. Obviously, this configuration parameter can be set only through environment variables, not through the `sqlpage.json` file itself in order to find the `sqlpage.json` file. Be careful not to use a path that is accessible from the public WEB_ROOT | -| `allow_exec` | false | Allow usage of the `sqlpage.exec` function. Do this only if all users with write access to sqlpage query files and to the optional `sqlpage_files` table on the database are trusted. | -| `max_uploaded_file_size` | 5242880 | Maximum size of forms and uploaded files in bytes. Defaults to 5 MiB. | -| `max_pending_rows` | 256 | Maximum number of rendered rows that can be queued up in memory when a client is slow to receive them. | -| `compress_responses` | true | When the client supports it, compress the http response body. This can save bandwidth and speed up page loading on slow connections, but can also increase CPU usage and cause rendering delays on pages that take time to render (because streaming responses are buffered for longer than necessary). | -| `https_domain` | | Domain name to request a certificate for. Setting this parameter will automatically make SQLPage listen on port 443 and request an SSL certificate. The server will take a little bit longer to start the first time it has to request a certificate. | -| `https_certificate_email` | contact@ | The email address to use when requesting a certificate. | -| `https_certificate_cache_dir` | ./sqlpage/https | A writeable directory where to cache the certificates, so that SQLPage can serve https traffic immediately when it restarts. | -| `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` | `script-src 'self' 'nonce-XXX` | 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. | -| `max_recursion_depth` | 10 | Maximum depth of recursion allowed in the `run_sql` function. Maximum value is 255. | +| `allow_exec` | false | Allow usage of the `sqlpage.exec` function. Do this only if all users with write access to sqlpage query files and to the optional `sqlpage_files` table on the database are trusted. | +| `max_uploaded_file_size` | 5242880 | Maximum size of forms and uploaded files in bytes. Defaults to 5 MiB. | +| `max_pending_rows` | 256 | Maximum number of rendered rows that can be queued up in memory when a client is slow to receive them. | +| `compress_responses` | true | When the client supports it, compress the http response body. This can save bandwidth and speed up page loading on slow connections, but can also increase CPU usage and cause rendering delays on pages that take time to render (because streaming responses are buffered for longer than necessary). | +| `https_domain` | | Domain name to request a certificate for. Setting this parameter will automatically make SQLPage listen on port 443 and request an SSL certificate. The server will take a little bit longer to start the first time it has to request a certificate. | +| `https_certificate_email` | contact@ | The email address to use when requesting a certificate. | +| `https_certificate_cache_dir` | ./sqlpage/https | A writeable directory where to cache the certificates, so that SQLPage can serve https traffic immediately when it restarts. | +| `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` | `script-src 'self' 'nonce-XXX` | 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. | +| `max_recursion_depth` | 10 | Maximum depth of recursion allowed in the `run_sql` function. Maximum value is 255. | 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 21c59441..199a2184 100644 --- a/src/app_config.rs +++ b/src/app_config.rs @@ -225,6 +225,12 @@ pub struct AppConfig { )] pub site_prefix: String, + /// Setting this to `true` will cause the sqlpage link function to omit the file extension when + /// generating links (e.g. `/path/to/your/file` instead of `/path/to/your/file.sql`). + /// Use this if running behind a reverse proxy with appropriate rewrite rules. + #[serde(default)] + pub suppress_file_extensions: bool, + /// Maximum number of messages that can be stored in memory before sending them to the client. /// This prevents a single request from using up all available memory. #[serde(default = "default_max_pending_rows")] diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index f9996b58..30a20dcf 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -27,7 +27,7 @@ super::function_definition_macro::sqlpage_functions! { hash_password(password: Option); header((&RequestInfo), name: Cow); - link(file: Cow, parameters: Option>, hash: Option>); + link((&RequestInfo), file: Cow, parameters: Option>, hash: Option>); path((&RequestInfo)); persist_uploaded_file((&RequestInfo), field_name: Cow, folder: Option>, allowed_extensions: Option>); @@ -262,15 +262,32 @@ async fn header<'a>(request: &'a RequestInfo, name: Cow<'a, str>) -> Option( + request: &'a RequestInfo, + file: Cow<'a, str>, + parameters: Option>, + hash: Option>, +) -> anyhow::Result { + let suppress_file_extensions = request.app_state.config.suppress_file_extensions; + do_link(file, parameters, hash, suppress_file_extensions).await +} + +async fn do_link<'a>( file: Cow<'a, str>, parameters: Option>, hash: Option>, + suppress_file_extensions: bool, ) -> anyhow::Result { let mut url = file.into_owned(); + if suppress_file_extensions { + url = url + .strip_suffix(".sql") + .ok_or(anyhow!("Unable to strip file suffix"))? + .parse()? + } if let Some(parameters) = parameters { url.push('?'); let encoded = serde_json::from_str::(¶meters).with_context(|| { @@ -604,3 +621,56 @@ async fn variables<'a>( async fn version() -> &'static str { env!("CARGO_PKG_VERSION") } + +#[cfg(test)] +mod tests { + use super::do_link; + use rstest::rstest; + use std::borrow::Cow; + + #[tokio::test] + #[rstest] + #[case::bare("/some/path/to/file.sql", None, None, false, "/some/path/to/file.sql")] + #[case::single_parameter( + "/some/path/to/file.sql", + Some(Cow::from(r#"{ "q":"search" }"#)), + None, + false, + "/some/path/to/file.sql?q=search" + )] + #[case::two_parameters( + "/some/path/to/file.sql", + Some(Cow::from(r#"{ "q":"search", "limit":100 }"#)), + None, + false, + "/some/path/to/file.sql?q=search&limit=100" + )] + #[case::hash( + "/some/path/to/file.sql", + None, + Some(Cow::from("anchor1")), + false, + "/some/path/to/file.sql#anchor1" + )] + #[case::suppress_file_extensions( + "/some/path/to/file.sql", + None, + None, + true, + "/some/path/to/file" + )] + async fn link_spec( + #[case] filename: &str, + #[case] parameters: Option>, + #[case] hash: Option>, + #[case] suppress_file_extensions: bool, + #[case] expected: &str, + ) { + let file = Cow::from(filename); + let actual = do_link(file, parameters, hash, suppress_file_extensions) + .await + .unwrap(); + + assert_eq!(actual, String::from(expected)); + } +}