CH-J ToolsSerververwaltung über SSH
Menü
Veröffentlichte Quellen

CH-J Tools

Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.

Quellen als ZIP herunterladen
CH-J / Server Manager 872 Dateien
core-20261030T220507Z-61e4558d889f Hinzugefügt: · Erstellt:
protocol.rs Hinzugefügt: Datum nicht erfasst 5,5 KB · 136 Zeilen
Code Datei herunterladen
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 shared
3//! window-chrome stylesheet. Port of `PluginRuntime.handleRequest` with stricter checks
4//! (canonical path containment, requesting window must belong to the module).
6use crate::core::app::CoreSlot;
7use crate::core::modules::service::MODULE_CSP;
8use std::path::{Component, Path, PathBuf};
9use tauri::http::{Request, Response, StatusCode};
10use tauri::{Manager, Runtime, UriSchemeContext};
12const MAX_RESOURCE: u64 = 10 * 1024 * 1024;
14fn 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 }
32fn not_found() -> Response<Vec<u8>> {
33 Response::builder().status(StatusCode::NOT_FOUND).header("Content-Type", "text/plain").body(b"Not found".to_vec()).unwrap()
36/// Splits the request into (module id, relative path) for both URL forms.
37pub 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 host
48 };
49 Some((id, segments.join("/")))
52fn 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()
69fn 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)
83pub 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())
124#[cfg(test)]
125mod 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 }
SHA-2562a68aa0e3d8e03f708d1330d09d9c096d1dd37d29c389ad5b43ee4c14d1b2136

CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.

SHA-256 des Archivs77380d4805485ffa4027fbe7858b5836f197a4aa9d52358a43e2d12d018744df
Dateien