Vydané zdroje
Stáhnout zdroje ZIP
CH-J Tools
Procházejte adresáře a soubory konkrétního vydání aplikace.
CH-J / Server Manager
872 souborů
core-20261030T220507Z-61e4558d889f
Přidáno: · Sestaveno:
1
//! `chj-module://<module-id>/<path>` (Windows: `http://chj-module.localhost/<module-id>/<path>`):2
//! serves only files of the module that owns the requesting window, plus the shared3
//! window-chrome stylesheet. Port of `PluginRuntime.handleRequest` with stricter checks4
//! (canonical path containment, requesting window must belong to the module).6
use crate::core::app::CoreSlot;7
use crate::core::modules::service::MODULE_CSP;8
use std::path::{Component, Path, PathBuf};9
use tauri::http::{Request, Response, StatusCode};10
use tauri::{Manager, Runtime, UriSchemeContext};12
const MAX_RESOURCE: u64 = 10 * 1024 * 1024;14
fn mime(path: &Path) -> &'static str {15
match path.extension().map(|e| e.to_string_lossy().to_lowercase()).as_deref() {16
Some("css") => "text/css; charset=utf-8",17
Some("gif") => "image/gif",18
Some("html") => "text/html; charset=utf-8",19
Some("jpeg") | Some("jpg") => "image/jpeg",20
Some("js") => "text/javascript; charset=utf-8",21
Some("json") => "application/json; charset=utf-8",22
Some("png") => "image/png",23
Some("svg") => "image/svg+xml",24
Some("webp") => "image/webp",25
Some("woff") => "font/woff",26
Some("woff2") => "font/woff2",27
Some("ttf") => "font/ttf",28
_ => "application/octet-stream",29
}30
}32
fn not_found() -> Response<Vec<u8>> {33
Response::builder().status(StatusCode::NOT_FOUND).header("Content-Type", "text/plain").body(b"Not found".to_vec()).unwrap()34
}36
/// Splits the request into (module id, relative path) for both URL forms.37
pub fn split_request(uri: &str) -> Option<(String, String)> {38
let url = url::Url::parse(uri).ok()?;39
let host = url.host_str()?.to_string();40
let mut segments: Vec<String> = url.path_segments()?.map(|s| percent_decode(s)).collect::<Option<Vec<_>>>()?;41
let id = if host == "chj-module.localhost" || host == "localhost" {42
if segments.is_empty() {43
return None;44
}45
segments.remove(0)46
} else {47
host48
};49
Some((id, segments.join("/")))50
}52
fn percent_decode(segment: &str) -> Option<String> {53
let bytes = segment.as_bytes();54
let mut out = Vec::with_capacity(bytes.len());55
let mut i = 0;56
while i < bytes.len() {57
if bytes[i] == b'%' {58
let hex = std::str::from_utf8(bytes.get(i + 1..i + 3)?).ok()?;59
out.push(u8::from_str_radix(hex, 16).ok()?);60
i += 3;61
} else {62
out.push(bytes[i]);63
i += 1;64
}65
}66
String::from_utf8(out).ok()67
}69
fn safe_relative(requested: &str) -> Option<PathBuf> {70
if requested.is_empty() || requested.starts_with('.') || requested.contains('\\') {71
return None;72
}73
let path = PathBuf::from(requested);74
for component in path.components() {75
match component {76
Component::Normal(part) if !part.to_string_lossy().starts_with('.') => {}77
_ => return None,78
}79
}80
Some(path)81
}83
pub fn handle<R: Runtime>(context: UriSchemeContext<'_, R>, request: Request<Vec<u8>>) -> Response<Vec<u8>> {84
let app = context.app_handle();85
let Some(core) = app.state::<CoreSlot>().try_get() else { return not_found() };86
let Some((module_id, requested)) = split_request(&request.uri().to_string()) else { return not_found() };87
// The requesting window must be the module's own window.88
match core.windows.module_by_label(context.webview_label()) {89
Some(owner) if owner.id == module_id => {}90
_ => {91
core.logger.warn("Plugin resource request rejected.", serde_json::json!({ "url": request.uri().to_string(), "message": "foreign window" }));92
return not_found();93
}94
}95
let (root, relative) = if requested == "__chj_core__/window-chrome.css" {96
(core.paths.bundled_modules(), PathBuf::from("window-chrome.css"))97
} else {98
let Ok(resolved) = core.services.modules.registry.resolve(&module_id) else { return not_found() };99
let entry = resolved.manifest["entry"].as_str().unwrap_or("").to_string();100
let Some(relative) = safe_relative(if requested.is_empty() { &entry } else { &requested }) else { return not_found() };101
(resolved.root, relative)102
};103
let file = root.join(&relative);104
let (Ok(real_root), Ok(real_file)) = (std::fs::canonicalize(&root), std::fs::canonicalize(&file)) else { return not_found() };105
if !real_file.starts_with(&real_root) {106
return not_found();107
}108
match std::fs::metadata(&real_file) {109
Ok(meta) if meta.is_file() && meta.len() <= MAX_RESOURCE => {}110
_ => return not_found(),111
}112
let Ok(data) = std::fs::read(&real_file) else { return not_found() };113
Response::builder()114
.status(StatusCode::OK)115
.header("Content-Type", mime(&real_file))116
.header("Content-Security-Policy", MODULE_CSP)117
.header("Cache-Control", "no-store")118
.header("X-Content-Type-Options", "nosniff")119
.header("Referrer-Policy", "no-referrer")120
.body(data)121
.unwrap_or_else(|_| not_found())122
}124
#[cfg(test)]125
mod tests {126
use super::*;128
#[test]129
fn request_splitting() {130
assert_eq!(split_request("chj-module://chj.users/ui/index.html"), Some(("chj.users".into(), "ui/index.html".into())));131
assert_eq!(split_request("http://chj-module.localhost/chj.users/ui/a%20b.js"), Some(("chj.users".into(), "ui/a b.js".into())));132
assert!(safe_relative("../x").is_none());133
assert!(safe_relative("ui/.hidden").is_none());134
assert!(safe_relative("ui/index.html").is_some());135
}136
}SHA-256
2a68aa0e3d8e03f708d1330d09d9c096d1dd37d29c389ad5b43ee4c14d1b2136CH-J Proprietary Software License 1.14 · Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.
SHA-256 archivu
77380d4805485ffa4027fbe7858b5836f197a4aa9d52358a43e2d12d018744df