Skip to content

Commit 3551cfd

Browse files
committed
fix special characters in site_prefix
fix #689
1 parent e438109 commit 3551cfd

File tree

3 files changed

+54
-16
lines changed

3 files changed

+54
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
- Adds support for `ANY`, `ALL`, and `SOME` subqueries, like `SELECT * FROM t WHERE a = ANY (SELECT b FROM t2)`
3232
- Add support for `change_percent` without `description` in the big_number component to display the percentage change of a value.
3333
- Add support for `freeze_columns` and `freeze_headers` in the table component to freeze columns and headers.
34+
- Fix an error that occured when the site_prefix configuration option was set to a value containing special characters like `-` and a request was made directly to the server without url-encoding the site prefix.
3435

3536
## 0.30.1 (2024-10-31)
3637
- fix a bug where table sorting would break if table search was not also enabled.

src/app_config.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,16 +348,24 @@ fn deserialize_site_prefix<'de, D: Deserializer<'de>>(deserializer: D) -> Result
348348
/// We also percent-encode special characters in the prefix, but allow it to contain slashes (to allow
349349
/// hosting on a sub-sub-path).
350350
fn normalize_site_prefix(prefix: &str) -> String {
351-
const TO_ENCODE: AsciiSet = percent_encoding::NON_ALPHANUMERIC.remove(b'/');
351+
const TO_ENCODE: AsciiSet = percent_encoding::CONTROLS
352+
.add(b' ')
353+
.add(b'"')
354+
.add(b'#')
355+
.add(b'<')
356+
.add(b'>')
357+
.add(b'?');
352358

353359
let prefix = prefix.trim_start_matches('/').trim_end_matches('/');
354360
if prefix.is_empty() {
355361
return default_site_prefix();
356362
}
357363
let encoded_prefix = percent_encoding::percent_encode(prefix.as_bytes(), &TO_ENCODE);
358364

365+
let invalid_chars = ["%09", "%0A", "%0D"];
366+
359367
std::iter::once("/")
360-
.chain(encoded_prefix)
368+
.chain(encoded_prefix.filter(|c| !invalid_chars.contains(c)))
361369
.chain(std::iter::once("/"))
362370
.collect::<String>()
363371
}
@@ -550,6 +558,13 @@ mod test {
550558
assert_eq!(normalize_site_prefix("a/b/c"), "/a/b/c/");
551559
assert_eq!(normalize_site_prefix("a b"), "/a%20b/");
552560
assert_eq!(normalize_site_prefix("a b/c"), "/a%20b/c/");
561+
assert_eq!(normalize_site_prefix("*-+/:;,?%\"'{"), "/*-+/:;,%3F%%22'{/");
562+
assert_eq!(
563+
normalize_site_prefix(
564+
&(0..=0x7F).map(|b| char::from(b)).collect::<String>()
565+
),
566+
"/%00%01%02%03%04%05%06%07%08%0B%0C%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20!%22%23$%&'()*+,-./0123456789:;%3C=%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~%7F/"
567+
);
553568
}
554569

555570
#[test]

src/webserver/http.rs

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ async fn serve_file(
314314
state: &AppState,
315315
if_modified_since: Option<IfModifiedSince>,
316316
) -> actix_web::Result<HttpResponse> {
317-
let path = path.strip_prefix(&state.config.site_prefix).unwrap_or(path);
317+
let path = strip_site_prefix(path, state);
318318
if let Some(IfModifiedSince(date)) = if_modified_since {
319319
let since = DateTime::<Utc>::from(SystemTime::from(date));
320320
let modified = state
@@ -345,6 +345,11 @@ async fn serve_file(
345345
})
346346
}
347347

