Skip to content

Commit b0c63d4

Browse files
Merge pull request #157 from TryCli-Studio/fix/keys
Fix/keys
2 parents 8f76f31 + f6ce466 commit b0c63d4

4 files changed

Lines changed: 64 additions & 19 deletions

File tree

client/src/pages/view.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,9 @@ pub fn ViewPage() -> impl IntoView {
448448
let vip = if key.is_empty() {
449449
String::new()
450450
} else {
451-
format!("{}/{}/{}?key={}", origin_clone, username_clone, slug_clone, key)
451+
// URL encode the key to handle special characters in base64
452+
let encoded_key = js_sys::encode_uri_component(key);
453+
format!("{}/{}/{}?key={}", origin_clone, username_clone, slug_clone, encoded_key)
452454
};
453455
set_vip_link.set(vip);
454456
}
@@ -501,7 +503,9 @@ pub fn ViewPage() -> impl IntoView {
501503
let vip = if key.is_empty() {
502504
String::new()
503505
} else {
504-
format!("{}/{}/{}?key={}", origin_clone, username_clone, slug_clone, key)
506+
// URL encode the key to handle special characters in base64
507+
let encoded_key = js_sys::encode_uri_component(key);
508+
format!("{}/{}/{}?key={}", origin_clone, username_clone, slug_clone, encoded_key)
505509
};
506510
set_vip_link.set(vip);
507511
}

server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ openssl-sys = { version = "0.9", features = ["vendored"] }
2626
url = "2.5.8"
2727
dashmap = "6.1"
2828
governor = "0.10"
29+
urlencoding = "2.1"

server/src/handlers/project.rs

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ fn normalize_url(url_str: &str) -> Option<String> {
5353

5454
// Require a valid host for http(s) URLs
5555
let host = url.host_str()?;
56+
let port = url.port();
5657
let path = url.path();
5758

5859
// Normalize trailing slashes for consistency
@@ -62,7 +63,11 @@ fn normalize_url(url_str: &str) -> Option<String> {
6263
path.trim_end_matches('/')
6364
};
6465

65-
Some(format!("{}://{}{}", scheme, host, normalized_path))
66+
if let Some(p) = port {
67+
Some(format!("{}://{}:{}{}", scheme, host, p, normalized_path))
68+
} else {
69+
Some(format!("{}://{}{}", scheme, host, normalized_path))
70+
}
6671
}
6772

