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
//! Fixed remote file helper (`remoteEditor.py`) for the editor, privileged file operations and2
//! transfers (port of `SessionManager.remoteEditor` / `remoteChecksum`). Neither the UI nor a3
//! module can supply code: only JSON requests for the fixed script.5
use crate::core::error::{CoreError, CoreResult};6
use crate::core::ssh::platform::detect_platform;7
use crate::core::ssh::privileged::{normalize_password, Frame, PrivilegedExecutor, RunOptions};8
use crate::core::ssh::session::{shell_quote, SessionManager};9
use crate::core::ssh::windows_exec::run_windows_script;10
use once_cell::sync::Lazy;11
use regex::Regex;12
use serde_json::Value;13
use std::time::Duration;15
pub const REMOTE_EDITOR: &str = include_str!("../../../resources/remote/remoteEditor.py");16
const EDITOR_PRIVILEGED_SCRIPT: &str = include_str!("../ssh/scripts/editor_privileged.sh");17
const WINDOWS_HASH_SCRIPT: &str = include_str!("../ssh/scripts/windows_hash.ps1");18
const EDITOR_STARTED: &str = "\x1eCHJ_EDITOR_START\n";19
const EDITOR_MUTATIONS: [&str; 9] = ["finalize", "publish", "rename", "remove", "mkdir", "create", "cleanup", "stage-out", "transfer-remove"];20
pub const EDITOR_OUTPUT_LIMIT: usize = 36 * 1024 * 1024;22
static FILE_CODE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^FILE_[A-Z_]+$").unwrap());23
static 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)]27
pub 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>,35
}37
impl 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
self56
}58
pub fn with_timeout(mut self, ms: u64) -> Self {59
self.timeout_ms = Some(ms);60
self61
}62
}64
fn session_error(code: &str, message: &str) -> CoreError {65
CoreError::new(code, message)66
}68
/// `remoteEditor(sessionId, request, authorization)`.69
pub 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())134
}136
/// SHA-256 of a remote file with standard tools, for servers without python3.137
pub 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
}161
}SHA-256
dd7e82b327d8cf57e308d0d74d44e8091c3d4620bb186f7a7665b181d007924cCH-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