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:
editor.rs Hinzugefügt: Datum nicht erfasst 8,5 KB · 161 Zeilen
Code Datei herunterladen
1//! Fixed remote file helper (`remoteEditor.py`) for the editor, privileged file operations and
2//! transfers (port of `SessionManager.remoteEditor` / `remoteChecksum`). Neither the UI nor a
3//! module can supply code: only JSON requests for the fixed script.
5use crate::core::error::{CoreError, CoreResult};
6use crate::core::ssh::platform::detect_platform;
7use crate::core::ssh::privileged::{normalize_password, Frame, PrivilegedExecutor, RunOptions};
8use crate::core::ssh::session::{shell_quote, SessionManager};
9use crate::core::ssh::windows_exec::run_windows_script;
10use once_cell::sync::Lazy;
11use regex::Regex;
12use serde_json::Value;
13use std::time::Duration;
15pub const REMOTE_EDITOR: &str = include_str!("../../../resources/remote/remoteEditor.py");
16const EDITOR_PRIVILEGED_SCRIPT: &str = include_str!("../ssh/scripts/editor_privileged.sh");
17const WINDOWS_HASH_SCRIPT: &str = include_str!("../ssh/scripts/windows_hash.ps1");
18const EDITOR_STARTED: &str = "\x1eCHJ_EDITOR_START\n";
19const EDITOR_MUTATIONS: [&str; 9] = ["finalize", "publish", "rename", "remove", "mkdir", "create", "cleanup", "stage-out", "transfer-remove"];
20pub const EDITOR_OUTPUT_LIMIT: usize = 36 * 1024 * 1024;
22static FILE_CODE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^FILE_[A-Z_]+$").unwrap());
23static SUDO_CODE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^SUDO_[A-Z_]+$").unwrap());
25/// Authorization fields a caller may pass (0.1.0 `authorization()` + Core-internal options).
26#[derive(Debug, Clone, Default)]
27pub struct Authorization {
28 pub sudo: bool,
29 pub sudo_password: Option<String>,
30 pub remember_sudo: bool,
31 /// Core-internal: `useCachedSudo !== false`.
32 pub ignore_cached_sudo: bool,
33 /// Core-internal timeout (clamped 10 s .. 24 h, default 60 s).
34 pub timeout_ms: Option<u64>,
37impl Authorization {
38 /// Only these option fields reach the helper; modules cannot set timeouts or internals.
39 pub fn from_options(options: &Value) -> Self {
40 Authorization {
41 sudo: options.get("sudo") == Some(&Value::Bool(true)),
42 sudo_password: match options.get("sudoPassword") {
43 None | Some(Value::Null) => None,
44 Some(Value::String(s)) if s.is_empty() => None,
45 Some(Value::String(s)) => Some(s.clone()),
46 Some(other) => Some(other.to_string()),
47 },
48 remember_sudo: options.get("rememberSudo") == Some(&Value::Bool(true)),
49 ..Default::default()
50 }
51 }
53 pub fn sudo(mut self) -> Self {
54 self.sudo = true;
55 self
56 }
58 pub fn with_timeout(mut self, ms: u64) -> Self {
59 self.timeout_ms = Some(ms);
60 self
61 }
64fn session_error(code: &str, message: &str) -> CoreError {
65 CoreError::new(code, message)
68/// `remoteEditor(sessionId, request, authorization)`.
69pub async fn remote_editor(sessions: &SessionManager, privileged: &PrivilegedExecutor, session_id: &str, request: &Value, auth: &Authorization) -> CoreResult<Value> {
70 let record = sessions.require_connected(session_id)?;
71 let platform = detect_platform(sessions, &record, false).await?;
72 if platform["os"] == "windows" {
73 let caption = platform["caption"].as_str().filter(|s| !s.is_empty()).unwrap_or("Windows");
74 return Err(session_error("FEATURE_PLATFORM_UNSUPPORTED", &format!("The remote text editor is available only on Linux/POSIX servers; this server runs {caption}.")));
75 }
76 let password = normalize_password(auth.sudo_password.as_deref()).map_err(|_| session_error("FILE_SUDO_PASSWORD_INVALID", "Invalid sudo password."))?;
77 if !password.is_empty() && !auth.sudo {
78 return Err(session_error("FILE_SUDO_AUTHORIZATION_REQUIRED", "Explicit sudo authorization is required."));
79 }
80 let timeout = Duration::from_millis(auth.timeout_ms.unwrap_or(60_000).clamp(10_000, 24 * 3600 * 1000));
81 let operation = request["operation"].as_str().unwrap_or("");
82 let mutation = EDITOR_MUTATIONS.contains(&operation);
83 let not_confirmed = |dependency: bool| {
84 session_error(
85 if dependency { "FILE_DEPENDENCY_UNAVAILABLE" } else if mutation { "FILE_RESULT_UNKNOWN" } else { "FILE_EXEC_FAILED" },
86 "Remote file operation was not confirmed. Inspect the target before retrying.",
87 )
88 };
89 let request_json = serde_json::to_string(request).unwrap_or_default();
90 let (stdout, code) = if auth.sudo {
91 let options = RunOptions {
92 script: EDITOR_PRIVILEGED_SCRIPT.to_string(),
93 args: vec![REMOTE_EDITOR.to_string()],
94 frames: vec![Frame::text("REQ", &request_json)],
95 privileged: true,
96 sudo_password: (!password.is_empty()).then(|| password.to_string()),
97 remember_password: auth.remember_sudo,
98 use_cached_password: !auth.ignore_cached_sudo,
99 purpose: format!("files.{operation}"),
100 error_prefix: "FILE_".into(),
101 timeout,
102 output_limit: EDITOR_OUTPUT_LIMIT,
103 };
104 match privileged.run(session_id, options).await {
105 Ok(result) => (result.stdout, result.code),
106 // sudo refusals happen before the helper starts: definite, original untouched.
107 Err(error) if SUDO_CODE.is_match(&error.code) || error.code == "FILE_DEPENDENCY_UNAVAILABLE" => return Err(error),
108 Err(_) => return Err(not_confirmed(false)),
109 }
110 } else {
111 let command = format!("printf '\\036CHJ_EDITOR_START\\n'; exec python3 -I -B -c {} {}", shell_quote(REMOTE_EDITOR), shell_quote(&request_json));
112 match sessions.exec_fixed(&record, &command, timeout, EDITOR_OUTPUT_LIMIT).await {
113 Ok(result) => (result.stdout, result.code),
114 // Remote stderr never becomes editor errors or logs.
115 Err(error) => return Err(not_confirmed(error.details.get("remoteExitCode").and_then(Value::as_i64) == Some(127))),
116 }
117 };
118 let response: Value = match stdout.strip_prefix(EDITOR_STARTED).and_then(|rest| serde_json::from_str(rest).ok()) {
119 Some(value) => value,
120 None => {
121 let code = if code == Some(127) { "FILE_DEPENDENCY_UNAVAILABLE" } else if mutation { "FILE_RESULT_UNKNOWN" } else { "FILE_EXEC_FAILED" };
122 return Err(session_error(code, "Remote file operation result could not be verified."));
123 }
124 };
125 if response["ok"] != true || !response["value"].is_object() {
126 let code = response["code"].as_str().filter(|c| FILE_CODE.is_match(c)).unwrap_or("FILE_PROTOCOL_ERROR").to_string();
127 let suffix = if mutation { "; the original was kept and recoverable copies were retained" } else { "" };
128 return Err(session_error(&code, &format!("{code}: Remote file operation stopped{suffix}.")));
129 }
130 if code != Some(0) {
131 return Err(not_confirmed(code == Some(127)));
132 }
133 Ok(response["value"].clone())
136/// SHA-256 of a remote file with standard tools, for servers without python3.
137pub async fn remote_checksum(sessions: &SessionManager, session_id: &str, remote_path: &str) -> CoreResult<String> {
138 let record = sessions.require_connected(session_id)?;
139 if !remote_path.starts_with('/') || remote_path.contains(['\0', '\r', '\n']) {
140 return Err(session_error("FILE_UNSAFE_PATH", "Remote path must be absolute."));
141 }
142 let unavailable = || session_error("FILE_CHECKSUM_UNAVAILABLE", "The remote checksum could not be read.");
143 let six_hours = Duration::from_secs(6 * 3600);
144 if detect_platform(sessions, &record, false).await?["os"] == "windows" {
145 // Windows OpenSSH SFTP paths look like /C:/Users/x; the path travels as JSON, never in the command line.
146 let result = run_windows_script(sessions, &record, WINDOWS_HASH_SCRIPT, &serde_json::json!({ "path": remote_path }), six_hours, 4 * 1024 * 1024, "METRICS_").await?;
147 let hash = result["hash"].as_str().unwrap_or("");
148 return if hash.len() == 64 && hash.chars().all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c)) { Ok(hash.to_string()) } else { Err(unavailable()) };
149 }
150 let command = format!(
151 "LC_ALL=C; p={}; if command -v sha256sum >/dev/null 2>&1; then sha256sum -- \"$p\"; elif command -v shasum >/dev/null 2>&1; then shasum -a 256 -- \"$p\"; else exit 127; fi",
152 shell_quote(remote_path)
153 );
154 let output = sessions.exec_fixed_default(&record, &command, six_hours).await?;
155 let first = output.stdout.trim().split(|c: char| !c.is_ascii_hexdigit()).next().unwrap_or("");
156 if first.len() == 64 && first.chars().all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c)) {
157 Ok(first.to_string())
158 } else {
159 Err(unavailable())
160 }
SHA-256dd7e82b327d8cf57e308d0d74d44e8091c3d4620bb186f7a7665b181d007924c

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