mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-07 01:22:15 +08:00
* refactor(utils): drop dead reset helper and tidy init_resources
- remove resolve_reset_async, which had no callers
- drop the duplicated time.*.com entry from the default fake-ip-filter
- init_resources no longer recreates app_home (ensure_directories during
init_config already created it) and now propagates res_dir creation errors
* refactor(profile): remove identical blind retry when switching node
The second select_node_for_group call in switch_proxy_node was a
zero-interval retry with identical arguments. Its success path was a
degraded copy of the first (missing the Refresh::Proxies announcement
ever since the old force-refresh emit was removed), and its distinct
"回退/最终失败" log wording misled maintainers. Keep a single call,
the primary success aftermath, and one error log.
* refactor(enhance): simplify tun mode check and lowercase helpers
- Drop the always-true second clause in use_tun's enhanced-mode check:
the key is absent only when the unwrap_or default already makes
current_mode == "fake-ip" true.
- Collapse use_lowercase into a delegation to use_lowercase_owned.
* refactor(core): drop unreachable hotkey dtor and dedupe tray predicate
Remove impl Drop for Hotkey: the instance lives in a OnceLock static via
the singleton! macro and is never dropped; exit-time unregistration is
already handled by Hotkey::reset().
Spell the include_proxy_groups/display-mode predicate identically at
both evaluation sites in tray menu building; behavior is unchanged
(Option != Some("disable") is equivalent to unwrap_or("default") != "disable").
* refactor(config): fold port guards into single helper
The five IClashTemp port guards duplicated the same read/parse/default
logic with drifting defaults: some hardcoded 7895/7898/7899 while others
referenced network::ports constants. Route them all through one
guard_port_value helper and use the constants everywhere, keeping each
guard's name, key and cfg attributes.
* refactor(config): dedupe prfitem factories and companion seeding
from_rules/from_proxies/from_groups differed only in uid prefix, type
string and template, so they now delegate to a shared from_seq_item.
The five current_* accessors collapse into a local macro. The identical
five-block companion seeding in from_local and from_url moves into
ensure_companion_items, preserving seeding order and error propagation.
* refactor(config): share patch macro and profiles helpers
IVerge::patch_config, IProfiles::patch_item and PrfOption::merge each
hand-rolled the same copy-Some-overwrite pattern; they now share one
patch_opt_field macro (the or-else merge form is field-for-field
equivalent for Option fields). The four profiles *_safe wrappers that
inlined Config::profiles().with_data_modify boilerplate delegate to a
with_profiles_edit helper, and record/forget_selected_node share the
current-item lookup prelude via current_item_mut.
* refactor(enhance): merge chain processing and drop unused builtin js payload
- collapse the identical process_global_items/process_profile_items into
one parameterized process_override_items (entry uid sources unchanged)
- fold the identical rules/proxies/groups arms of ChainItem::from_async
into a single arm with an inner dispatch
- stop embedding builtin JS payloads never executed by any path: the
enhance loop dispatches builtin items by uid to native implementations,
so ChainItem::builtin now carries empty script data (uids unchanged)
- source ProfileItems fallback literals from Default instead of a second
hand-written copy in collect_profile_items
* refactor(net): reuse webpki tls config and fallback
Extract build_webpki_rooted_tls_config so the delay-test TLS connector
and the reqwest clients share one static-webpki-roots builder; ALPN
stays caller-specific (only reqwest offers h2).
Collapse the duplicated platform-verifier -> static-webpki-roots retry
in get/get_bytes into request_with_static_webpki_fallback, gated by the
caller's certificate-verification setting.
* refactor(app): extract common exit cleanup
restart_app and quit duplicate the whole exit preparation: set the
exiting flag, persist configs, run interactive cleanup, and on a failed
core stop clear the flag, restore public DNS under the lifecycle lock
on macOS, and send the stop-failed notice.
Move that flow into run_exit_cleanup_flow next to clean_async, taking
the notice variant as a parameter and returning the CleanupResult so
each caller keeps its own tail: restart aborts before shutdown, quit
always shuts down and exits with the cleanup code.
* refactor(backup): share restore finalize path
restore_webdav_backup and restore_local_backup duplicated the
credential extraction (backup creation strips the WebDAV credentials,
so both re-inject the live ones), the archive extraction and the
finalize tail. Move that tail into a private restore_backup_archive
helper so the two entries keep only their download/locate logic; the
config-write lock scope and the off-runtime extraction land in one
place. The webdav entry keeps its downloaded-file cleanup.
* refactor(sysopt): drop redundant lock and busy-wait
The proxy target pair sat behind an Arc<RwLock> with no readers and an
Arc that was never cloned; both writers already serialize on
update_lock, so store the pair inside that mutex and delete the RwLock.
refresh_guard waited for the guard to become ready with a yield_now spin
at full CPU while holding guard_operation_lock; a 10 ms sleep keeps the
same wait condition and lock scope without the spin.
* refactor(sysopt): share macos bypass formatting
The macOS default bypass list and its three-way composition (empty ->
default, default + custom, custom only) were maintained twice: in
sysopt's format_bypass and inline in proxy_control's service_bypass.
Keep the literal once in constants and compose both paths through
format_bypass; service_bypass keeps its NUL check and UTF-8 truncation.
* refactor(core): dedupe window listeners, elevated runners, backup settings
- Collapse the close/focus window listener pairs in lightweight.rs onto a shared WindowListener with per-listener event and log label.
- Extract the windows runas-or-direct execution into service::run_maybe_elevated, shared by win_uwp and the service uninstaller, and factor the common uninstaller path resolution and status check shared across platforms.
- Make the auto-backup watch channel the single settings source, replacing the duplicate Arc<RwLock> copy with a stored receiver.
* refactor(enhance): fold lowercase helpers into single owned function
use_lowercase_owned disappears; the surviving use_lowercase takes the
owned Mapping and borrowed callers clone at the call site.
* refactor(enhance): rename ChainSupport variants to Mihomo names
Clash is a legacy name; the variants match the verge-mihomo and
verge-mihomo-alpha cores they gate.
* refactor(enhance): lowercase takes a reference and chain support is copy
use_lowercase borrows the mapping and clones only the values; call
sites no longer clone whole configs. ChainSupport is a fieldless enum
and now Copy; builtin() registers each support pair without clones.
* refactor(config): name prfitem constructor for what it builds
new_seq_item states that a fresh item is built from a template; the
from_ prefix wrongly implied an existing source. append_companion_item
returns the uid directly: append_item bails on a missing uid, so the
Option was always Some on the Ok path.
* refactor(sysopt): name the proxy state lock for its full role
The lock now owns the (Sysproxy, Autoproxy) state, so proxy_state_lock
says what it guards; the guard locals follow. constants::proxy held a
single sysproxy constant and is renamed sysproxy.
* refactor: replace remaining clash meta naming with mihomo
Doc comment, generated bindings and the prebuild sidecar helpers stop
referring to the legacy Clash Meta name; installer legacy-process
cleanup keeps the old executable names on purpose.
* refactor(enhance): drop builtin core filtering that never discriminates
Both valid cores matched a different ChainSupport variant, yet each
variant registered the same uid pair, so the filtered builtin list was
identical for stable and alpha. Invalid cores are coerced at startup
(validate_and_fix_config / get_valid_clash_core) and the None arm of
is_support was unreachable. builtin() now returns the items directly
and the clash_core threading through ConfigValues goes away; re-add
core discrimination when a builtin actually differs per core.
* refactor(enhance): drop unreachable mode script guard
merge_default_config overwrites the profile's mode with the verge-owned
clash config, whose mode is always present (template seeds rule and
patch semantics never clear it) and restricted to rule/global/direct;
change_clash_mode is rejected by the core before persisting anything
else. mode:script can therefore only arrive through a hand-crafted
patch_clash call, and for that path an explicit invalid-mode validation
error is more honest than silently rewriting to rule. mihomo rejects
mode:script outright (invalid mode), and user merge/script chains run
after the builtins, so the guard never covered them anyway.
* refactor(enhance): call the hysteria alpn builtin directly
builtin() existed to feed the uid-dispatch loop; with a single
builtin the vec and match are ceremony. apply_builtin_scripts calls
builtin_hy_alpn directly and to_script goes away; tests construct
ChainItem literals the way the merge items already did.
225 lines
8.2 KiB
Rust
225 lines
8.2 KiB
Rust
use crate::{process::AsyncHandler, utils::tmpl};
|
|
|
|
use super::field::use_lowercase;
|
|
use anyhow::{Error, Result};
|
|
use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function::NativeFunction, property::Attribute};
|
|
use clash_verge_logging::{Type, logging};
|
|
use parking_lot::Mutex;
|
|
use serde_yaml_ng::Mapping;
|
|
use smartstring::alias::String;
|
|
use std::sync::Arc;
|
|
|
|
const MAX_OUTPUTS: usize = 1000;
|
|
const MAX_OUTPUT_SIZE: usize = 1024 * 1024; // 1MB
|
|
const MAX_JSON_SIZE: usize = 10 * 1024 * 1024; // 10MB
|
|
const MAX_LOOP_ITERATIONS: u64 = 10_000_000;
|
|
const SCRIPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
|
|
|
type ScriptResult = (Mapping, Vec<String>, Vec<(String, String)>);
|
|
type EvalOutcome = (Result<Mapping, std::string::String>, Vec<(String, String)>);
|
|
|
|
/// Never fails: exceptions are logged and the input config is returned unchanged.
|
|
pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult {
|
|
// The template is identity-plus-lowercasing; skip the Boa round-trip.
|
|
if script == tmpl::ITEM_SCRIPT {
|
|
return (use_lowercase(&config), vec![], vec![]);
|
|
}
|
|
|
|
let json = match serde_json::to_string(&use_lowercase(&config)) {
|
|
Ok(json) if json.len() <= MAX_JSON_SIZE => json,
|
|
Ok(_) => {
|
|
let message = "Configuration size exceeds maximum allowed size";
|
|
return (config, vec![], vec![("exception".into(), message.into())]);
|
|
}
|
|
Err(err) => return (config, vec![], vec![("exception".into(), err.to_string().into())]),
|
|
};
|
|
|
|
let evaluated = AsyncHandler::spawn_blocking(move || eval_script(&script, &json, &name));
|
|
let (outcome, mut logs) = match tokio::time::timeout(SCRIPT_TIMEOUT, evaluated).await {
|
|
Ok(Ok(evaluated)) => evaluated,
|
|
Ok(Err(join_err)) => (Err(format!("script task panicked: {join_err}")), vec![]),
|
|
Err(_elapsed) => (
|
|
Err(format!("script execution timed out after {SCRIPT_TIMEOUT:?}")),
|
|
vec![],
|
|
),
|
|
};
|
|
|
|
match outcome {
|
|
Ok(result) => {
|
|
// Compared against the config as handed in, not the lowercased copy the script saw.
|
|
let changed_keys = result
|
|
.iter()
|
|
.filter(|(key, value)| config.get(key) != Some(value))
|
|
.filter_map(|(key, _)| {
|
|
let mut key: String = key.as_str()?.into();
|
|
key.make_ascii_lowercase();
|
|
Some(key)
|
|
})
|
|
.collect();
|
|
(result, changed_keys, logs)
|
|
}
|
|
Err(reason) => {
|
|
logs.push(("exception".into(), reason.into()));
|
|
(config, vec![], logs)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Console logs survive every failure path.
|
|
fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
|
|
let mut context = Context::default();
|
|
|
|
context
|
|
.runtime_limits_mut()
|
|
.set_loop_iteration_limit(MAX_LOOP_ITERATIONS);
|
|
|
|
let outputs = Arc::new(Mutex::new(vec![]));
|
|
let total_size = Arc::new(Mutex::new(0usize));
|
|
|
|
let outputs_clone = Arc::clone(&outputs);
|
|
let total_size_clone = Arc::clone(&total_size);
|
|
|
|
let bail = |reason: std::string::String| (Err(reason), outputs.lock().to_vec());
|
|
|
|
let _ = context.register_global_builtin_callable("__verge_log__".into(), 2, unsafe {
|
|
NativeFunction::from_closure(move |_: &JsValue, args: &[JsValue], context: &mut Context| {
|
|
let level = args
|
|
.first()
|
|
.ok_or_else(|| boa_engine::JsError::from_opaque(JsString::from("Missing level argument").into()))?;
|
|
let level = level.to_string(context)?;
|
|
let level = level.to_std_string().map_err(|_| {
|
|
boa_engine::JsError::from_opaque(JsString::from("Failed to convert level to string").into())
|
|
})?;
|
|
|
|
let data = args
|
|
.get(1)
|
|
.ok_or_else(|| boa_engine::JsError::from_opaque(JsString::from("Missing data argument").into()))?;
|
|
let data = data.to_string(context)?;
|
|
let data = data.to_std_string().map_err(|_| {
|
|
boa_engine::JsError::from_opaque(JsString::from("Failed to convert data to string").into())
|
|
})?;
|
|
|
|
// 检查输出限制
|
|
if outputs_clone.lock().len() >= MAX_OUTPUTS {
|
|
return Err(boa_engine::JsError::from_opaque(
|
|
JsString::from("Maximum number of log outputs exceeded").into(),
|
|
));
|
|
}
|
|
|
|
let mut size = total_size_clone.lock();
|
|
let new_size = *size + level.len() + data.len();
|
|
if new_size > MAX_OUTPUT_SIZE {
|
|
return Err(boa_engine::JsError::from_opaque(
|
|
JsString::from("Maximum output size exceeded").into(),
|
|
));
|
|
}
|
|
*size = new_size;
|
|
drop(size);
|
|
outputs_clone.lock().push((level.into(), data.into()));
|
|
Ok(JsValue::undefined())
|
|
})
|
|
});
|
|
|
|
let _ = context.eval(Source::from_bytes(
|
|
r#"var console = Object.freeze({
|
|
log(data){__verge_log__("log",JSON.stringify(data, null, 2))},
|
|
info(data){__verge_log__("info",JSON.stringify(data, null, 2))},
|
|
error(data){__verge_log__("error",JSON.stringify(data, null, 2))},
|
|
debug(data){__verge_log__("debug",JSON.stringify(data, null, 2))},
|
|
warn(data){__verge_log__("warn",JSON.stringify(data, null, 2))},
|
|
table(data){__verge_log__("table",JSON.stringify(data, null, 2))},
|
|
});"#,
|
|
));
|
|
|
|
// Bind the JSON instead of embedding it in program source; Boa's parser never chews it.
|
|
if let Err(err) = context.register_global_property(
|
|
js_string!("__verge_config__"),
|
|
JsValue::from(JsString::from(config_json)),
|
|
Attribute::all(),
|
|
) {
|
|
return bail(format!("failed to bind config for script: {err}"));
|
|
}
|
|
|
|
// 仅处理 name 参数中的特殊字符
|
|
let safe_name = escape_js_string_for_single_quote(name);
|
|
if safe_name.len() > 1024 {
|
|
return bail("Name parameter too long".to_owned());
|
|
}
|
|
|
|
let code = format!(
|
|
r"try{{
|
|
{script};
|
|
JSON.stringify(main(JSON.parse(globalThis.__verge_config__),'{safe_name}')||'')
|
|
}} catch(err) {{
|
|
`__error_flag__ ${{err.toString()}}`
|
|
}}"
|
|
);
|
|
|
|
let result = match context.eval(Source::from_bytes(code.as_str())) {
|
|
Ok(result) if result.is_string() => result,
|
|
_ => return bail("main function should return object".to_owned()),
|
|
};
|
|
let result = match result.to_string(&mut context) {
|
|
Ok(result) => result,
|
|
Err(e) => return bail(format!("Failed to convert JS result to string: {e}")),
|
|
};
|
|
let result = match result.to_std_string() {
|
|
Ok(result) => result,
|
|
Err(_) => return bail("Failed to convert JS string to std string".to_owned()),
|
|
};
|
|
|
|
if result.len() > MAX_JSON_SIZE {
|
|
return bail("Script result exceeds maximum allowed size".to_owned());
|
|
}
|
|
|
|
match parse_json_safely(&result) {
|
|
Ok(config) => (Ok(use_lowercase(&config)), outputs.lock().to_vec()),
|
|
Err(err) => {
|
|
logging!(
|
|
error,
|
|
Type::Config,
|
|
"Script execution error: {err:#}. Script name: {name}"
|
|
);
|
|
bail("Script execution failed".to_owned())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_json_safely(json_str: &str) -> Result<Mapping, Error> {
|
|
if json_str.len() > MAX_JSON_SIZE {
|
|
anyhow::bail!("JSON string too large");
|
|
}
|
|
|
|
let json_str = strip_outer_quotes(json_str);
|
|
Ok(serde_json::from_str::<Mapping>(json_str)?)
|
|
}
|
|
|
|
// 安全地移除外层引号
|
|
fn strip_outer_quotes(s: &str) -> &str {
|
|
let s = s.trim();
|
|
|
|
if s.len() < 2 {
|
|
return s;
|
|
}
|
|
|
|
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
|
|
&s[1..s.len() - 1]
|
|
} else {
|
|
s
|
|
}
|
|
}
|
|
|
|
// 安全地转义字符串
|
|
fn escape_js_string_for_single_quote(s: &str) -> String {
|
|
// 限制处理的字符串长度
|
|
if s.len() > 10240 {
|
|
return s[..10240].replace('\\', "\\\\").replace('\'', "\\'").into();
|
|
}
|
|
|
|
s.replace('\\', "\\\\")
|
|
.replace('\'', "\\'")
|
|
.replace('\n', "\\n") // 添加换行符转义
|
|
.replace('\r', "\\r") // 添加回车转义
|
|
.into()
|
|
}
|