6873
/// Validates CSRF protection by checking Origin/Referer headers against expected frontend URL.
@@ -398,21 +403,47 @@ pub async fn get_project(
398403

399404
// 4. Dual-layer security for embeds (VIP key + Guest List)
400405
if !is_owner {
401-
// VIP Pass: compare URL ?key with stored embed_key using a match on Options
402-
let vip_allowed = match (params.get("key"), embed_key.as_ref()) {
403-
(Some(request_key), Some(stored_key)) if request_key == stored_key => true,
404-
_ => false,
406+
// VIP Pass: compare URL ?key with stored embed_key
407+
// Note: Axum's Query extractor already URL-decodes parameters
408+
let vip_allowed = match (params.get("key"), embed_key.as_ref()) {
409+
(Some(request_key), Some(stored_key)) => {
410+
// Axum already decodes query params; treat spaces as '+' to handle
411+
// unencoded base64 keys in query strings.
412+
let trimmed_request = request_key.trim();
413+
let normalized_request = trimmed_request.replace(' ', "+");
414+
let trimmed_stored = stored_key.trim();
415+
416+
let matches = trimmed_request == trimmed_stored
417+
|| normalized_request == trimmed_stored;
418+
419+
tracing::debug!(
420+
"VIP key comparison for project {}: request='{}' normalized='{}' stored='{}' match={}",
421+
project_id,
422+
trimmed_request,
423+
normalized_request,
424+
trimmed_stored,
425+
matches
426+
);
427+
428+
matches
429+
}
430+
_ => {
431+
tracing::debug!("VIP key check failed for project {}: request_key={:?} stored_key={:?}",
432+
project_id, params.get("key"), embed_key.as_ref());
433+
false
434+
}
405435
};
406436

407437
if !vip_allowed {
408-
// Guest List: validate Referer header against project_whitelists
438+
// Guest List: validate parent page URL against project_whitelists.
439+
// Prefer X-Embed-Referer (forwarded by embed page) and fallback to Referer.
409440
let referer = headers
410-
.get("X-Embed-Referer")
441+
.get("x-embed-referer")
411442
.and_then(|v| v.to_str().ok())
412443
.map(|s| s.to_string())
413444
.or_else(|| {
414445
headers
415-
.get("Referer")
446+
.get("referer")
416447
.and_then(|v| v.to_str().ok())
417448
.map(|s| s.to_string())
418449
});
@@ -421,7 +452,7 @@ pub async fn get_project(
421452
let whitelist_allowed: Option<bool> = if let Some(referer_url) = referer {
422453
// Normalize the Referer URL to prevent bypasses via query params or fragments
423454
let normalized_referer = normalize_url(&referer_url);
424-
455+
425456
if let Some(normalized) = normalized_referer {
426457
let exists_row: Option<(bool,)> = sqlx::query_as(
427458
"SELECT TRUE FROM project_whitelists \
@@ -870,16 +901,16 @@ pub async fn resolve_secret_embed(
870901
State(state): State<AppState>,
871902
Path(token): Path<String>,
872903
) -> Result<Html<String>, (StatusCode, String)> {
873-
// 1. Validate Token from DB
874-
let row: Option<(String, String)> = sqlx::query_as(
875-
"SELECT owner_username, slug FROM projects WHERE embed_token = $1"
904+
// 1. Validate Token from DB and fetch embed_key for VIP access
905+
let row: Option<(String, String, Option<String>)> = sqlx::query_as(
906+
"SELECT owner_username, slug, embed_key FROM projects WHERE embed_token = $1"
876907
)
877908
.bind(&token)
878909
.fetch_optional(&state.db)
879910
.await
880911
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
881912

882-
let (owner, slug) = row.ok_or((StatusCode::NOT_FOUND, "Invalid embed token".to_string()))?;
913+
let (owner, slug, embed_key) = row.ok_or((StatusCode::NOT_FOUND, "Invalid embed token".to_string()))?;
883914

884915
// 2. Get Configurations
885916
let api_url = std::env::var("API_URL").unwrap_or("http://localhost:3000".to_string());
@@ -889,8 +920,14 @@ pub async fn resolve_secret_embed(
889920
// 3. Generate Discovery URL for OEmbed (Points to Backend)
890921
let oembed_url = format!("{}/api/oembed?url={}/e/{}", api_url, api_url, token);
891922

892-
// 4. Construct Target URL (Points to Frontend)
893-
let target_url = format!("{}/embed/{}/{}", frontend_url, owner, slug);
923+
// 4. Construct Target URL with VIP key for bypass (Points to Frontend)
924+
let target_url = if let Some(key) = embed_key {
925+
// URL encode the key to handle special characters in base64
926+
let encoded_key = urlencoding::encode(&key);
927+
format!("{}/embed/{}/{}?key={}", frontend_url, owner, slug, encoded_key)
928+
} else {
929+
format!("{}/embed/{}/{}", frontend_url, owner, slug)
930+
};
894931

895932
// 5. Return HTML with <link> tags + Meta Refresh
896933
let html = format!(r#"<!DOCTYPE html>

server/src/router.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use axum::{
44
http::Method,
55
};
66
use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer};
7-
use axum::http::header::{CONTENT_TYPE, AUTHORIZATION};
7+
use axum::http::header::{CONTENT_TYPE, AUTHORIZATION, REFERER, ORIGIN};
88
use crate::state::AppState;
99
use crate::handlers::{auth, project, spawn, analytics, admin, oembed};
1010
use crate::services::websocket;
@@ -20,6 +20,9 @@ pub fn create_router(state: AppState) -> Result<Router, Box<dyn std::error::Erro
2020
let frontend_url = std::env::var("FRONTEND_URL")
2121
.unwrap_or_else(|_| "http://localhost:8080".to_string());
2222

23+
// Custom header for embed referer
24+
let x_embed_referer = axum::http::HeaderName::from_static("x-embed-referer");
25+
2326
let app = Router::new()
2427
.merge(auth::routes())
2528
.merge(spawn::routes())
@@ -32,7 +35,7 @@ pub fn create_router(state: AppState) -> Result<Router, Box<dyn std::error::Erro
3235
.allow_origin(frontend_url.parse::<axum::http::HeaderValue>()?)
3336
// 2. ALLOW DELETE METHOD
3437
.allow_methods([Method::GET, Method::POST, Method::DELETE, Method::OPTIONS])
35-
.allow_headers([CONTENT_TYPE, AUTHORIZATION])
38+
.allow_headers([CONTENT_TYPE, AUTHORIZATION, REFERER, ORIGIN, x_embed_referer])
3639
.allow_credentials(true)
3740
)
3841
.layer(session_layer)

0 commit comments

Comments
 (0)