From 3f73964617a7ee723feea914fa98c52d871a4e90 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Mon, 18 Sep 2023 10:54:23 +0200 Subject: [PATCH] fix #89 --- sqlpage/sqlpage.json | 2 +- src/file_cache.rs | 7 +++++-- src/filesystem.rs | 44 +++++++++++++++++++++++++++++++++---------- src/webserver/http.rs | 4 ++-- tests/index.rs | 22 +++++++++++++--------- 5 files changed, 55 insertions(+), 24 deletions(-) diff --git a/sqlpage/sqlpage.json b/sqlpage/sqlpage.json index 9f4ae491..3f0f0b0b 100644 --- a/sqlpage/sqlpage.json +++ b/sqlpage/sqlpage.json @@ -1,4 +1,4 @@ { "database_url": "sqlite://./sqlpage.db?mode=rwc", - "listen_on": "localhost:8081" + "listen_on": "localhost:8080" } \ No newline at end of file diff --git a/src/file_cache.rs b/src/file_cache.rs index e1706fcd..d420019f 100644 --- a/src/file_cache.rs +++ b/src/file_cache.rs @@ -101,7 +101,7 @@ impl FileCache { } match app_state .file_system - .modified_since(app_state, path, cached.last_check_time()) + .modified_since(app_state, path, cached.last_check_time(), true) .await { Ok(false) => { @@ -115,7 +115,10 @@ impl FileCache { } // Read lock is released log::trace!("Loading and parsing {:?}", path); - let file_contents = app_state.file_system.read_to_string(app_state, path).await; + let file_contents = app_state + .file_system + .read_to_string(app_state, path, true) + .await; let parsed = match file_contents { Ok(contents) => { diff --git a/src/filesystem.rs b/src/filesystem.rs index b81f6622..b35c4b64 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -6,6 +6,7 @@ use chrono::{DateTime, Utc}; use sqlx::any::{AnyKind, AnyStatement, AnyTypeInfo}; use sqlx::postgres::types::PgTimeTz; use sqlx::{Postgres, Statement, Type}; +use std::borrow::Cow; use std::io::ErrorKind; use std::path::{Component, Path, PathBuf}; @@ -37,8 +38,13 @@ impl FileSystem { app_state: &AppState, path: &Path, since: DateTime, + priviledged: bool, ) -> anyhow::Result { - let local_path = self.safe_local_path(path)?; + let local_path = if priviledged { + Cow::Borrowed(path) + } else { + Cow::Owned(self.safe_local_path(path)?) + }; let local_result = file_modified_since_local(&local_path, since).await; match (local_result, &self.db_fs_queries) { (Ok(modified), _) => Ok(modified), @@ -58,14 +64,27 @@ impl FileSystem { &self, app_state: &AppState, path: &Path, + priviledged: bool, ) -> anyhow::Result { - let bytes = self.read_file(app_state, path).await?; + let bytes = self.read_file(app_state, path, priviledged).await?; String::from_utf8(bytes) .with_context(|| format!("The file at {path:?} contains invalid UTF8 characters")) } - pub async fn read_file(&self, app_state: &AppState, path: &Path) -> anyhow::Result> { - let local_path = self.safe_local_path(path)?; + /** + * Priviledged files are the ones that are in sqlpage's config directory. + */ + pub async fn read_file( + &self, + app_state: &AppState, + path: &Path, + priviledged: bool, + ) -> anyhow::Result> { + let local_path = if priviledged { + Cow::Borrowed(path) + } else { + Cow::Owned(self.safe_local_path(path)?) + }; let local_result = tokio::fs::read(&local_path).await; match (local_result, &self.db_fs_queries) { (Ok(f), _) => Ok(f), @@ -82,11 +101,16 @@ impl FileSystem { } fn safe_local_path(&self, path: &Path) -> anyhow::Result { - for component in path.components() { - anyhow::ensure!( - matches!(component, Component::Normal(_)), - "Unsupported path: {path:?}. Path component {component:?} is not allowed." - ); + for (i, component) in path.components().enumerate() { + if let Component::Normal(c) = component { + if c.eq_ignore_ascii_case("sqlpage") && i == 0 { + anyhow::bail!("Access to the sqlpage config directory is not allowed."); + } + } else { + anyhow::bail!( + "Unsupported path: {path:?}. Path component '{component:?}' is not allowed." + ); + } } Ok(self.local_root.join(path)) } @@ -202,7 +226,7 @@ async fn test_sql_file_read_utf8() -> anyhow::Result<()> { .await?; let fs = FileSystem::init("/", &state.db).await; let actual = fs - .read_to_string(&state, "unit test file.txt".as_ref()) + .read_to_string(&state, "unit test file.txt".as_ref(), false) .await?; assert_eq!(actual, "Héllö world! 😀"); Ok(()) diff --git a/src/webserver/http.rs b/src/webserver/http.rs index e959c867..bfa8d77d 100644 --- a/src/webserver/http.rs +++ b/src/webserver/http.rs @@ -439,7 +439,7 @@ async fn serve_file( let since = DateTime::::from(SystemTime::from(date)); let modified = state .file_system - .modified_since(state, path.as_ref(), since) + .modified_since(state, path.as_ref(), since, false) .await .map_err(actix_web::error::ErrorBadRequest)?; if !modified { @@ -448,7 +448,7 @@ async fn serve_file( } state .file_system - .read_file(state, path.as_ref()) + .read_file(state, path.as_ref(), false) .await .map_err(actix_web::error::ErrorBadRequest) .map(|b| { diff --git a/tests/index.rs b/tests/index.rs index 1089abcb..b27e111d 100644 --- a/tests/index.rs +++ b/tests/index.rs @@ -6,23 +6,27 @@ use sqlpage::{app_config::AppConfig, webserver::http::main_handler, AppState}; #[actix_web::test] async fn test_index_ok() { + let resp = req_path("/").await; + assert_eq!(resp.status(), http::StatusCode::OK); + let body = test::read_body(resp).await; + assert!(body.starts_with(b"")); + // the body should contain the strint "It works!" and should not contain the string "error" + let body = String::from_utf8(body.to_vec()).unwrap(); + assert!(body.contains("It works !")); + assert!(!body.contains("error")); +} + +async fn req_path(path: &str) -> actix_web::dev::ServiceResponse { init_log(); let config = test_config(); let state = AppState::init(&config).await.unwrap(); let data = actix_web::web::Data::new(state); let req = test::TestRequest::get() - .uri("/") + .uri(path) .app_data(data) .insert_header(ContentType::plaintext()) .to_srv_request(); - let resp = main_handler(req).await.unwrap(); - assert_eq!(resp.status(), http::StatusCode::OK); - let body = test::read_body(resp).await; - assert!(body.starts_with(b"")); - // the body should contain the strint "It works!" and should not contain the string "error" - let body = String::from_utf8(body.to_vec()).unwrap(); - assert!(body.contains("It works !")); - assert!(!body.contains("error")); + main_handler(req).await.unwrap() } pub fn test_config() -> AppConfig {