CH-J Toolspengurusan pelayan melalui SSH
Menu
Sumber yang diterbitkan

CH-J Tools

Layari direktori dan fail untuk keluaran aplikasi tertentu.

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

CH-J Proprietary Software License 1.14 · Kod sumber disediakan di bawah CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah terma lesen atau memberikan hak tambahan.

SHA-256 arkibac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Fail