348+
/// Strips the site prefix from a path
349+
fn strip_site_prefix<'a>(path: &'a str, state: &AppState) -> &'a str {
350+
path.strip_prefix(&state.config.site_prefix).unwrap_or(path)
351+
}
352+
348353
/// Fallback handler for when a file could not be served
349354
///
350355
/// Recursively traverses upwards in the request's path, looking for a `404.sql` to call as the
@@ -361,7 +366,7 @@ async fn serve_fallback(
361366

362367
let app_state: &web::Data<AppState> = service_request.app_data().expect("app_state");
363368

364-
// Find the indeces of each char which follows a directroy separator (`/`). Also consider the 0
369+
// Find the indices of each char which follows a directroy separator (`/`). Also consider the 0
365370
// index, as we also have to try to check the empty path, for a root dir `404.sql`.
366371
for idx in req_path
367372
.rmatch_indices('/')
@@ -408,6 +413,10 @@ async fn serve_fallback(
408413
pub async fn main_handler(
409414
mut service_request: ServiceRequest,
410415
) -> actix_web::Result<ServiceResponse> {
416+
if let Some(redirect) = redirect_missing_prefix(&service_request) {
417+
return Ok(service_request.into_response(redirect));
418+
}
419+
411420
let path = req_path(&service_request);
412421
let sql_file_path = path_to_sql_file(&path);
413422
let maybe_response = if let Some(sql_path) = sql_file_path {
@@ -419,11 +428,10 @@ pub async fn main_handler(
419428
}
420429
} else {
421430
log::debug!("Serving file: {:?}", path);
422-
let app_state = service_request.extract::<web::Data<AppState>>().await?;
423431
let path = req_path(&service_request);
424432
let if_modified_since = IfModifiedSince::parse(&service_request).ok();
425-
426-
serve_file(&path, &app_state, if_modified_since).await
433+
let app_state: &web::Data<AppState> = service_request.app_data().expect("app_state");
434+
serve_file(&path, app_state, if_modified_since).await
427435
};
428436

429437
// On 404/NOT_FOUND error, fall back to `404.sql` handler if it exists
@@ -442,13 +450,27 @@ pub async fn main_handler(
442450
Ok(service_request.into_response(response))
443451
}
444452

453+
fn redirect_missing_prefix(service_request: &ServiceRequest) -> Option<HttpResponse> {
454+
let app_state: &web::Data<AppState> = service_request.app_data().expect("app_state");
455+
if !service_request
456+
.path()
457+
.starts_with(&app_state.config.site_prefix)
458+
{
459+
let header = (header::LOCATION, app_state.config.site_prefix.clone());
460+
return Some(
461+
HttpResponse::PermanentRedirect()
462+
.insert_header(header)
463+
.finish(),
464+
);
465+
}
466+
None
467+
}
468+
445469
/// Extracts the path from a request and percent-decodes it
446470
fn req_path(req: &ServiceRequest) -> Cow<'_, str> {
447471
let encoded_path = req.path();
448472
let app_state: &web::Data<AppState> = req.app_data().expect("app_state");
449-
let encoded_path = encoded_path
450-
.strip_prefix(&app_state.config.site_prefix)
451-
.unwrap_or(encoded_path);
473+
let encoded_path = strip_site_prefix(encoded_path, app_state);
452474
percent_encoding::percent_decode_str(encoded_path).decode_utf8_lossy()
453475
}
454476

@@ -481,12 +503,12 @@ async fn default_prefix_redirect(
481503
service_request: ServiceRequest,
482504
) -> actix_web::Result<ServiceResponse> {
483505
let app_state: &web::Data<AppState> = service_request.app_data().expect("app_state");
484-
let redirect_path = app_state
485-
.config
486-
.site_prefix
487-
.trim_end_matches('/')
488-
.to_string()
489-
+ service_request.path();
506+
let original_path = service_request.path();
507+
let site_prefix = &app_state.config.site_prefix;
508+
let redirect_path = site_prefix.trim_end_matches('/').to_string() + original_path;
509+
log::info!(
510+
"Received request to {original_path} (outside of site prefix {site_prefix}), redirecting to {redirect_path}"
511+
);
490512
Ok(service_request.into_response(
491513
HttpResponse::PermanentRedirect()
492514
.insert_header((header::LOCATION, redirect_path))

0 commit comments

Comments
 (0)