প্রকাশিত সোর্স
সোর্স ZIP ডাউনলোড করুন
CH-J Tools
নির্দিষ্ট একটি অ্যাপ্লিকেশন রিলিজের ডিরেক্টরি ও ফাইল ব্রাউজ করুন।
CH-J / Server Manager
1704 টি ফাইল
0.1.2-beta
যোগ করা হয়েছে: · বিল্ড হয়েছে:
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::matrix;6
use crate::core::error::{CoreError, CoreResult};7
use crate::core::ssh::platform::detect_platform;8
use crate::core::ssh::privileged::{normalize_password, Frame, PrivilegedExecutor, RunOptions};9
use crate::core::ssh::session::SessionManager;10
use crate::core::ssh::windows_exec::run_windows_script;11
use once_cell::sync::Lazy;12
use regex::Regex;13
use serde_json::Value;14
use std::time::Duration;16
const EDITOR_STARTED: &str = "\x1eCHJ_EDITOR_START\n";17
const EDITOR_MUTATIONS: [&str; 11] = ["finalize", "publish", "rename", "remove", "mkdir", "create", "cleanup", "stage-out", "transfer-remove", "remove-source", "recover-source"];18
pub const EDITOR_OUTPUT_LIMIT: usize = 36 * 1024 * 1024;20
static FILE_CODE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^FILE_[A-Z_]+$").unwrap());21
static 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)]25
pub 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>,33
}35
impl 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
self54
}56
pub fn with_timeout(mut self, ms: u64) -> Self {57
self.timeout_ms = Some(ms);58
self59
}60
}62
fn session_error(code: &str, message: &str) -> CoreError {63
CoreError::new(code, message)64
}66
/// `remoteEditor(sessionId, request, authorization)`.67
pub 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())133
}135
/// SHA-256 of a remote file with standard tools, for servers without python3.136
pub 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
}156
}SHA-256
d726646b76ee47d75d5f8a1691561a92b3aeb6e362b7fea4abaacbc833a70272CH-J Proprietary Software License 1.14 · সোর্স CH-J Proprietary Software License 1.14-এর অধীনে দেওয়া হয়। এর প্রাপ্যতা লাইসেন্সের শর্ত বদলায় না এবং অতিরিক্ত কোনো অধিকারও দেয় না।
আর্কাইভ SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f