Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
- ![screenshot](https://github.com/lovasoa/SQLpage/assets/552629/df085592-8359-4fed-9aeb-27a2416ab6b8)
- **Multiple page layouts** : The page layout is now configurable from the [shell component](https://sql.ophir.dev/documentation.sql?component=shell#component). 3 layouts are available: `boxed` (the default), `fluid` (full width), and `horizontal` (with boxed contents but a full-width header).
- ![horizontal layout screenshot](https://github.com/lovasoa/SQLpage/assets/552629/3c0fde36-7bf6-414e-b96f-c8880a2fc786)
- New `SQLPAGE_CONFIGURATION_DIRECTORY` environment variable to set the configuration directory from the environment.
The configuration directory is where SQLPage looks for the `sqlpage.json` configuration file, for the `migrations` and `templates` directories, and the `on_connect.sql` file. It used to be hardcoded to `./sqlpage/`, which made each SQLPage invokation dependent on the [current working directory](https://en.wikipedia.org/wiki/Working_directory).
Now you can, for instance, set `SQLPAGE_CONFIGURATION_DIRECTORY=/etc/sqlpage/` in your environment, and SQLPage will look for its configuration files in `/etc/sqlpage`, which is a more standard location for configuration files in a Unix environment.
- The official docker image now sets `SQLPAGE_CONFIGURATION_DIRECTORY=/etc/sqlpage/` by default, and changes the working directory to `/var/www/` by default.

## 0.18.3 (2024-02-03)

Expand Down
8 changes: 6 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ RUN touch src/main.rs && \

FROM busybox:glibc
RUN addgroup --gid 1000 --system sqlpage && \
adduser --uid 1000 --system --no-create-home --ingroup sqlpage sqlpage
adduser --uid 1000 --system --no-create-home --ingroup sqlpage sqlpage && \
mkdir -p /etc/sqlpage && \
touch /etc/sqlpage/sqlpage.db && \
chown -R sqlpage:sqlpage /etc/sqlpage/sqlpage.db
ENV SQLPAGE_WEB_ROOT=/var/www
WORKDIR /etc
ENV SQLPAGE_CONFIGURATION_DIRECTORY=/etc/sqlpage
WORKDIR /var/www
COPY --from=builder /usr/src/sqlpage/sqlpage.bin /usr/local/bin/sqlpage
COPY --from=builder /usr/src/sqlpage/libgcc_s.so.1 /lib/libgcc_s.so.1
USER sqlpage
Expand Down
1 change: 1 addition & 0 deletions configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Here are the available configuration options and their default values:
| `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. |
| `configuration_directory` | `./sqlpage/` | The directory where the `sqlpage.json` file is located. This is used to find the path to `templates/`, `migrations/`, 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. |
| `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 uploaded files in bytes. Defaults to 5 MiB. |
| `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. |
Expand Down
35 changes: 28 additions & 7 deletions src/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ use serde::{Deserialize, Deserializer};
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::PathBuf;

#[cfg(not(feature = "lambda-web"))]
const DEFAULT_DATABASE_DIR: &str = "sqlpage";
#[cfg(not(feature = "lambda-web"))]
const DEFAULT_DATABASE_FILE: &str = "sqlpage.db";

Expand Down Expand Up @@ -36,9 +34,14 @@ pub struct AppConfig {
#[serde(default = "default_database_connection_acquire_timeout_seconds")]
pub database_connection_acquire_timeout_seconds: f64,

/// The directory where the .sql files are located. Defaults to the current directory.
#[serde(default = "default_web_root")]
pub web_root: PathBuf,

/// The directory where the sqlpage configuration file is located. Defaults to `./sqlpage`.
#[serde(default = "configuration_directory")]
pub configuration_directory: PathBuf,

/// Set to true to allow the `sqlpage.exec` function to be used in SQL queries.
/// This should be enabled only if you trust the users writing SQL queries, since it gives
/// them the ability to execute arbitrary shell commands on the server.
Expand Down Expand Up @@ -93,9 +96,27 @@ impl AppConfig {
}
}

/// The directory where the `sqlpage.json` file is located.
/// Determined by the `SQLPAGE_CONFIGURATION_DIRECTORY` environment variable
fn configuration_directory() -> PathBuf {
std::env::var("SQLPAGE_CONFIGURATION_DIRECTORY")
.or_else(|_| std::env::var("CONFIGURATION_DIRECTORY"))
.map_or_else(|_| PathBuf::from("sqlpage"), PathBuf::from)
}

fn cannonicalize_if_possible(path: &std::path::Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_owned())
}

pub fn load() -> anyhow::Result<AppConfig> {
let configuration_directory = &configuration_directory();
log::debug!(
"Loading configuration from {:?}",
cannonicalize_if_possible(configuration_directory)
);
let config_file = configuration_directory.join("sqlpage");
Config::builder()
.add_source(config::File::with_name("sqlpage/sqlpage").required(false))
.add_source(config::File::from(config_file).required(false))
.add_source(env_config())
.add_source(env_config().prefix("SQLPAGE"))
.build()?
Expand Down Expand Up @@ -135,14 +156,14 @@ fn default_database_url() -> String {

#[cfg(not(feature = "lambda-web"))]
{
let cwd = std::env::current_dir().unwrap_or_default();
let old_default_db_path = cwd.join(DEFAULT_DATABASE_FILE);
let default_db_path = cwd.join(DEFAULT_DATABASE_DIR).join(DEFAULT_DATABASE_FILE);
let config_dir = cannonicalize_if_possible(&configuration_directory());
let old_default_db_path = PathBuf::from(DEFAULT_DATABASE_FILE);
let default_db_path = config_dir.join(DEFAULT_DATABASE_FILE);
if let Ok(true) = old_default_db_path.try_exists() {
log::warn!("Your sqlite database in {old_default_db_path:?} is publicly accessible through your web server. Please move it to {default_db_path:?}.");
return prefix + old_default_db_path.to_str().unwrap();
} else if let Ok(true) = default_db_path.try_exists() {
log::debug!("Using the default datbase file in {default_db_path:?}.");
log::debug!("Using the default database file in {default_db_path:?}.");
return prefix + default_db_path.to_str().unwrap();
}
// Create the default database file if we can
Expand Down
47 changes: 34 additions & 13 deletions src/filesystem.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::webserver::ErrorWithStatus;
use crate::webserver::{make_placeholder, Database};
use crate::AppState;
use crate::{AppState, TEMPLATES_DIR};
use anyhow::Context;
use chrono::{DateTime, Utc};
use sqlx::any::{AnyKind, AnyStatement, AnyTypeInfo};
Expand Down Expand Up @@ -39,7 +39,7 @@ impl FileSystem {
since: DateTime<Utc>,
priviledged: bool,
) -> anyhow::Result<bool> {
let local_path = self.safe_local_path(path, priviledged)?;
let local_path = self.safe_local_path(app_state, path, priviledged)?;
let local_result = file_modified_since_local(&local_path, since).await;
match (local_result, &self.db_fs_queries) {
(Ok(modified), _) => Ok(modified),
Expand Down Expand Up @@ -75,7 +75,7 @@ impl FileSystem {
path: &Path,
priviledged: bool,
) -> anyhow::Result<Vec<u8>> {
let local_path = self.safe_local_path(path, priviledged)?;
let local_path = self.safe_local_path(app_state, path, priviledged)?;
let local_result = tokio::fs::read(&local_path).await;
match (local_result, &self.db_fs_queries) {
(Ok(f), _) => Ok(f),
Expand All @@ -91,18 +91,39 @@ impl FileSystem {
}
}

fn safe_local_path(&self, path: &Path, priviledged: bool) -> anyhow::Result<PathBuf> {
for (i, component) in path.components().enumerate() {
if let Component::Normal(c) = component {
if !priviledged && i == 0 && c.eq_ignore_ascii_case("sqlpage") {
anyhow::bail!(ErrorWithStatus {
status: actix_web::http::StatusCode::FORBIDDEN,
});
}
} else {
anyhow::bail!(
fn safe_local_path(
&self,
app_state: &AppState,
path: &Path,
priviledged: bool,
) -> anyhow::Result<PathBuf> {
if priviledged {
// Templates requests are always made to the static TEMPLATES_DIR, because this is where they are stored in the database
// but when serving them from the filesystem, we need to serve them from the `SQLPAGE_CONFIGURATION_DIRECTORY/templates` directory
if let Ok(template_path) = path.strip_prefix(TEMPLATES_DIR) {
let normalized = [
&app_state.config.configuration_directory,
Path::new("templates"),
template_path,
]
.iter()
.collect();
log::trace!("Normalizing template path {path:?} to {normalized:?}");
return Ok(normalized);
}
} else {
for (i, component) in path.components().enumerate() {
if let Component::Normal(c) = component {
if i == 0 && c.eq_ignore_ascii_case("sqlpage") {
anyhow::bail!(ErrorWithStatus {
status: actix_web::http::StatusCode::FORBIDDEN,
});
}
} else {
anyhow::bail!(
"Unsupported path: {path:?}. Path component '{component:?}' is not allowed."
);
}
}
}
Ok(self.local_root.join(path))
Expand Down
9 changes: 6 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ use std::path::PathBuf;
use templates::AllTemplates;
use webserver::Database;

pub const TEMPLATES_DIR: &str = "sqlpage/templates";
pub const MIGRATIONS_DIR: &str = "sqlpage/migrations";
pub const ON_CONNECT_FILE: &str = "sqlpage/on_connect.sql";
/// `TEMPLATES_DIR` is the directory where .handlebars files are stored
/// When a template is requested, it is looked up in `sqlpage/templates/component_name.handlebars` in the database,
/// or in `$SQLPAGE_CONFIGURATION_DIRECTORY/templates/component_name.handlebars` in the filesystem.
pub const TEMPLATES_DIR: &str = "sqlpage/templates/";
pub const MIGRATIONS_DIR: &str = "migrations";
pub const ON_CONNECT_FILE: &str = "on_connect.sql";

pub struct AppState {
pub db: Database,
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async fn start() -> anyhow::Result<()> {
let app_config = app_config::load()?;
log::debug!("Starting with the following configuration: {app_config:#?}");
let state = AppState::init(&app_config).await?;
webserver::database::migrations::apply(&state.db).await?;
webserver::database::migrations::apply(&app_config, &state.db).await?;
log::debug!("Starting server...");
let (r, _) = tokio::join!(
webserver::http::run_server(&app_config, state),
Expand Down
2 changes: 1 addition & 1 deletion src/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ impl AllTemplates {
) -> anyhow::Result<Arc<SplitTemplate>> {
use anyhow::Context;
let mut path: PathBuf =
PathBuf::with_capacity(TEMPLATES_DIR.len() + name.len() + ".handlebars".len() + 2);
PathBuf::with_capacity(TEMPLATES_DIR.len() + 1 + name.len() + ".handlebars".len());
path.push(TEMPLATES_DIR);
path.push(name);
path.set_extension("handlebars");
Expand Down
11 changes: 6 additions & 5 deletions src/webserver/database/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,16 @@ impl Database {
.acquire_timeout(Duration::from_secs_f64(
config.database_connection_acquire_timeout_seconds,
));
pool_options = add_on_connection_handler(pool_options);
pool_options = add_on_connection_handler(config, pool_options);
pool_options
}
}

fn add_on_connection_handler(pool_options: PoolOptions<Any>) -> PoolOptions<Any> {
let on_connect_file = std::env::current_dir()
.unwrap_or_default()
.join(ON_CONNECT_FILE);
fn add_on_connection_handler(
config: &AppConfig,
pool_options: PoolOptions<Any>,
) -> PoolOptions<Any> {
let on_connect_file = config.configuration_directory.join(ON_CONNECT_FILE);
if !on_connect_file.exists() {
log::debug!("Not creating a custom SQL database connection handler because {on_connect_file:?} does not exist");
return pool_options;
Expand Down
6 changes: 2 additions & 4 deletions src/webserver/database/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ use sqlx::migrate::MigrateError;
use sqlx::migrate::Migration;
use sqlx::migrate::Migrator;

pub async fn apply(db: &Database) -> anyhow::Result<()> {
let migrations_dir = std::env::current_dir()
.unwrap_or_default()
.join(MIGRATIONS_DIR);
pub async fn apply(config: &crate::app_config::AppConfig, db: &Database) -> anyhow::Result<()> {
let migrations_dir = config.configuration_directory.join(MIGRATIONS_DIR);
if !migrations_dir.exists() {
log::info!(
"Not applying database migrations because '{}' does not exist",
Expand Down
4 changes: 2 additions & 2 deletions tests/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ async fn make_app_data() -> actix_web::web::Data<AppState> {
init_log();
let config = test_config();
let state = AppState::init(&config).await.unwrap();
let data = actix_web::web::Data::new(state);
data

actix_web::web::Data::new(state)
}

async fn req_path(
Expand Down