From 62357443702a2378e2f414ca2f394e5d3d27c9c5 Mon Sep 17 00:00:00 2001 From: "github-action[bot]" Date: Sat, 7 Sep 2024 20:31:59 +0200 Subject: [PATCH] Update On Sat Sep 7 20:31:58 CEST 2024 --- .github/update.log | 1 + clash-meta/rules/provider/mrs_converter.go | 2 +- clash-nyanpasu/.github/workflows/ci.yml | 2 +- .../.github/workflows/deps-build-macos.yaml | 2 + clash-nyanpasu/.gitignore | 2 - clash-nyanpasu/CHANGELOG.md | 74 ++++ clash-nyanpasu/backend/tauri/.gitignore | 3 + clash-nyanpasu/backend/tauri/build.rs | 4 +- .../backend/tauri/overrides/nightly.conf.json | 2 +- .../backend/tauri/src/enhance/merge.rs | 356 +++++++++++++++++- clash-nyanpasu/backend/tauri/tauri.conf.json | 6 +- .../backend/tauri/templates/installer.nsi | 14 + .../frontend/interface/package.json | 2 +- clash-nyanpasu/frontend/nyanpasu/package.json | 7 +- .../src/components/app/app-drawer.tsx | 3 +- .../src/components/app/drawer-content.tsx | 4 +- .../app/modules/route-list-item.tsx | 6 +- .../src/components/base/content-display.tsx | 7 +- .../src/components/layout/animated-logo.tsx | 4 +- .../src/components/layout/layout-control.tsx | 4 +- .../src/components/layout/page-transition.tsx | 4 +- .../nyanpasu/src/components/logs/log-item.tsx | 4 +- .../components/profiles/modules/side-log.tsx | 6 +- .../components/profiles/profile-dialog.tsx | 72 ++-- .../src/components/profiles/profile-item.tsx | 3 +- .../profiles/profile-monaco-diff-viewer.tsx | 9 + .../profiles/profile-monaco-view.tsx | 107 ------ .../profiles/profile-monaco-viewer.tsx | 119 ++++++ .../profiles/runtime-config-diff-dialog.tsx | 78 ++-- .../src/components/profiles/script-dialog.tsx | 51 ++- .../src/components/proxies/delay-button.tsx | 8 +- .../src/components/proxies/delay-chip.tsx | 9 +- .../src/components/proxies/node-card.tsx | 4 +- .../setting/modules/hotkey-input.tsx | 8 +- .../frontend/nyanpasu/src/pages/_app.tsx | 6 +- .../frontend/nyanpasu/src/services/monaco.ts | 60 +-- .../nyanpasu/src/utils/monaco-yaml.worker.ts | 3 + .../frontend/nyanpasu/vite.config.ts | 32 +- clash-nyanpasu/frontend/ui/package.json | 4 +- .../components/baseDialog/index.tsx | 4 +- clash-nyanpasu/package.json | 4 +- clash-nyanpasu/pnpm-lock.yaml | 74 +++- clash-nyanpasu/scripts/package.json | 2 +- clash-nyanpasu/scripts/utils/env.ts | 2 +- echo/internal/cmgr/ms/ms.go | 8 +- echo/internal/conn/relay_conn.go | 79 ++-- echo/internal/conn/relay_conn_test.go | 5 +- echo/internal/metrics/metrics.go | 1 + echo/internal/metrics/ping.go | 25 +- echo/internal/web/js/rule_metrics.js | 230 +++++++++-- .../web/templates/_rule_metrics_dash.html | 4 +- echo/pkg/metric_reader/node.go | 2 +- echo/pkg/sub/clash.go | 190 ---------- echo/pkg/sub/clash_test.go | 63 ---- echo/pkg/sub/clash_types.go | 197 ---------- echo/pkg/sub/utils.go | 101 ----- mieru/Makefile | 2 +- .../package/mieru/amd64/debian/DEBIAN/control | 2 +- .../build/package/mieru/amd64/rpm/mieru.spec | 2 +- .../package/mieru/arm64/debian/DEBIAN/control | 2 +- .../build/package/mieru/arm64/rpm/mieru.spec | 2 +- .../package/mita/amd64/debian/DEBIAN/control | 2 +- mieru/build/package/mita/amd64/rpm/mita.spec | 2 +- .../package/mita/arm64/debian/DEBIAN/control | 2 +- mieru/build/package/mita/arm64/rpm/mita.spec | 2 +- mieru/docs/server-install.md | 16 +- mieru/docs/server-install.zh_CN.md | 16 +- mieru/pkg/appctl/server.go | 10 +- mieru/pkg/appctl/server_test.go | 1 + .../server_reject_no_port_bindings.json | 8 + mieru/pkg/cli/client.go | 12 +- mieru/pkg/cli/server.go | 10 +- mieru/pkg/protocolv2/mux.go | 1 + mieru/pkg/socks5/auth.go | 47 +++ mieru/pkg/socks5/socks5.go | 15 +- mieru/pkg/testtool/pipe.go | 32 +- mieru/pkg/version/current.go | 2 +- mihomo/rules/provider/mrs_converter.go | 2 +- .../model/cbi/passwall/client/global.lua | 6 + .../luci-app-passwall/luasrc/passwall/api.lua | 7 +- .../view/passwall/app_update/app_version.htm | 9 +- .../luci-app-passwall/po/zh-cn/passwall.po | 6 + shadowsocks-rust/Cargo.lock | 2 +- .../crates/shadowsocks/Cargo.toml | 2 +- small/luci-app-homeproxy/po/zh-cn | 1 + .../po/zh_Hans/homeproxy.po | 107 +++--- .../model/cbi/passwall/client/global.lua | 6 + .../luci-app-passwall/luasrc/passwall/api.lua | 7 +- .../view/passwall/app_update/app_version.htm | 9 +- small/luci-app-passwall/po/zh-cn/passwall.po | 6 + small/v2ray-geodata/Makefile | 4 +- .../v2rayN/ServiceLib/Common/FileManager.cs | 44 ++- .../ServiceLib/Handler/ConfigHandler.cs | 2 + .../ServiceLib/Handler/NoticeHandler.cs | 2 +- .../ServiceLib/Handler/WebDavHandler.cs | 162 ++++++++ v2rayn/v2rayN/ServiceLib/Models/Config.cs | 1 + .../v2rayN/ServiceLib/Models/ConfigItems.cs | 8 + .../v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 99 +++++ v2rayn/v2rayN/ServiceLib/Resx/ResUI.resx | 33 ++ .../v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 33 ++ .../v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 33 ++ v2rayn/v2rayN/ServiceLib/ServiceLib.csproj | 1 + .../ViewModels/BackupAndRestoreViewModel.cs | 151 ++++++++ .../ViewModels/CheckUpdateViewModel.cs | 17 +- .../ViewModels/MainWindowViewModel.cs | 18 + .../Views/CheckUpdateView.axaml.cs | 2 +- .../v2rayN/Views/BackupAndRestoreView.xaml | 239 ++++++++++++ .../v2rayN/Views/BackupAndRestoreView.xaml.cs | 63 ++++ .../v2rayN/v2rayN/Views/CheckUpdateView.xaml | 8 +- v2rayn/v2rayN/v2rayN/Views/MainWindow.xaml | 8 +- v2rayn/v2rayN/v2rayN/Views/MainWindow.xaml.cs | 22 +- .../v2rayN/v2rayN/Views/ProfilesView.xaml.cs | 1 - xray-core/core/core.go | 6 +- xray-core/infra/conf/xray.go | 47 --- xray-core/infra/conf/xray_test.go | 103 ----- yass/.github/ISSUE_TEMPLATE/bug_report.md | 45 +++ .../.github/ISSUE_TEMPLATE/feature_request.md | 18 + yass/third_party/tun2proxy/src/android.rs | 16 +- yass/third_party/tun2proxy/src/harmony.rs | 16 +- yass/third_party/tun2proxy/src/setup.rs | 22 +- yt-dlp/yt_dlp/extractor/samplefocus.py | 12 +- 121 files changed, 2380 insertions(+), 1297 deletions(-) create mode 100644 clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-monaco-diff-viewer.tsx delete mode 100644 clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-monaco-view.tsx create mode 100644 clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-monaco-viewer.tsx create mode 100644 clash-nyanpasu/frontend/nyanpasu/src/utils/monaco-yaml.worker.ts delete mode 100644 echo/pkg/sub/clash.go delete mode 100644 echo/pkg/sub/clash_test.go delete mode 100644 echo/pkg/sub/clash_types.go delete mode 100644 echo/pkg/sub/utils.go create mode 100644 mieru/pkg/appctl/testdata/server_reject_no_port_bindings.json create mode 100644 mieru/pkg/socks5/auth.go create mode 120000 small/luci-app-homeproxy/po/zh-cn create mode 100644 v2rayn/v2rayN/ServiceLib/Handler/WebDavHandler.cs create mode 100644 v2rayn/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs create mode 100644 v2rayn/v2rayN/v2rayN/Views/BackupAndRestoreView.xaml create mode 100644 v2rayn/v2rayN/v2rayN/Views/BackupAndRestoreView.xaml.cs create mode 100644 yass/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 yass/.github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/update.log b/.github/update.log index aadda73115..34d07be720 100644 --- a/.github/update.log +++ b/.github/update.log @@ -756,3 +756,4 @@ Update On Tue Sep 3 20:32:43 CEST 2024 Update On Wed Sep 4 20:31:01 CEST 2024 Update On Thu Sep 5 20:35:23 CEST 2024 Update On Fri Sep 6 20:34:58 CEST 2024 +Update On Sat Sep 7 20:31:47 CEST 2024 diff --git a/clash-meta/rules/provider/mrs_converter.go b/clash-meta/rules/provider/mrs_converter.go index edc24e7eea..dbbe51cb29 100644 --- a/clash-meta/rules/provider/mrs_converter.go +++ b/clash-meta/rules/provider/mrs_converter.go @@ -34,7 +34,7 @@ func ConvertToMrs(buf []byte, behavior P.RuleBehavior, format P.RuleFormat, w io } var encoder *zstd.Encoder - encoder, err = zstd.NewWriter(w) + encoder, err = zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.SpeedBestCompression)) if err != nil { return err } diff --git a/clash-nyanpasu/.github/workflows/ci.yml b/clash-nyanpasu/.github/workflows/ci.yml index 459c0115de..21aec4463d 100644 --- a/clash-nyanpasu/.github/workflows/ci.yml +++ b/clash-nyanpasu/.github/workflows/ci.yml @@ -75,7 +75,7 @@ jobs: run: pnpm install --no-frozen-lockfile - name: Prepare fronend - run: pnpm -r build && mkdir -p ./backend/tauri/.tmp/dist # Build frontend + run: pnpm -r build # Build frontend - name: Prepare sidecar and resources run: pnpm check - name: Lint diff --git a/clash-nyanpasu/.github/workflows/deps-build-macos.yaml b/clash-nyanpasu/.github/workflows/deps-build-macos.yaml index 2ac65bc75b..9ef753b504 100644 --- a/clash-nyanpasu/.github/workflows/deps-build-macos.yaml +++ b/clash-nyanpasu/.github/workflows/deps-build-macos.yaml @@ -110,6 +110,7 @@ jobs: TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }} + NODE_OPTIONS: "--max_old_space_size=4096" with: tagName: ${{ inputs.tag }} releaseName: "Clash Nyanpasu Dev" @@ -127,6 +128,7 @@ jobs: TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }} + NODE_OPTIONS: "--max_old_space_size=4096" run: | ${{ inputs.nightly == true && 'pnpm build:nightly --target aarch64-apple-darwin' || 'pnpm build --target aarch64-apple-darwin' }} pnpm upload:osx-aarch64 diff --git a/clash-nyanpasu/.gitignore b/clash-nyanpasu/.gitignore index c4fd896a52..eb97745211 100644 --- a/clash-nyanpasu/.gitignore +++ b/clash-nyanpasu/.gitignore @@ -16,5 +16,3 @@ tauri.preview.conf.json .idea *.tsbuildinfo - -**/.tmp diff --git a/clash-nyanpasu/CHANGELOG.md b/clash-nyanpasu/CHANGELOG.md index b627d6dcc1..01ab52dc71 100644 --- a/clash-nyanpasu/CHANGELOG.md +++ b/clash-nyanpasu/CHANGELOG.md @@ -1,3 +1,77 @@ +## [1.6.1] - 2024-09-07 + +### ✨ Features + +- **dock:** Try to setup macos dock handler by @greenhat616 + +- **enhance:** Finish all filter test suites by @greenhat616 + +- **enhance:** Add sequence filter support and partial test suite by @greenhat616 + +- **enhance:** Add complex filter syntax support by @greenhat616 + +- **monaco:** Add onValidation before submit, and close #1491 by @greenhat616 + +- **monaco:** Add yaml config prompt by @greenhat616 + +- **nsis:** Cleanup reg while uninstall by @greenhat616 + +- **service:** Add manual prompt for service uninstall, stop, start by @greenhat616 + +- **service:** Add a manual install prompt while service install failed by @greenhat616 + +- **tun:** Support auto-route while clash-rs support it by @greenhat616 + +- Use cross-rs to build aarch64 by @greenhat616 + +- Try to support linux aarch64 build by @greenhat616 + +### 🐛 Bug Fixes + +- **ci:** Update publish script by @greenhat616 + +- **dialog:** Position func err by @keiko233 + +- **nsis:** Cleanup app config and data dir if option is selected by @greenhat616 + +- **os:** Create no window by @greenhat616 + +- **shiki:** Shell lang loader by @greenhat616 + +- Monaco clash config prompt by @greenhat616 + +- Monaco url resolve issue by @greenhat616 + +- Try to resolve the yaml schema by @greenhat616 + +- Try to escape the string by @greenhat616 + +- Add service install error prompt by @greenhat616 + +- Shiki import by @greenhat616 + +- Try to fix create no window by @greenhat616 + +- Typo by @greenhat616 + +- Windows nightly build version issue by @greenhat616 + +- Build by @greenhat616 + +- Aarch build by @greenhat616 + +- Dont merge falsy theme settings by @greenhat616 + +### 🔨 Refactor + +- Use @monaco-editor/react instead by @greenhat616 + +- Service shoutcuts use core manager internal state by @greenhat616 + +--- + +**Full Changelog**: https://github.com/libnyanpasu/clash-nyanpasu/compare/v1.6.0...v1.6.1 + ## [1.6.0] - 2024-08-29 ### 💥 Breaking Changes diff --git a/clash-nyanpasu/backend/tauri/.gitignore b/clash-nyanpasu/backend/tauri/.gitignore index 7b128ffce5..374924112b 100644 --- a/clash-nyanpasu/backend/tauri/.gitignore +++ b/clash-nyanpasu/backend/tauri/.gitignore @@ -4,3 +4,6 @@ WixTools resources sidecar +tmp/ + +!/tmp/.gitkeep diff --git a/clash-nyanpasu/backend/tauri/build.rs b/clash-nyanpasu/backend/tauri/build.rs index f888ed57e0..c97f7a6aee 100644 --- a/clash-nyanpasu/backend/tauri/build.rs +++ b/clash-nyanpasu/backend/tauri/build.rs @@ -37,9 +37,9 @@ fn main() { let is_prerelase = !version.pre.is_empty(); println!("cargo:rustc-env=NYANPASU_VERSION={}", version); // Git Information - let (commit_hash, commit_author, commit_date) = if let Ok(true) = exists("./.tmp/git-info.json") + let (commit_hash, commit_author, commit_date) = if let Ok(true) = exists("./tmp/git-info.json") { - let mut git_info = read("./.tmp/git-info.json").unwrap(); + let mut git_info = read("./tmp/git-info.json").unwrap(); let git_info: GitInfo = simd_json::from_slice(&mut git_info).unwrap(); (git_info.hash, git_info.author, git_info.time) } else { diff --git a/clash-nyanpasu/backend/tauri/overrides/nightly.conf.json b/clash-nyanpasu/backend/tauri/overrides/nightly.conf.json index 1f9ccd6532..ea59b9150b 100644 --- a/clash-nyanpasu/backend/tauri/overrides/nightly.conf.json +++ b/clash-nyanpasu/backend/tauri/overrides/nightly.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/1.x/tooling/cli/schema.json", "package": { - "version": "1.6.1" + "version": "1.6.2" }, "tauri": { "updater": { diff --git a/clash-nyanpasu/backend/tauri/src/enhance/merge.rs b/clash-nyanpasu/backend/tauri/src/enhance/merge.rs index df370a5c46..3701c5367e 100644 --- a/clash-nyanpasu/backend/tauri/src/enhance/merge.rs +++ b/clash-nyanpasu/backend/tauri/src/enhance/merge.rs @@ -160,7 +160,12 @@ fn do_filter(logs: &mut Logs, config: &mut Value, field_str: &str, filter: &Valu let r#match = run_expr(logs, item, when); if r#match.unwrap_or(false) { for (key, value) in merge.iter() { - override_recursive(item.as_mapping_mut().unwrap(), key, value.clone()); + let item = item.as_mapping_mut().unwrap(); + if item.contains_key(key) { + override_recursive(item, key, value.clone()); + } else { + item.insert(key.clone(), value.clone()); + } } } }); @@ -181,25 +186,44 @@ fn do_filter(logs: &mut Logs, config: &mut Value, field_str: &str, filter: &Valu let key_str = key.as_str().unwrap(); // 对 key_str 做一下处理,跳过最后一个元素 let mut keys = key_str.split('.').collect::>(); - let last_key = if keys.len() > 1 { - keys.pop().unwrap() - } else { - key_str - }; + let last_key = if keys.len() > 1 { keys.pop() } else { None }; let key_str = keys.join("."); - if let Some(field) = find_field(item, &key_str) { - field.as_mapping_mut().unwrap().remove(last_key); + match last_key { + None => { + item.as_mapping_mut().unwrap().remove(key_str); + } + Some(last_key) => { + let field = find_field(item, &key_str); + if let Some(field) = field { + match field { + Value::Mapping(map) => { + map.remove(last_key); + } + Value::Sequence(list) + if last_key.parse::().is_ok() => + { + let index = last_key.parse::().unwrap(); + if index < list.len() { + list.remove(index); + } + } + _ => { + logs.info(format!("invalid key: {:#?}", last_key)); + } + } + } + } } } else { match item { Value::Sequence(list) if key.is_i64() => { - let index = key.as_i64().unwrap() as usize; - if index < list.len() { - list.remove(index); + let index = key.as_i64().unwrap(); + if index >= 0 && (index as usize) < list.len() { + list.remove(index as usize); } } _ => { - logs.warn(format!("invalid key: {:#?}", key)); + logs.info(format!("invalid key: {:#?}", key)); } } } @@ -294,7 +318,9 @@ pub fn use_merge(merge: Mapping, mut config: Mapping) -> ProcessOutput { } mod tests { + #[allow(unused_imports)] use pretty_assertions::{assert_eq, assert_ne}; + #[test] fn test_find_field() { let config = r" @@ -696,6 +722,312 @@ mod tests { assert_eq!(result.unwrap(), expected); } + #[test] + fn test_filter_when_and_merge() { + let merge = r" + filter__proxy-groups: + when: | + item.name == 'Spotify' + merge: + icon: 'https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png' + filter__wow: + when: | + item == 'wow' + merge: + item: 'wow'"; + let config = r#"proxy-groups: +- name: Spotify + type: select + proxies: + - Proxies + - DIRECT + - HK + - JP + - SG + - TW + - US +- name: Steam + type: select + proxies: + - Proxies + - DIRECT + - HK + - JP + - SG + - TW + - US +- name: Telegram + type: select + proxies: + - Proxies + - HK + - JP + - SG + - TW + - US"#; + let expected = r#"proxy-groups: +- name: Spotify + type: select + icon: https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png + proxies: + - Proxies + - DIRECT + - HK + - JP + - SG + - TW + - US +- name: Steam + type: select + proxies: + - Proxies + - DIRECT + - HK + - JP + - SG + - TW + - US +- name: Telegram + type: select + proxies: + - Proxies + - HK + - JP + - SG + - TW + - US"#; + let merge = serde_yaml::from_str::(merge).unwrap(); + let config = serde_yaml::from_str::(config).unwrap(); + let (result, logs) = super::use_merge(merge, config); + eprintln!("{:#?}\n\n{:#?}", logs, result); + assert_eq!(logs.len(), 1); + let expected = serde_yaml::from_str::(expected).unwrap(); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_filter_when_and_remove() { + let merge = r" + filter__proxies: + when: | + type(item) == 'table' and (item.type == 'ss' or item.type == 'hysteria2') + remove: + - name + - type + filter__list: # note that Lua table index starts from 1 + when: | + item[1] == 123 + remove: + - 0 + filter__wow: + when: | + item.flag == true + remove: + - test.1 + - good.should_remove + "; + let config = r#" + wow: + - test: + - 123 + - 456 + flag: true + - good: + should_remove: true + should_not_remove: true + flag: true + list: + - - 123 + - 456 + - 222 + - - 123 + - 456 + - 222 + proxies: + - 123 + - 555 + - name: "hysteria2" + type: hysteria2 + server: server.com + port: 443 + ports: 443-8443 + password: yourpassword + up: "30 Mbps" + down: "200 Mbps" + obfs: salamander # 默认为空,如果填写则开启obfs,目前仅支持salamander + obfs-password: yourpassword + + sni: server.com + skip-cert-verify: false + fingerprint: xxxx + alpn: + - h3 + ca: "./my.ca" + ca-str: "xyz" + - name: "hysteria2" + type: ss + server: server.com + port: 443 + ports: 443-8443 + password: yourpassword + up: "30 Mbps" + down: "200 Mbps" + obfs: salamander # 默认为空,如果填写则开启obfs,目前仅支持salamander + obfs-password: yourpassword + + sni: server.com + skip-cert-verify: false + fingerprint: xxxx + alpn: + - h3 + ca: "./my.ca" + ca-str: "xyz" + "#; + let expected = r#" + wow: + - test: + - 123 + flag: true + - good: + should_not_remove: true + flag: true + list: + - - 456 + - 222 + - - 456 + - 222 + proxies: + - 123 + - 555 + - server: server.com + port: 443 + ports: 443-8443 + password: yourpassword + up: "30 Mbps" + down: "200 Mbps" + obfs: salamander + obfs-password: yourpassword + sni: server.com + skip-cert-verify: false + fingerprint: xxxx + alpn: + - h3 + ca: "./my.ca" + ca-str: "xyz" + - server: server.com + port: 443 + ports: 443-8443 + password: yourpassword + up: "30 Mbps" + down: "200 Mbps" + obfs: salamander + obfs-password: yourpassword + sni: server.com + skip-cert-verify: false + fingerprint: xxxx + alpn: + - h3 + ca: "./my.ca" + ca-str: "xyz" + "#; + let merge = serde_yaml::from_str::(merge).unwrap(); + let config = serde_yaml::from_str::(config).unwrap(); + let (result, logs) = super::use_merge(merge, config); + eprintln!("{:#?}\n\n{:#?}", logs, result); + assert_eq!(logs.len(), 0); + let expected = serde_yaml::from_str::(expected).unwrap(); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_filter_sequence() { + let merge = r" + filter__proxy-groups: + - when: | + item.name == 'Spotify' + merge: + icon: 'https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png' + - when: | + item.name == 'Steam' + merge: + icon: 'https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Steam.png' + - when: | + item.name == 'Telegram' + merge: + icon: 'https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Telegram.png' + "; + let config = r#"proxy-groups: +- name: Spotify + type: select + proxies: + - Proxies + - DIRECT + - HK + - JP + - SG + - TW + - US +- name: Steam + type: select + proxies: + - Proxies + - DIRECT + - HK + - JP + - SG + - TW + - US +- name: Telegram + type: select + proxies: + - Proxies + - HK + - JP + - SG + - TW + - US"#; + let expected = r#"proxy-groups: +- name: Spotify + type: select + icon: https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Spotify.png + proxies: + - Proxies + - DIRECT + - HK + - JP + - SG + - TW + - US +- name: Steam + type: select + icon: https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Steam.png + proxies: + - Proxies + - DIRECT + - HK + - JP + - SG + - TW + - US +- name: Telegram + type: select + icon: https://raw.githubusercontent.com/Koolson/Qure/master/IconSet/Color/Telegram.png + proxies: + - Proxies + - HK + - JP + - SG + - TW + - US"#; + let merge = serde_yaml::from_str::(merge).unwrap(); + let config = serde_yaml::from_str::(config).unwrap(); + let (result, logs) = super::use_merge(merge, config); + eprintln!("{:#?}\n\n{:#?}", logs, result); + assert_eq!(logs.len(), 0); + let expected = serde_yaml::from_str::(expected).unwrap(); + assert_eq!(result.unwrap(), expected); + } + #[test] fn test_override_recursive() { let merge = r" diff --git a/clash-nyanpasu/backend/tauri/tauri.conf.json b/clash-nyanpasu/backend/tauri/tauri.conf.json index fa41e4cbe2..41ee97b2b1 100644 --- a/clash-nyanpasu/backend/tauri/tauri.conf.json +++ b/clash-nyanpasu/backend/tauri/tauri.conf.json @@ -1,13 +1,13 @@ { "package": { "productName": "Clash Nyanpasu", - "version": "1.6.0" + "version": "1.6.1" }, "build": { - "distDir": "./.tmp/dist", + "distDir": "./tmp/dist", "devPath": "http://localhost:3000/", "beforeDevCommand": "pnpm run web:dev", - "beforeBuildCommand": "pnpm run-p web:build generate:git-info && echo $(pwd) && rm -rf ./tauri/.tmp/dist && mv ../frontend/nyanpasu/dist ./tauri/.tmp/dist" + "beforeBuildCommand": "pnpm run-p web:build generate:git-info && echo $(pwd)" }, "tauri": { "systemTray": { diff --git a/clash-nyanpasu/backend/tauri/templates/installer.nsi b/clash-nyanpasu/backend/tauri/templates/installer.nsi index 493e723419..03e61e681a 100644 --- a/clash-nyanpasu/backend/tauri/templates/installer.nsi +++ b/clash-nyanpasu/backend/tauri/templates/installer.nsi @@ -778,10 +778,22 @@ FunctionEnd DetailPrint "Service directory does not exist, skipping stop and remove service directory" !macroend +!macro RemoveRegs + ; cleanup auto start registry keys + DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "Clash Nyanpasu" + DeleteRegValue HKLM "SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run" "Clash Nyanpasu" + ; cleanup custom protocol handler + DeleteRegKey HKCU "Software\Classes\clash" + DeleteRegKey HKCU "Software\Classes\clash-nyanpasu" + DeleteRegKey HKCR "clash" + DeleteRegKey HKCR "clash-nyanpasu" +!macroend + Section Uninstall !insertmacro GetProgramDataPath !insertmacro StopAndRemoveServiceDirectory !insertmacro CheckAllNyanpasuProcesses + !insertmacro RemoveRegs ; !insertmacro CheckIfAppIsRunning ; Delete the app directory and its content from disk @@ -834,6 +846,8 @@ Section Uninstall SetShellVarContext current RmDir /r "$APPDATA\${BUNDLEID}" RmDir /r "$LOCALAPPDATA\${BUNDLEID}" + RmDir /r "$APPDATA\Clash Nyanpasu" + RmDir /r "$LOCALAPPDATA\Clash Nyanpasu" ${EndIf} ${GetOptions} $CMDLINE "/P" $R0 diff --git a/clash-nyanpasu/frontend/interface/package.json b/clash-nyanpasu/frontend/interface/package.json index e130b1c561..209966bfe0 100644 --- a/clash-nyanpasu/frontend/interface/package.json +++ b/clash-nyanpasu/frontend/interface/package.json @@ -1,6 +1,6 @@ { "name": "@nyanpasu/interface", - "version": "1.6.0", + "version": "1.6.1", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", "require": { diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json index d0420d3286..22e14b6d53 100644 --- a/clash-nyanpasu/frontend/nyanpasu/package.json +++ b/clash-nyanpasu/frontend/nyanpasu/package.json @@ -1,6 +1,6 @@ { "name": "@nyanpasu/nyanpasu", - "version": "1.6.0", + "version": "1.6.1", "license": "GPL-3.0", "type": "module", "scripts": { @@ -16,12 +16,14 @@ "@generouted/react-router": "1.19.6", "@juggle/resize-observer": "3.4.0", "@material/material-color-utilities": "0.3.0", + "@monaco-editor/react": "4.6.0", "@mui/icons-material": "5.16.7", "@mui/lab": "5.0.0-alpha.173", "@mui/material": "5.16.7", "@nyanpasu/interface": "workspace:^", "@nyanpasu/ui": "workspace:^", "@tauri-apps/api": "1.6.0", + "@types/json-schema": "7.0.15", "ahooks": "3.8.1", "allotment": "1.20.2", "country-code-emoji": "2.3.0", @@ -29,6 +31,7 @@ "framer-motion": "12.0.0-alpha.1", "i18next": "23.14.0", "jotai": "2.9.3", + "json-schema": "0.4.0", "material-react-table": "2.13.3", "monaco-editor": "0.51.0", "mui-color-input": "4.0.0", @@ -55,7 +58,7 @@ "@vitejs/plugin-react": "4.3.1", "@vitejs/plugin-react-swc": "3.7.0", "clsx": "2.1.1", - "meta-json-schema": "github:libnyanpasu/meta-json-schema#main", + "meta-json-schema": "libnyanpasu/meta-json-schema#main", "monaco-yaml": "5.2.2", "nanoid": "5.0.7", "sass": "1.78.0", diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/app/app-drawer.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/app/app-drawer.tsx index 1311990302..9de1f77cf0 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/app/app-drawer.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/app/app-drawer.tsx @@ -1,6 +1,5 @@ import { AnimatePresence, motion } from "framer-motion"; import { useState } from "react"; -import { classNames } from "@/utils"; import getSystem from "@/utils/get-system"; import { MenuOpen } from "@mui/icons-material"; import { @@ -25,7 +24,7 @@ export const AppDrawer = () => { const DrawerTitle = () => { return (
(
{children ? ( diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/layout/animated-logo.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/layout/animated-logo.tsx index 67fe7dbf5a..75d3716ed2 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/layout/animated-logo.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/layout/animated-logo.tsx @@ -1,8 +1,8 @@ import { AnimatePresence, motion, Variants } from "framer-motion"; import { CSSProperties } from "react"; import LogoSvg from "@/assets/image/logo.svg?react"; -import { classNames } from "@/utils"; import { useNyanpasu } from "@nyanpasu/interface"; +import { cn } from "@nyanpasu/ui"; import styles from "./animated-logo.module.scss"; // @ts-expect-error framer-motion types is wrong @@ -59,7 +59,7 @@ export default function AnimatedLogo({ return ( { }, []); return ( -
+
{nyanpasuConfig?.always_on_top ? ( diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/layout/page-transition.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/layout/page-transition.tsx index da68b74586..d9b87e9ea6 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/layout/page-transition.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/layout/page-transition.tsx @@ -1,7 +1,7 @@ import { AnimatePresence, motion, Variant } from "framer-motion"; import { useLocation, useOutlet } from "react-router-dom"; -import { classNames } from "@/utils"; import { useNyanpasu } from "@nyanpasu/interface"; +import { cn } from "@nyanpasu/ui"; type PageVariantKey = "initial" | "visible" | "hidden"; @@ -60,7 +60,7 @@ export default function PageTransition({ className }: { className?: string }) { return ( { @@ -38,7 +38,7 @@ export const LogItem = ({ value }: { value: LogMessage }) => {

{ const { scripts } = filterProfiles(getProfiles.data?.items); return ( -

+
diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-dialog.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-dialog.tsx index 1ed5a20220..c6aaa07acc 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-dialog.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-dialog.tsx @@ -1,7 +1,10 @@ import { version } from "~/package.json"; import { useAsyncEffect } from "ahooks"; +import { type editor } from "monaco-editor"; import { createContext, + lazy, + Suspense, use, useEffect, useMemo, @@ -15,13 +18,16 @@ import { useForm, } from "react-hook-form-mui"; import { useTranslation } from "react-i18next"; +import { useLatest } from "react-use"; +import { message } from "@/utils/notification"; import { Divider, InputAdornment } from "@mui/material"; import { Profile, useClash } from "@nyanpasu/interface"; import { BaseDialog } from "@nyanpasu/ui"; import { LabelSwitch } from "../setting/modules/clash-field"; -import { ProfileMonacoView, ProfileMonacoViewRef } from "./profile-monaco-view"; import { ReadProfile } from "./read-profile"; +const ProfileMonacoViewer = lazy(() => import("./profile-monaco-viewer")); + export interface ProfileDialogProps { profile?: Profile.Item; open: boolean; @@ -82,11 +88,14 @@ export const ProfileDialog = ({ setIsEdit(!!profile); }, [profile]); - const commonProps = { - autoComplete: "off", - autoCorrect: "off", - fullWidth: true, - }; + const commonProps = useMemo( + () => ({ + autoComplete: "off", + autoCorrect: "off", + fullWidth: true, + }), + [], + ); const handleProfileSelected = (content: string) => { localProfile.current = content; @@ -94,7 +103,25 @@ export const ProfileDialog = ({ setLocalProfileMessage(""); }; + const [editor, setEditor] = useState({ + value: "", + language: "yaml", + }); + + const latestEditor = useLatest(editor); + + const editorMarks = useRef([]); + const editorHasError = () => + editorMarks.current.length > 0 && + editorMarks.current.some((m) => m.severity === 8); + const onSubmit = handleSubmit(async (form) => { + if (editorHasError()) { + message("Please fix the error before saving", { + type: "error", + }); + return; + } const toCreate = async () => { if (isRemote) { await createProfile(form); @@ -109,7 +136,7 @@ export const ProfileDialog = ({ }; const toUpdate = async () => { - const value = profileMonacoViewRef.current?.getValue() || ""; + const value = latestEditor.current.value; await setProfileFile(form.uid, value); await setProfiles(form.uid, form); }; @@ -128,13 +155,6 @@ export const ProfileDialog = ({ } }); - const profileMonacoViewRef = useRef(null); - - const [editor, setEditor] = useState({ - value: "", - language: "yaml", - }); - const dialogProps = isEdit && { contentStyle: { overflow: "hidden", @@ -296,15 +316,21 @@ export const ProfileDialog = ({ - + + {open && ( + + setEditor((editor) => ({ ...editor, value })) + } + onValidate={(marks) => (editorMarks.current = marks)} + language={editor.language} + /> + )} +
) : ( MetaInfo diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx index 128d2e780c..620277c454 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx @@ -1,5 +1,4 @@ import { useLockFn, useMemoizedFn, useSetState } from "ahooks"; -import clsx from "clsx"; import dayjs from "dayjs"; import { AnimatePresence, motion } from "framer-motion"; import { memo, useEffect, useMemo, useState } from "react"; @@ -253,7 +252,7 @@ export const ProfileItem = memo(function ProfileItem({
, +) { + return ; +} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-monaco-view.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-monaco-view.tsx deleted file mode 100644 index 3f3c5703c0..0000000000 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-monaco-view.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useUpdateEffect } from "ahooks"; -import { useAtomValue } from "jotai"; -import { nanoid } from "nanoid"; -import { forwardRef, useEffect, useImperativeHandle, useRef } from "react"; -import { OS } from "@/consts"; -import { monaco } from "@/services/monaco"; -import { themeMode } from "@/store"; - -export interface ProfileMonacoViewProps { - open: boolean; - value?: string; - language?: string; - className?: string; - readonly?: boolean; - schemaType?: "clash" | "merge"; -} - -export interface ProfileMonacoViewRef { - getValue: () => string | undefined; -} - -export const ProfileMonacoView = forwardRef(function ProfileMonacoView( - { - open, - value, - language, - readonly = false, - schemaType, - className, - }: ProfileMonacoViewProps, - ref, -) { - const mode = useAtomValue(themeMode); - - const monacoRef = useRef(null); - - const monacoEditorRef = useRef(null); - - const instanceRef = useRef(null); - - useEffect(() => { - const run = async () => { - const { monaco } = await import("@/services/monaco"); - monacoEditorRef.current = monaco; - - if (!monacoRef.current) { - return; - } - - instanceRef.current = monaco.editor.create(monacoRef.current, { - readOnly: readonly, - renderValidationDecorations: "on", - theme: mode === "light" ? "vs" : "vs-dark", - tabSize: language === "yaml" ? 2 : 4, - minimap: { enabled: false }, - automaticLayout: true, - fontLigatures: true, - smoothScrolling: true, - fontFamily: `'Cascadia Code NF', 'Cascadia Code', Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${ - OS === "windows" ? ", twemoji mozilla" : "" - }`, - quickSuggestions: { - strings: true, - comments: true, - other: true, - }, - }); - const uri = monaco.Uri.parse( - `${nanoid()}.${!!schemaType ? `${schemaType}.` : ""}.${language}`, - ); - const model = monaco.editor.createModel(value || "", language, uri); - instanceRef.current.setModel(model); - }; - if (open) { - run().catch(console.error); - } - return () => { - instanceRef.current?.dispose(); - }; - }, [language, mode, open, readonly, schemaType, value]); - - useImperativeHandle(ref, () => ({ - getValue: () => instanceRef.current?.getValue(), - })); - - useUpdateEffect(() => { - const model = instanceRef.current?.getModel(); - - if (!model || !language) { - return; - } - - monacoEditorRef.current?.editor.setModelLanguage(model, language); - }, [language]); - - useUpdateEffect(() => { - const model = instanceRef.current?.getModel(); - - if (!model || !value) { - return; - } - - model.setValue(value); - }, [value]); - - return
; -}); diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-monaco-viewer.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-monaco-viewer.tsx new file mode 100644 index 0000000000..372809b41b --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-monaco-viewer.tsx @@ -0,0 +1,119 @@ +import { OS } from "@/consts"; +import "@/services/monaco"; +import { useAtomValue } from "jotai"; +import { type JSONSchema7 } from "json-schema"; +import nyanpasuMergeSchema from "meta-json-schema/schemas/clash-nyanpasu-merge-json-schema.json"; +import clashMetaSchema from "meta-json-schema/schemas/meta-json-schema.json"; +import { type editor } from "monaco-editor"; +import { configureMonacoYaml } from "monaco-yaml"; +import { nanoid } from "nanoid"; +import { useCallback, useMemo } from "react"; +// schema +import { themeMode } from "@/store"; +import MonacoEditor, { type Monaco } from "@monaco-editor/react"; +import { cn } from "@nyanpasu/ui"; + +export interface ProfileMonacoViewProps { + value?: string; + onChange?: (value: string) => void; + language?: string; + className?: string; + readonly?: boolean; + schemaType?: "clash" | "merge"; + onValidate?: (markers: editor.IMarker[]) => void; +} + +export interface ProfileMonacoViewRef { + getValue: () => string | undefined; +} + +let initd = false; + +export const beforeEditorMount = (monaco: Monaco) => { + if (!initd) { + monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + target: monaco.languages.typescript.ScriptTarget.ES2020, + allowNonTsExtensions: true, + allowJs: true, + }); + console.log(clashMetaSchema); + console.log(nyanpasuMergeSchema); + configureMonacoYaml(monaco, { + validate: true, + enableSchemaRequest: true, + completion: true, + schemas: [ + { + uri: "http://example.com/schema-name.json", + fileMatch: ["**/*.clash.yaml"], + // @ts-expect-error JSONSchema7 as JSONSchema + schema: clashMetaSchema as JSONSchema7, + }, + { + uri: "http://example.com/schema-name.json", + fileMatch: ["**/*.merge.yaml"], + // @ts-expect-error JSONSchema7 as JSONSchema + schema: nyanpasuMergeSchema as JSONSchema7, + }, + ], + }); + } + initd = true; +}; + +export default function ProfileMonacoViewer({ + value, + language, + readonly = false, + schemaType, + className, + onValidate, + ...others +}: ProfileMonacoViewProps) { + const mode = useAtomValue(themeMode); + + const path = useMemo( + () => `${nanoid()}.${!!schemaType ? `${schemaType}.` : ""}${language}`, + [schemaType, language], + ); + + const onChange = useCallback( + (value: string | undefined) => { + if (value && others.onChange) { + others.onChange(value); + } + }, + [others], + ); + + return ( + + ); +} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/runtime-config-diff-dialog.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/runtime-config-diff-dialog.tsx index cfcdfedfc9..b5459add63 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/runtime-config-diff-dialog.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/runtime-config-diff-dialog.tsx @@ -1,12 +1,14 @@ import { useAtomValue } from "jotai"; -import { useEffect, useRef, useState } from "react"; +import { nanoid } from "nanoid"; +import { lazy, Suspense, useMemo } from "react"; import { useTranslation } from "react-i18next"; import useSWR from "swr"; -import { monaco } from "@/services/monaco"; import { themeMode } from "@/store"; import { getRuntimeYaml, useClash } from "@nyanpasu/interface"; import { BaseDialog, cn } from "@nyanpasu/ui"; +const MonacoDiffEditor = lazy(() => import("./profile-monaco-diff-viewer")); + export type RuntimeConfigDiffDialogProps = { open: boolean; onClose: () => void; @@ -20,17 +22,12 @@ export default function RuntimeConfigDiffDialog({ const { getProfiles, getProfileFile } = useClash(); const currentProfileUid = getProfiles.data?.current; const mode = useAtomValue(themeMode); - const [loaded, setLoaded] = useState(false); - const { - data: runtimeConfig, - isLoading: isLoadingRuntimeConfig, - error: errorRuntimeConfig, - } = useSWR(open ? "/getRuntimeConfigYaml" : null, getRuntimeYaml); - const { - data: profileConfig, - isLoading: isLoadingProfileConfig, - error: errorProfileConfig, - } = useSWR( + const { data: runtimeConfig, isLoading: isLoadingRuntimeConfig } = useSWR( + open ? "/getRuntimeConfigYaml" : null, + getRuntimeYaml, + {}, + ); + const { data: profileConfig, isLoading: isLoadingProfileConfig } = useSWR( open ? `/readProfileFile?uid=${currentProfileUid}` : null, async (key) => { const url = new URL(key, window.location.origin); @@ -41,35 +38,12 @@ export default function RuntimeConfigDiffDialog({ refreshInterval: 0, }, ); - const monacoRef = useRef(null); - const editorRef = useRef(null); - const domRef = useRef(null); - useEffect(() => { - if (open && runtimeConfig && profileConfig) { - console.log("init monaco"); - const run = async () => { - const { monaco } = await import("@/services/monaco"); - monacoRef.current = monaco; - editorRef.current = monaco.editor.createDiffEditor(domRef.current!, { - theme: mode === "light" ? "vs" : "vs-dark", - minimap: { enabled: false }, - automaticLayout: true, - readOnly: true, - }); - editorRef.current.setModel({ - original: monaco.editor.createModel(profileConfig, "yaml"), - modified: monaco.editor.createModel(runtimeConfig, "yaml"), - }); - setLoaded(true); - }; - run().catch(console.error); - } - return () => { - monacoRef.current = null; - editorRef.current?.dispose(); - setLoaded(false); - }; - }, [mode, open, runtimeConfig, profileConfig]); + + const loaded = !isLoadingRuntimeConfig && !isLoadingProfileConfig; + + const originalModelPath = useMemo(() => `${nanoid()}.clash.yaml`, []); + const modifiedModelPath = useMemo(() => `${nanoid()}.runtime.yaml`, []); + if (!currentProfileUid) { return null; } @@ -86,7 +60,25 @@ export default function RuntimeConfigDiffDialog({ 原始配置 运行配置
-
+
+ + {loaded && ( + + )} + +
); diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/script-dialog.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/script-dialog.tsx index 8e63543605..ba8d6d6ad1 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/script-dialog.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/script-dialog.tsx @@ -1,14 +1,17 @@ import { useAsyncEffect, useReactive } from "ahooks"; -import { useEffect, useRef, useState } from "react"; +import { type editor } from "monaco-editor"; +import { lazy, Suspense, useEffect, useRef, useState } from "react"; import { SelectElement, TextFieldElement, useForm } from "react-hook-form-mui"; import { useTranslation } from "react-i18next"; +import { message } from "@/utils/notification"; import { Divider } from "@mui/material"; import { Profile, useClash } from "@nyanpasu/interface"; import { BaseDialog, BaseDialogProps } from "@nyanpasu/ui"; import LanguageChip from "./modules/language-chip"; -import { ProfileMonacoView, ProfileMonacoViewRef } from "./profile-monaco-view"; import { getLanguage } from "./utils"; +const ProfileMonacoViewer = lazy(() => import("./profile-monaco-viewer")); + const formCommonProps = { autoComplete: "off", autoCorrect: "off", @@ -82,8 +85,6 @@ export const ScriptDialog = ({ const [openMonaco, setOpenMonaco] = useState(false); - const profileMonacoViewRef = useRef(null); - const editor = useReactive<{ value: string; language: string; @@ -94,10 +95,22 @@ export const ScriptDialog = ({ rawType: "merge", }); + const editorMarks = useRef([]); + const editorHasError = () => + editorMarks.current.length > 0 && + editorMarks.current.some((m) => m.severity === 8); + const onSubmit = form.handleSubmit(async (data) => { + if (editorHasError()) { + message("Please fix the error before submitting", { + type: "error", + }); + return; + } + convertTypeMapping(data); - const editorValue = profileMonacoViewRef.current?.getValue(); + const editorValue = editor.value; if (!editorValue) { return; @@ -216,16 +229,24 @@ export const ScriptDialog = ({ - + + {openMonaco && ( + { + editor.value = value; + }} + language={editor.language} + onValidate={(marks) => { + editorMarks.current = marks; + }} + schemaType={ + editor.rawType === Profile.Type.Merge ? "merge" : undefined + } + /> + )} +
); diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/delay-button.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/delay-button.tsx index 55406246fe..e9b3470cff 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/delay-button.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/delay-button.tsx @@ -1,7 +1,6 @@ import { useDebounceFn, useLockFn } from "ahooks"; import { memo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { classNames } from "@/utils"; import { Bolt, Done } from "@mui/icons-material"; import { alpha, @@ -10,6 +9,7 @@ import { Tooltip, useTheme, } from "@mui/material"; +import { cn } from "@nyanpasu/ui"; export const DelayButton = memo(function DelayButton({ onClick, @@ -66,7 +66,7 @@ export const DelayButton = memo(function DelayButton({ onClick={handleClick} > { @@ -32,9 +32,9 @@ export default function HotkeyInput({ const [keys, setKeys] = useState(value || []); return (
-
+
{ return (

Oops!

Something went wrong... Caught at _app error boundary.

diff --git a/clash-nyanpasu/frontend/nyanpasu/src/services/monaco.ts b/clash-nyanpasu/frontend/nyanpasu/src/services/monaco.ts index 64e183d0fb..4dc2c57f29 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/services/monaco.ts +++ b/clash-nyanpasu/frontend/nyanpasu/src/services/monaco.ts @@ -1,37 +1,45 @@ -import nyanpasuMergeSchema from "meta-json-schema/schemas/clash-nyanpasu-merge-json-schema.json"; -import clashMetaSchema from "meta-json-schema/schemas/meta-json-schema.json"; -import { configureMonacoYaml } from "monaco-yaml"; // features // langs import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js"; import "monaco-editor/esm/vs/basic-languages/lua/lua.contribution.js"; import "monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js"; import "monaco-editor/esm/vs/editor/editor.all.js"; -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; // language services +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; import "monaco-editor/esm/vs/language/typescript/monaco.contribution.js"; +import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; +import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; +import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; +// workers +import yamlWorker from "@/utils/monaco-yaml.worker?worker"; +// others +import { loader } from "@monaco-editor/react"; -monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ - target: monaco.languages.typescript.ScriptTarget.ES2020, - allowNonTsExtensions: true, - allowJs: true, -}); +self.MonacoEnvironment = { + getWorker(_, label) { + switch (label) { + case "json": + return new jsonWorker(); + case "typescript": + case "javascript": + return new tsWorker(); + case "yaml": + return new yamlWorker(); + default: + return new editorWorker(); + } + }, +}; -configureMonacoYaml(monaco, { - validate: true, - enableSchemaRequest: true, - schemas: [ - { - fileMatch: ["**/*.clash.yaml"], - // @ts-expect-error monaco-yaml parse issue - schema: clashMetaSchema, - }, - { - fileMatch: ["**/*.merge.yaml"], - // @ts-expect-error monaco-yaml parse issue - schema: nyanpasuMergeSchema, - }, - ], -}); +loader.config({ monaco }); -export { monaco }; +loader + .init() + .then(() => { + console.log("Monaco is ready"); + }) + .catch((error) => { + console.error("Monaco initialization failed", error); + }); + +export {}; diff --git a/clash-nyanpasu/frontend/nyanpasu/src/utils/monaco-yaml.worker.ts b/clash-nyanpasu/frontend/nyanpasu/src/utils/monaco-yaml.worker.ts new file mode 100644 index 0000000000..47ccfc9868 --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/utils/monaco-yaml.worker.ts @@ -0,0 +1,3 @@ +// This file just to fix https://github.com/remcohaszing/monaco-yaml?tab=readme-ov-file#why-doesnt-it-work-with-vite + +import "monaco-yaml/yaml.worker.js"; diff --git a/clash-nyanpasu/frontend/nyanpasu/vite.config.ts b/clash-nyanpasu/frontend/nyanpasu/vite.config.ts index f184835405..7dec3cc9a9 100644 --- a/clash-nyanpasu/frontend/nyanpasu/vite.config.ts +++ b/clash-nyanpasu/frontend/nyanpasu/vite.config.ts @@ -3,10 +3,10 @@ import AutoImport from "unplugin-auto-import/vite"; import IconsResolver from "unplugin-icons/resolver"; import Icons from "unplugin-icons/vite"; import { defineConfig } from "vite"; -import monaco from "vite-plugin-monaco-editor"; import sassDts from "vite-plugin-sass-dts"; import svgr from "vite-plugin-svgr"; import tsconfigPaths from "vite-tsconfig-paths"; +// import monaco from "vite-plugin-monaco-editor"; import generouted from "@generouted/react-router/plugin"; // import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react-swc"; @@ -68,15 +68,15 @@ export default defineConfig(({ command }) => { }), generouted(), sassDts({ esmExport: true }), - monaco({ - languageWorkers: ["editorWorkerService", "typescript"], - customWorkers: [ - { - label: "yaml", - entry: "monaco-yaml/yaml.worker", - }, - ], - }), + // monaco({ + // languageWorkers: ["editorWorkerService", "typescript", "json"], + // customWorkers: [ + // { + // label: "yaml", + // entry: "monaco-yaml/yaml.worker", + // }, + // ], + // }), isDev && devtools(), ], resolve: { @@ -95,7 +95,17 @@ export default defineConfig(({ command }) => { pure: isDev || IS_NIGHTLY ? [] : ["console.log"], }, build: { - outDir: "dist", + outDir: "../../backend/tauri/tmp/dist", + rollupOptions: { + output: { + manualChunks: { + jsonWorker: [`monaco-editor/esm/vs/language/json/json.worker`], + tsWorker: [`monaco-editor/esm/vs/language/typescript/ts.worker`], + editorWorker: [`monaco-editor/esm/vs/editor/editor.worker`], + yamlWorker: [`monaco-yaml/yaml.worker`], + }, + }, + }, emptyOutDir: true, sourcemap: isDev || IS_NIGHTLY ? "inline" : false, }, diff --git a/clash-nyanpasu/frontend/ui/package.json b/clash-nyanpasu/frontend/ui/package.json index f88fed5072..daaa7051d8 100644 --- a/clash-nyanpasu/frontend/ui/package.json +++ b/clash-nyanpasu/frontend/ui/package.json @@ -1,6 +1,6 @@ { "name": "@nyanpasu/ui", - "version": "1.6.0", + "version": "1.6.1", "type": "module", "exports": { ".": "./dist/index.js", @@ -44,6 +44,6 @@ "sass": "1.78.0", "tailwind-merge": "2.5.2", "typescript-plugin-css-modules": "5.1.0", - "vite-plugin-dts": "4.1.0" + "vite-plugin-dts": "4.1.1" } } diff --git a/clash-nyanpasu/frontend/ui/src/materialYou/components/baseDialog/index.tsx b/clash-nyanpasu/frontend/ui/src/materialYou/components/baseDialog/index.tsx index 8868eb78ba..a9b2c3f394 100644 --- a/clash-nyanpasu/frontend/ui/src/materialYou/components/baseDialog/index.tsx +++ b/clash-nyanpasu/frontend/ui/src/materialYou/components/baseDialog/index.tsx @@ -118,7 +118,7 @@ export const BaseDialog = ({ {!full && ( = 0.21.0 < 1' + + '@monaco-editor/react@4.6.0': + resolution: {integrity: sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: npm:react@rc + react-dom: npm:react-dom@rc + '@mui/base@5.0.0-beta.40': resolution: {integrity: sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==} engines: {node: '>=12.0.0'} @@ -2498,6 +2519,9 @@ packages: '@types/js-cookie@2.2.7': resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -4622,6 +4646,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -4661,8 +4688,8 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - knip@5.29.2: - resolution: {integrity: sha512-NfJ3VDyV7gHvI4lVmr9PQCvC4lvrnTdaRMmtHIVBWB2GWWKj86uTw8Yfnp07M+fQeqOnX3AGPG8hjXHPlE1MEw==} + knip@5.30.0: + resolution: {integrity: sha512-QDpxtXosXK3OBnmWC2LJudjJROozAXyGzSi+aTuEx/Pf9/OKjmegQWix+X6uBYhPbMb8YEFcKWvI7qBnQCkIEA==} engines: {node: '>=18.6.0'} hasBin: true peerDependencies: @@ -6090,6 +6117,9 @@ packages: stacktrace-js@2.0.2: resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + store2@2.14.3: resolution: {integrity: sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg==} @@ -6643,8 +6673,8 @@ packages: vue: optional: true - vite-plugin-dts@4.1.0: - resolution: {integrity: sha512-sRlmt9k2q8MrX4F2058N3KmB6WyJ3Ao6QaExOv1X99F3j0GhPziEz1zscWQ1q2r1PeFc96L7GIUu8Pl2DPr2Hg==} + vite-plugin-dts@4.1.1: + resolution: {integrity: sha512-SxYXwJQbAZ1IMtGEcOuzzZtDWCdcV2JkU7esvpPA8E5tIWVcJB42rZwN9EdULicWGLfaXrUgPIGVSidXBTae2Q==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: typescript: '*' @@ -7891,6 +7921,18 @@ snapshots: '@microsoft/tsdoc@0.15.0': {} + '@monaco-editor/loader@1.4.0(monaco-editor@0.51.0)': + dependencies: + monaco-editor: 0.51.0 + state-local: 1.0.7 + + '@monaco-editor/react@4.6.0(monaco-editor@0.51.0)(react-dom@19.0.0-rc-e948a5ac-20240807(react@19.0.0-rc-e948a5ac-20240807))(react@19.0.0-rc-e948a5ac-20240807)': + dependencies: + '@monaco-editor/loader': 1.4.0(monaco-editor@0.51.0) + monaco-editor: 0.51.0 + react: 19.0.0-rc-e948a5ac-20240807 + react-dom: 19.0.0-rc-e948a5ac-20240807(react@19.0.0-rc-e948a5ac-20240807) + '@mui/base@5.0.0-beta.40(react-dom@19.0.0-rc-e948a5ac-20240807(react@19.0.0-rc-e948a5ac-20240807))(react@19.0.0-rc-e948a5ac-20240807)(types-react@19.0.0-rc.1)': dependencies: '@babel/runtime': 7.24.8 @@ -8873,6 +8915,8 @@ snapshots: '@types/js-cookie@2.2.7': {} + '@types/json-schema@7.0.15': {} + '@types/json5@0.0.29': {} '@types/jsonfile@6.1.4': @@ -9111,7 +9155,7 @@ snapshots: agent-base@7.1.1: dependencies: - debug: 4.3.4 + debug: 4.3.7 transitivePeerDependencies: - supports-color @@ -11267,6 +11311,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-safe@5.0.1: @@ -11305,7 +11351,7 @@ snapshots: kind-of@6.0.3: {} - knip@5.29.2(@types/node@22.5.4)(typescript@5.5.4): + knip@5.30.0(@types/node@22.5.4)(typescript@5.5.4): dependencies: '@nodelib/fs.walk': 1.2.8 '@snyk/github-codeowners': 1.1.0 @@ -12839,6 +12885,8 @@ snapshots: stack-generator: 2.0.10 stacktrace-gps: 3.1.2 + state-local@1.0.7: {} + store2@2.14.3: {} string-argv@0.3.2: {} @@ -13546,14 +13594,14 @@ snapshots: react: 19.0.0-rc-e948a5ac-20240807 react-dom: 19.0.0-rc-e948a5ac-20240807(react@19.0.0-rc-e948a5ac-20240807) - vite-plugin-dts@4.1.0(@types/node@22.5.4)(rollup@4.21.0)(typescript@5.5.4)(vite@5.4.3(@types/node@22.5.4)(less@4.2.0)(sass@1.78.0)(stylus@0.62.0)): + vite-plugin-dts@4.1.1(@types/node@22.5.4)(rollup@4.21.0)(typescript@5.5.4)(vite@5.4.3(@types/node@22.5.4)(less@4.2.0)(sass@1.78.0)(stylus@0.62.0)): dependencies: '@microsoft/api-extractor': 7.47.4(@types/node@22.5.4) '@rollup/pluginutils': 5.1.0(rollup@4.21.0) '@volar/typescript': 2.4.0 '@vue/language-core': 2.0.29(typescript@5.5.4) compare-versions: 6.1.1 - debug: 4.3.6 + debug: 4.3.7 kolorist: 1.8.0 local-pkg: 0.5.0 magic-string: 0.30.11 diff --git a/clash-nyanpasu/scripts/package.json b/clash-nyanpasu/scripts/package.json index f6151edcf6..07d0c5c82b 100644 --- a/clash-nyanpasu/scripts/package.json +++ b/clash-nyanpasu/scripts/package.json @@ -1,7 +1,7 @@ { "name": "@nyanpasu/scripts", "type": "module", - "version": "1.6.0", + "version": "1.6.1", "dependencies": { "@actions/github": "6.0.0", "@types/figlet": "1.5.8", diff --git a/clash-nyanpasu/scripts/utils/env.ts b/clash-nyanpasu/scripts/utils/env.ts index 701b7342ae..25d779149b 100644 --- a/clash-nyanpasu/scripts/utils/env.ts +++ b/clash-nyanpasu/scripts/utils/env.ts @@ -7,7 +7,7 @@ export const GITHUB_PROXY = "https://mirror.ghproxy.com/"; export const GITHUB_TOKEN = process.env.GITHUB_TOKEN; export const TEMP_DIR = path.join(cwd, "node_modules/.verge"); export const MANIFEST_VERSION_PATH = path.join(MANIFEST_DIR, "version.json"); -export const TAURI_APP_TEMP_DIR = path.join(TAURI_APP_DIR, ".tmp"); +export const TAURI_APP_TEMP_DIR = path.join(TAURI_APP_DIR, "tmp"); export const GIT_SUMMARY_INFO_PATH = path.join( TAURI_APP_TEMP_DIR, "git-info.json", diff --git a/echo/internal/cmgr/ms/ms.go b/echo/internal/cmgr/ms/ms.go index fd7b38ad16..f42c47fbbc 100644 --- a/echo/internal/cmgr/ms/ms.go +++ b/echo/internal/cmgr/ms/ms.go @@ -91,11 +91,11 @@ func (ms *MetricsStore) initDB() error { remote TEXT, ping_latency INTEGER, tcp_connection_count INTEGER, - tcp_handshake_duration INTEGER, - tcp_network_transmit_bytes INTEGER, + tcp_handshake_duration BIGINT, + tcp_network_transmit_bytes BIGINT, udp_connection_count INTEGER, - udp_handshake_duration INTEGER, - udp_network_transmit_bytes INTEGER, + udp_handshake_duration BIGINT, + udp_network_transmit_bytes BIGINT, PRIMARY KEY (timestamp, label, remote) ) `); err != nil { diff --git a/echo/internal/conn/relay_conn.go b/echo/internal/conn/relay_conn.go index f6fa62724a..1a792267e5 100644 --- a/echo/internal/conn/relay_conn.go +++ b/echo/internal/conn/relay_conn.go @@ -106,22 +106,28 @@ func (rc *relayConnImpl) Transport() error { defer func() { err := rc.Close() if err != nil { - rc.l.Errorf("error closing relay connection: %s", err.Error()) + rc.l.Errorf("Error closing Transport connection: %s", err) } }() + rc.l = rc.l.Named(shortHashSHA256(rc.GetFlow())) - rc.l.Debugf("transport start") - c1 := newInnerConn(rc.clientConn, rc) - c1.l = rc.l.Named("client") - c2 := newInnerConn(rc.remoteConn, rc) - c2.l = rc.l.Named("remote") + rc.l.Debugf("Starting transport: %s <-> %s", rc.clientConn.RemoteAddr(), rc.remoteConn.RemoteAddr()) + + clientConn := newInnerConn(rc.clientConn, rc) + clientConn.l = rc.l.Named("client") + remoteConn := newInnerConn(rc.remoteConn, rc) + remoteConn.l = rc.l.Named("remote") + rc.StartTime = time.Now().Local() - err := copyConn(c1, c2) - if err != nil { - rc.l.Errorf("transport error: %s", err.Error()) - } - rc.l.Debugf("transport end: stats: %s", rc.Stats.String()) + err := copyConn(clientConn, remoteConn, rc.l) rc.EndTime = time.Now().Local() + + if err != nil { + // wrap error with client and remote address + err = fmt.Errorf("(client: %s, remote: %s) %w", clientConn.RemoteAddr(), remoteConn.RemoteAddr(), err) + } + rc.l.Debugf("Transport ended Connection details: client=%s, remote=%s, duration=%v, stats=%s", + clientConn.RemoteAddr(), remoteConn.RemoteAddr(), rc.EndTime.Sub(rc.StartTime), rc.Stats) return err } @@ -156,22 +162,6 @@ func (rc *relayConnImpl) GetConnType() string { return rc.ConnType } -func combineErrorsAndMuteIDLE(err1, err2 error) error { - if err1 == ErrIdleTimeout { - err1 = nil - } - if err2 == ErrIdleTimeout { - return nil - } - if err1 != nil && err2 != nil { - return errors.Join(err1, err2) - } - if err1 != nil { - return err1 - } - return err2 -} - type Stats struct { Up int64 Down int64 @@ -184,10 +174,10 @@ func (s *Stats) Record(up, down int64) { } func (s *Stats) String() string { - return fmt.Sprintf("up: %s, down: %s, latency: %s", + return fmt.Sprintf("↑%s ↓%s ⏱%dms", bytes.PrettyByteSize(float64(s.Up)), bytes.PrettyByteSize(float64(s.Down)), - fmt.Sprintf("%d ms", s.HandShakeLatency.Milliseconds()), + s.HandShakeLatency.Milliseconds(), ) } @@ -201,7 +191,7 @@ type innerConn struct { } func newInnerConn(conn net.Conn, rc *relayConnImpl) *innerConn { - return &innerConn{Conn: conn, rc: rc, lastActive: time.Now().Local()} + return &innerConn{Conn: conn, rc: rc, lastActive: time.Now().Local(), l: zap.S()} } func (c *innerConn) recordStats(n int, isRead bool) { @@ -279,26 +269,47 @@ func shortHashSHA256(input string) string { return hex.EncodeToString(hash)[:shortHashLength] } -func copyConn(conn1, conn2 *innerConn) error { +func copyConn(conn1, conn2 *innerConn, l *zap.SugaredLogger) error { buf1 := buffer.BufferPool.Get() defer buffer.BufferPool.Put(buf1) buf2 := buffer.BufferPool.Get() defer buffer.BufferPool.Put(buf2) errCH := make(chan error, 1) - // copy conn1 to conn2,read from conn1 and write to conn2 + // copy conn1 to conn2, read from conn1 and write to conn2 go func() { _, err := io.CopyBuffer(conn2, conn1, buf1) _ = conn2.CloseWrite() // all data is written to conn2 now, so close the write side of conn2 to send eof + if err != nil { + conn1.l.Debugf("Error in conn1 -> conn2 direction: read from %s, write to %s, error: %v", conn1.RemoteAddr(), conn2.RemoteAddr(), err) + } errCH <- err }() - // reverse copy conn2 to conn1,read from conn2 and write to conn1 + // reverse copy conn2 to conn1, read from conn2 and write to conn1 _, err := io.CopyBuffer(conn1, conn2, buf2) + if err != nil { + l.Debugf("Error in conn2 -> conn1 direction: read from %s, write to %s, error: %v", conn2.RemoteAddr(), conn1.RemoteAddr(), err) + } _ = conn1.CloseWrite() - err2 := <-errCH _ = conn1.CloseRead() _ = conn2.CloseRead() return combineErrorsAndMuteIDLE(err, err2) } + +func combineErrorsAndMuteIDLE(err1, err2 error) error { + if err1 == ErrIdleTimeout { + err1 = nil + } + if err2 == ErrIdleTimeout { + return nil + } + if err1 != nil && err2 != nil { + return errors.Join(err1, err2) + } + if err1 != nil { + return err1 + } + return err2 +} diff --git a/echo/internal/conn/relay_conn_test.go b/echo/internal/conn/relay_conn_test.go index 1c7acecc96..049a1a5ef1 100644 --- a/echo/internal/conn/relay_conn_test.go +++ b/echo/internal/conn/relay_conn_test.go @@ -10,6 +10,7 @@ import ( "github.com/Ehco1996/ehco/internal/lb" "github.com/Ehco1996/ehco/internal/relay/conf" "github.com/stretchr/testify/assert" + "go.uber.org/zap" ) func TestInnerConn_ReadWrite(t *testing.T) { @@ -106,7 +107,7 @@ func TestCopyTCPConn(t *testing.T) { done := make(chan struct{}) go func() { - if err := copyConn(c1, c2); err != nil { + if err := copyConn(c1, c2, zap.S()); err != nil { t.Log(err) } done <- struct{}{} @@ -167,7 +168,7 @@ func TestCopyUDPConn(t *testing.T) { done := make(chan struct{}) go func() { - if err := copyConn(c1, c2); err != nil { + if err := copyConn(c1, c2, zap.S()); err != nil { t.Log(err) } done <- struct{}{} diff --git a/echo/internal/metrics/metrics.go b/echo/internal/metrics/metrics.go index 083120c379..1eec30dc2c 100644 --- a/echo/internal/metrics/metrics.go +++ b/echo/internal/metrics/metrics.go @@ -67,6 +67,7 @@ var ( }, []string{"label", "conn_type", "remote"}) HandShakeDurationMilliseconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Buckets: msBuckets, Subsystem: METRIC_SUBSYSTEM_TRAFFIC, Namespace: METRIC_NS, Name: "handshake_duration_milliseconds", diff --git a/echo/internal/metrics/ping.go b/echo/internal/metrics/ping.go index 68d42ec3a4..be52e5b654 100644 --- a/echo/internal/metrics/ping.go +++ b/echo/internal/metrics/ping.go @@ -19,9 +19,16 @@ func (pg *PingGroup) newPinger(ruleLabel string, remote string, addr string) (*p pinger.Interval = pingInterval pinger.Timeout = time.Duration(math.MaxInt64) pinger.RecordRtts = false - if runtime.GOOS != "darwin" { + + switch runtime.GOOS { + case "darwin": + case "linux": pinger.SetPrivileged(true) + default: + pinger.SetPrivileged(true) + pg.logger.Warn("Attempting to set privileged mode for unknown OS", zap.String("OS", runtime.GOOS)) } + pinger.OnRecv = func(pkt *ping.Packet) { ip := pkt.IPAddr.String() PingResponseDurationMilliseconds.WithLabelValues( @@ -35,25 +42,23 @@ func (pg *PingGroup) newPinger(ruleLabel string, remote string, addr string) (*p type PingGroup struct { logger *zap.Logger - // k: addr - Pingers map[string]*ping.Pinger + Pingers []*ping.Pinger } func NewPingGroup(cfg *config.Config) *PingGroup { - pg := &PingGroup{ - logger: zap.L().Named("pinger"), - Pingers: make(map[string]*ping.Pinger), - } + pg := &PingGroup{logger: zap.L().Named("pinger"), Pingers: make([]*ping.Pinger, 0)} + for _, relayCfg := range cfg.RelayConfigs { for _, remote := range relayCfg.GetAllRemotes() { addr, err := remote.GetAddrHost() if err != nil { pg.logger.Error("try parse host error", zap.Error(err)) + continue } if pinger, err := pg.newPinger(relayCfg.Label, remote.Address, addr); err != nil { pg.logger.Error("new pinger meet error", zap.Error(err)) } else { - pg.Pingers[addr] = pinger + pg.Pingers = append(pg.Pingers, pinger) } } } @@ -66,10 +71,10 @@ func (pg *PingGroup) Run() { } pg.logger.Sugar().Infof("Start Ping Group now total pinger: %d", len(pg.Pingers)) splay := time.Duration(pingInterval.Nanoseconds() / int64(len(pg.Pingers))) - for addr, pinger := range pg.Pingers { + for _, pinger := range pg.Pingers { go func() { if err := pinger.Run(); err != nil { - pg.logger.Error("Starting pinger meet err", zap.String("addr", addr), zap.Error(err)) + pg.logger.Error("Starting pinger meet err", zap.Error(err), zap.String("addr", pinger.Addr())) } }() time.Sleep(splay) diff --git a/echo/internal/web/js/rule_metrics.js b/echo/internal/web/js/rule_metrics.js index fc44e29d48..a8d98b35db 100644 --- a/echo/internal/web/js/rule_metrics.js +++ b/echo/internal/web/js/rule_metrics.js @@ -71,13 +71,14 @@ class ChartManager { connectionCount: this.initChart('connectionCountChart', 'line', 'Connection Count', 'Count'), handshakeDuration: this.initChart('handshakeDurationChart', 'line', 'Handshake Duration', 'ms'), pingLatency: this.initChart('pingLatencyChart', 'line', 'Ping Latency', 'ms'), - networkTransmitBytes: this.initChart('networkTransmitBytesChart', 'line', 'Network Transmit', 'MB'), + networkTransmitBytes: this.initStackedAreaChart('networkTransmitBytesChart', 'Network Transmit', 'MB'), }; } initChart(canvasId, type, title, unit) { const ctx = $(`#${canvasId}`)[0].getContext('2d'); const color = Config.CHART_COLORS[canvasId.replace('Chart', '')]; + const metricType = canvasId.replace('Chart', ''); return new Chart(ctx, { type: type, @@ -90,15 +91,29 @@ class ChartManager { backgroundColor: color.replace('1)', '0.2)'), borderWidth: 2, data: [], + pointRadius: 0, // Hide individual points + tension: 0.1, // Add slight curve to lines }, ], }, - options: this.getChartOptions(title, unit), + options: this.getChartOptions(title, unit, metricType), }); } - getChartOptions(title, unit) { - return { + initStackedAreaChart(canvasId, title, unit) { + const ctx = $(`#${canvasId}`)[0].getContext('2d'); + return new Chart(ctx, { + type: 'line', + data: { + labels: [], + datasets: [], + }, + options: this.getStackedAreaChartOptions(title, unit), + }); + } + + getChartOptions(title, unit, metricType) { + const baseOptions = { responsive: true, plugins: { title: { @@ -124,6 +139,87 @@ class ChartManager { }, }, }; + + // We'll update suggestedMin and suggestedMax dynamically in updateCharts method + switch (metricType) { + case 'connectionCount': + baseOptions.scales.y.ticks = { stepSize: 1 }; + baseOptions.scales.y.title.text = 'Number of Connections'; + break; + case 'handshakeDuration': + baseOptions.scales.y.title.text = 'Duration (ms)'; + break; + case 'pingLatency': + baseOptions.scales.y.title.text = 'Latency (ms)'; + break; + case 'networkTransmitBytes': + baseOptions.scales.y = { + beginAtZero: true, + title: { display: true, text: 'Data Transmitted (MB)' }, + ticks: { + callback: (value) => value.toFixed(2) + ' MB', + }, + }; + baseOptions.plugins.tooltip = { + callbacks: { + label: (context) => `${context.dataset.label}: ${context.parsed.y.toFixed(2)} MB`, + }, + }; + break; + } + + return baseOptions; + } + + getStackedAreaChartOptions(title, unit) { + return { + responsive: true, + plugins: { + title: { + display: true, + text: title, + font: { size: 16, weight: 'bold' }, + }, + tooltip: { + mode: 'index', + intersect: false, + callbacks: { + label: (context) => `${context.dataset.label}: ${(context.parsed.y / Config.BYTE_TO_MB).toFixed(2)} MB`, + }, + }, + }, + scales: { + x: { + type: 'time', + time: { unit: 'minute', displayFormats: { minute: 'HH:mm' } }, + title: { display: true, text: 'Time' }, + }, + y: { + stacked: true, + beginAtZero: true, + title: { display: true, text: 'Data Transmitted (MB)' }, + ticks: { + callback: (value) => (value / Config.BYTE_TO_MB).toFixed(2) + ' MB', + }, + }, + }, + interaction: { + mode: 'nearest', + axis: 'x', + intersect: false, + }, + }; + } + + adjustColor(color, amount) { + return color.replace( + /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/, + (match, r, g, b, a) => + `rgba(${Math.min(255, Math.max(0, parseInt(r) + amount))}, ${Math.min(255, Math.max(0, parseInt(g) + amount))}, ${Math.min( + 255, + Math.max(0, parseInt(b) + amount) + )}, ${a || 1})` + ); } fillMissingDataPoints(data, startTime, endTime) { @@ -145,51 +241,115 @@ class ChartManager { } updateCharts(metrics, startTime, endTime) { - // 检查metrics是否为null或undefined if (!metrics) { - // 如果为null,则更新所有图表为空 Object.values(this.charts).forEach((chart) => { - chart.data.datasets = [ - { - label: 'No Data', - data: [], - }, - ]; + chart.data.datasets = []; chart.update(); }); return; } - // 首先按时间正序排列数据 + metrics.sort((a, b) => a.timestamp - b.timestamp); - // 按 label-remote 分组 const groupedMetrics = this.groupMetricsByLabelRemote(metrics); - console.log('groupedMetrics', groupedMetrics); - // 预处理所有指标的数据 - const processedData = {}; + // Calculate min and max values for each metric type + const ranges = this.calculateMetricRanges(groupedMetrics); - Object.keys(this.charts).forEach((key) => { - processedData[key] = groupedMetrics.map((group, index) => { - const data = group.metrics.map((m) => ({ - x: new Date(m.timestamp * 1000), - y: this.getMetricValue(key, m), - })); - const filledData = this.fillMissingDataPoints(data, startTime, endTime); - return { - label: `${group.label} - ${group.remote}`, - borderColor: this.getColor(index), - backgroundColor: this.getColor(index, 0.2), - borderWidth: 2, - data: filledData, - }; + Object.entries(this.charts).forEach(([key, chart]) => { + if (key === 'networkTransmitBytes') { + chart.data.datasets = []; + groupedMetrics.forEach((group, groupIndex) => { + const tcpData = []; + const udpData = []; + group.metrics.forEach((m) => { + const timestamp = new Date(m.timestamp * 1000); + tcpData.push({ x: timestamp, y: m.tcp_network_transmit_bytes }); + udpData.push({ x: timestamp, y: m.udp_network_transmit_bytes }); + }); + const baseColor = this.getColor(groupIndex); + chart.data.datasets.push( + { + label: `${group.label} - ${group.remote} (TCP)`, + borderColor: baseColor, + backgroundColor: baseColor.replace('1)', '0.5)'), + borderWidth: 1, + data: this.fillMissingDataPoints(tcpData, startTime, endTime), + fill: true, + }, + { + label: `${group.label} - ${group.remote} (UDP)`, + borderColor: this.adjustColor(baseColor, -40), + backgroundColor: this.adjustColor(baseColor, -40).replace('1)', '0.5)'), + borderWidth: 1, + data: this.fillMissingDataPoints(udpData, startTime, endTime), + fill: true, + } + ); + }); + } else { + chart.data.datasets = groupedMetrics.map((group, index) => { + const data = group.metrics.map((m) => ({ + x: new Date(m.timestamp * 1000), + y: this.getMetricValue(key, m), + })); + const filledData = this.fillMissingDataPoints(data, startTime, endTime); + return { + label: `${group.label} - ${group.remote}`, + borderColor: this.getColor(index), + backgroundColor: this.getColor(index, 0.2), + borderWidth: 2, + data: filledData, + }; + }); + } + + // Update chart options with calculated ranges + chart.options.scales.y.suggestedMin = ranges[key].min; + chart.options.scales.y.suggestedMax = ranges[key].max; + + chart.update(); + }); + } + + calculateMetricRanges(groupedMetrics) { + const ranges = { + connectionCount: { min: Infinity, max: -Infinity }, + handshakeDuration: { min: Infinity, max: -Infinity }, + pingLatency: { min: Infinity, max: -Infinity }, + networkTransmitBytes: { min: Infinity, max: -Infinity }, + }; + + groupedMetrics.forEach((group) => { + group.metrics.forEach((metric) => { + // Connection Count + const connectionCount = metric.tcp_connection_count + metric.udp_connection_count; + ranges.connectionCount.min = Math.min(ranges.connectionCount.min, connectionCount); + ranges.connectionCount.max = Math.max(ranges.connectionCount.max, connectionCount); + + // Handshake Duration + const handshakeDuration = Math.max(metric.tcp_handshake_duration, metric.udp_handshake_duration); + ranges.handshakeDuration.min = Math.min(ranges.handshakeDuration.min, handshakeDuration); + ranges.handshakeDuration.max = Math.max(ranges.handshakeDuration.max, handshakeDuration); + + // Ping Latency + ranges.pingLatency.min = Math.min(ranges.pingLatency.min, metric.ping_latency); + ranges.pingLatency.max = Math.max(ranges.pingLatency.max, metric.ping_latency); + + // Network Transmit Bytes + const networkTransmitBytes = (metric.tcp_network_transmit_bytes + metric.udp_network_transmit_bytes) / Config.BYTE_TO_MB; + ranges.networkTransmitBytes.min = Math.min(ranges.networkTransmitBytes.min, networkTransmitBytes); + ranges.networkTransmitBytes.max = Math.max(ranges.networkTransmitBytes.max, networkTransmitBytes); }); }); - // 更新每个图表 - Object.entries(this.charts).forEach(([key, chart]) => { - chart.data.datasets = processedData[key]; - chart.update(); + // Add some padding to the ranges + Object.keys(ranges).forEach((key) => { + const range = ranges[key].max - ranges[key].min; + ranges[key].min = Math.max(0, ranges[key].min - range * 0.1); + ranges[key].max += range * 0.1; }); + + return ranges; } groupMetricsByLabelRemote(metrics) { diff --git a/echo/internal/web/templates/_rule_metrics_dash.html b/echo/internal/web/templates/_rule_metrics_dash.html index a0f71b8fb9..b0e9d9701a 100644 --- a/echo/internal/web/templates/_rule_metrics_dash.html +++ b/echo/internal/web/templates/_rule_metrics_dash.html @@ -68,10 +68,10 @@
-
+
-
+
diff --git a/echo/pkg/metric_reader/node.go b/echo/pkg/metric_reader/node.go index c97a6de5c4..4730bbd548 100644 --- a/echo/pkg/metric_reader/node.go +++ b/echo/pkg/metric_reader/node.go @@ -152,7 +152,7 @@ func sumFloat64Metric(metricMap map[string]*dto.MetricFamily, metricName string) ret += getMetricValue(m, metric.GetType()) } } - return 0 + return ret } func getLabel(metric *dto.Metric, name string) string { diff --git a/echo/pkg/sub/clash.go b/echo/pkg/sub/clash.go deleted file mode 100644 index 3ea35dfa50..0000000000 --- a/echo/pkg/sub/clash.go +++ /dev/null @@ -1,190 +0,0 @@ -package sub - -import ( - "fmt" - "net" - "sort" - "strconv" - "strings" - - relay_cfg "github.com/Ehco1996/ehco/internal/relay/conf" - "gopkg.in/yaml.v3" -) - -type ClashSub struct { - Name string - URL string - - cCfg *clashConfig -} - -func NewClashSub(rawClashCfgBuf []byte, name string, url string) (*ClashSub, error) { - var rawCfg clashConfig - err := yaml.Unmarshal(rawClashCfgBuf, &rawCfg) - if err != nil { - return nil, err - } - rawCfg.Adjust() - return &ClashSub{cCfg: &rawCfg, Name: name, URL: url}, nil -} - -func NewClashSubByURL(url string, name string) (*ClashSub, error) { - body, err := getHttpBody(url) - if err != nil { - return nil, err - } - return NewClashSub(body, name, url) -} - -func (c *ClashSub) ToClashConfigYaml() ([]byte, error) { - return yaml.Marshal(c.cCfg) -} - -func (c *ClashSub) ToGroupedClashConfigYaml() ([]byte, error) { - groupProxy := c.cCfg.groupByLongestCommonPrefix() - ps := []*Proxies{} - groupNameList := []string{} - for groupName := range groupProxy { - groupNameList = append(groupNameList, groupName) - } - sort.Strings(groupNameList) - for _, groupName := range groupNameList { - proxies := groupProxy[groupName] - // only use first proxy will be show in proxy provider, other will be merged into load balance in relay - p := proxies[0].getOrCreateGroupLeader() - ps = append(ps, p) - } - groupedCfg := &clashConfig{&ps} - return yaml.Marshal(groupedCfg) -} - -func (c *ClashSub) Refresh() error { - // get new clash sub by url - newSub, err := NewClashSubByURL(c.URL, c.Name) - if err != nil { - return err - } - - needAdd := []*Proxies{} - needDeleteProxyName := map[string]struct{}{} - - // check if need add/delete proxies - for _, newProxy := range *newSub.cCfg.Proxies { - oldProxy := c.cCfg.GetProxyByRawName(newProxy.rawName) - if oldProxy == nil { - needAdd = append(needAdd, newProxy) - } else if oldProxy.Different(newProxy) { - // update so we need to delete and add again - needDeleteProxyName[oldProxy.rawName] = struct{}{} - needAdd = append(needAdd, newProxy) - } - } - // check if need delete proxies - for _, proxy := range *c.cCfg.Proxies { - newProxy := newSub.cCfg.GetProxyByRawName(proxy.rawName) - if newProxy == nil { - needDeleteProxyName[proxy.rawName] = struct{}{} - } - } - - tmp := []*Proxies{} - // delete proxies from changedCfg - for _, p := range *c.cCfg.Proxies { - if _, ok := needDeleteProxyName[p.rawName]; !ok { - tmp = append(tmp, p) - } - } - // add new proxies to changedCfg - tmp = append(tmp, needAdd...) - - // update current - c.cCfg.Proxies = &tmp - - // init group leader for each group - groupProxy := c.cCfg.groupByLongestCommonPrefix() - for _, proxies := range groupProxy { - // only use first proxy will be show in proxy provider, other will be merged into load balance in relay - proxies[0].getOrCreateGroupLeader() - } - return nil -} - -// ToRelayConfigs convert clash sub to relay configs -// Proxy's port will be used as relay listen port -// Group's proxies will be merged into load balance in relay -// a new free port will be used as relay listen port for each group -func (c *ClashSub) ToRelayConfigs(listenHost string) ([]*relay_cfg.Config, error) { - relayConfigs := []*relay_cfg.Config{} - portSet := map[int]struct{}{} - - const minPort = 10000 - const maxPort = 65535 - nextPort := minPort - getNextAvailablePort := func() int { - for ; nextPort <= maxPort; nextPort++ { - if _, occupied := portSet[nextPort]; !occupied { - portSet[nextPort] = struct{}{} - defer func() { nextPort++ }() - return nextPort - } - } - return -1 - } - - // generate relay config for each proxy - for _, proxy := range *c.cCfg.Proxies { - proxyPort, err := strconv.Atoi(proxy.Port) - if err != nil { - return nil, err - } - if _, ok := portSet[proxyPort]; ok { - proxyPort = getNextAvailablePort() - } - var newName string - if strings.HasSuffix(proxy.Name, "-") { - newName = fmt.Sprintf("%s%s", proxy.Name, c.Name) - } else { - newName = fmt.Sprintf("%s-%s", proxy.Name, c.Name) - } - rc, err := proxy.ToRelayConfig(listenHost, strconv.Itoa(proxyPort), newName) - if err != nil { - return nil, err - } - relayConfigs = append(relayConfigs, rc) - } - - // generate relay config for each group - groupProxy := c.cCfg.groupByLongestCommonPrefix() - for groupName, proxies := range groupProxy { - groupLeader := proxies[0].getOrCreateGroupLeader() - var newName string - if strings.HasSuffix(groupName, "-") { - newName = fmt.Sprintf("%slb", groupName) - } else { - newName = fmt.Sprintf("%s-lb", groupName) - } - port := getNextAvailablePort() - if port == -1 { - return nil, fmt.Errorf("no available port") - } - rc, err := groupLeader.ToRelayConfig(listenHost, strconv.Itoa(port), newName) - if err != nil { - return nil, err - } - // add other proxies address in group to relay config - for _, proxy := range proxies[1:] { - remote := net.JoinHostPort(proxy.rawServer, proxy.rawPort) - if strInArray(remote, rc.Remotes) { - continue - } - rc.Remotes = append(rc.Remotes, remote) - if proxy.UDP { - rc.Options = &relay_cfg.Options{ - EnableUDP: true, - } - } - } - relayConfigs = append(relayConfigs, rc) - } - return relayConfigs, nil -} diff --git a/echo/pkg/sub/clash_test.go b/echo/pkg/sub/clash_test.go deleted file mode 100644 index a04d8634e9..0000000000 --- a/echo/pkg/sub/clash_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package sub - -import ( - "testing" - - "github.com/Ehco1996/ehco/pkg/log" - "github.com/stretchr/testify/assert" - "go.uber.org/zap" -) - -var ( - l *zap.Logger - - configBuf = []byte(` -proxies: - - name: ss - type: ss - server: proxy1.example.com - port: 1080 - password: password - cipher: aes-128-gcm - udp: true - - name: trojan - type: trojan - server: proxy2.example.com - port: 443 - password: password - skip-cert-verify: true -`) -) - -func init() { - log.InitGlobalLogger("debug") - l = zap.L().Named("clash_test") -} - -func TestNewClashConfig(t *testing.T) { - // todo add more proxy types - - cs, err := NewClashSub(configBuf, "test", "") - assert.NoError(t, err, "NewConfig should not return an error") - assert.NotNil(t, cs, "Config should not be nil") - expectedProxyCount := 2 - assert.Equal(t, expectedProxyCount, len(*cs.cCfg.Proxies), "Proxy count should match") - - yamlBuf, err := cs.ToClashConfigYaml() - assert.NoError(t, err, "ToClashConfigYaml should not return an error") - assert.NotNil(t, yamlBuf, "yamlBuf should not be nil") - l.Info("yamlBuf", zap.String("yamlBuf", string(yamlBuf))) -} - -func TestToRelayConfigs(t *testing.T) { - cs, err := NewClashSub(configBuf, "test", "") - assert.NoError(t, err, "NewConfig should not return an error") - assert.NotNil(t, cs, "Config should not be nil") - - relayConfigs, err := cs.ToRelayConfigs("localhost") - assert.NoError(t, err, "ToRelayConfigs should not return an error") - assert.NotNil(t, relayConfigs, "relayConfigs should not be nil") - expectedRelayCount := 4 // 2 proxy + 2 load balance - assert.Equal(t, expectedRelayCount, len(relayConfigs), "Relay count should match") - l.Info("relayConfigs", zap.Any("relayConfigs", relayConfigs)) -} diff --git a/echo/pkg/sub/clash_types.go b/echo/pkg/sub/clash_types.go deleted file mode 100644 index 1761663758..0000000000 --- a/echo/pkg/sub/clash_types.go +++ /dev/null @@ -1,197 +0,0 @@ -package sub - -import ( - "net" - - "github.com/Ehco1996/ehco/internal/constant" - relay_cfg "github.com/Ehco1996/ehco/internal/relay/conf" -) - -type clashConfig struct { - Proxies *[]*Proxies `yaml:"proxies"` -} - -func (cc *clashConfig) GetProxyByRawName(name string) *Proxies { - for _, proxy := range *cc.Proxies { - if proxy.rawName == name { - return proxy - } - } - return nil -} - -func (cc *clashConfig) GetProxyByName(name string) *Proxies { - for _, proxy := range *cc.Proxies { - if proxy.Name == name { - return proxy - } - } - return nil -} - -func (cc *clashConfig) Adjust() { - for _, proxy := range *cc.Proxies { - if proxy.rawName == "" { - proxy.rawName = proxy.Name - proxy.rawPort = proxy.Port - proxy.rawServer = proxy.Server - } - } -} - -func (cc *clashConfig) groupByLongestCommonPrefix() map[string][]*Proxies { - proxies := cc.Proxies - - proxyNameList := []string{} - for _, proxy := range *proxies { - proxyNameList = append(proxyNameList, proxy.Name) - } - groupNameMap := groupByLongestCommonPrefix(proxyNameList) - - proxyGroups := make(map[string][]*Proxies) - for groupName, proxyNames := range groupNameMap { - for _, proxyName := range proxyNames { - proxyGroups[groupName] = append(proxyGroups[groupName], cc.GetProxyByName(proxyName)) - } - } - return proxyGroups -} - -type Proxies struct { - // basic fields - Name string `yaml:"name"` - Type string `yaml:"type"` - Server string `yaml:"server"` - Port string `yaml:"port"` - Password string `yaml:"password,omitempty"` - UDP bool `yaml:"udp,omitempty"` - - // for shadowsocks todo(support opts) - Cipher string `yaml:"cipher,omitempty"` - - // for trojan todo(support opts) - ALPN []string `yaml:"alpn,omitempty"` - SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"` - SNI string `yaml:"sni,omitempty"` - Network string `yaml:"network,omitempty"` - - // for socks5 todo(support opts) - UserName string `yaml:"username,omitempty"` - TLS bool `yaml:"tls,omitempty"` - - // for vmess todo(support opts) - UUID string `yaml:"uuid,omitempty"` - AlterID int `yaml:"alterId,omitempty"` - ServerName string `yaml:"servername,omitempty"` - - rawName string - rawServer string - rawPort string - relayCfg *relay_cfg.Config - - groupLeader *Proxies -} - -func (p *Proxies) Different(new *Proxies) bool { - if p.Type != new.Type || - p.Password != new.Password || - p.UDP != new.UDP || - p.Cipher != new.Cipher || - len(p.ALPN) != len(new.ALPN) || - p.SkipCertVerify != new.SkipCertVerify || - p.SNI != new.SNI || - p.Network != new.Network || - p.UserName != new.UserName || - p.TLS != new.TLS || - p.UUID != new.UUID || - p.AlterID != new.AlterID || - p.ServerName != new.ServerName { - return true - } - // ALPN field is a slice, should assert values successively. - for i, v := range p.ALPN { - if v != new.ALPN[i] { - return true - } - } - - // Server Port Name will be changed when ToRelayConfig is called. so we just need to compare the other fields. - if p.rawName != new.rawName || - p.rawServer != new.rawServer || - p.rawPort != new.rawPort { - return true - } - - // All fields are equivalent, so proxies are not different. - return false -} - -func (p *Proxies) ToRelayConfig(listenHost string, listenPort string, newName string) (*relay_cfg.Config, error) { - if p.relayCfg != nil { - return p.relayCfg, nil - } - remoteAddr := net.JoinHostPort(p.Server, p.Port) - r := &relay_cfg.Config{ - Label: newName, - ListenType: constant.RelayTypeRaw, - TransportType: constant.RelayTypeRaw, - Listen: net.JoinHostPort(listenHost, listenPort), - Remotes: []string{remoteAddr}, - } - if p.UDP { - r.Options = &relay_cfg.Options{ - EnableUDP: true, - } - } - if err := r.Validate(); err != nil { - return nil, err - } - // overwrite name,port,and server by relay - p.Name = newName - p.Server = listenHost - p.Port = listenPort - p.relayCfg = r - return r, nil -} - -func (p *Proxies) Clone() *Proxies { - cloned := &Proxies{ - Name: p.Name, - Type: p.Type, - Server: p.Server, - Port: p.Port, - Password: p.Password, - UDP: p.UDP, - Cipher: p.Cipher, - ALPN: p.ALPN, - SkipCertVerify: p.SkipCertVerify, - SNI: p.SNI, - Network: p.Network, - UserName: p.UserName, - TLS: p.TLS, - UUID: p.UUID, - AlterID: p.AlterID, - ServerName: p.ServerName, - - rawName: p.rawName, - rawServer: p.rawServer, - rawPort: p.rawPort, - } - if p.relayCfg != nil { - cloned.relayCfg = p.relayCfg.Clone() - } - return cloned -} - -func (p *Proxies) getOrCreateGroupLeader() *Proxies { - if p.groupLeader != nil { - return p.groupLeader - } - p.groupLeader = p.Clone() - // reset name,port,and server to raw - p.groupLeader.Name = p.rawName - p.groupLeader.Port = p.rawPort - p.groupLeader.Server = p.rawServer - p.groupLeader.relayCfg = nil - return p.groupLeader -} diff --git a/echo/pkg/sub/utils.go b/echo/pkg/sub/utils.go deleted file mode 100644 index d3c72e080c..0000000000 --- a/echo/pkg/sub/utils.go +++ /dev/null @@ -1,101 +0,0 @@ -package sub - -import ( - "fmt" - "io" - "net/http" - "sort" - "strings" - "time" -) - -var client = http.Client{Timeout: time.Second * 10} - -func getHttpBody(url string) ([]byte, error) { - resp, err := client.Get(url) - if err != nil { - msg := fmt.Sprintf("http get sub config url=%s meet err=%v", url, err) - return nil, fmt.Errorf(msg) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - msg := fmt.Sprintf("http get sub config url=%s meet status code=%d", url, resp.StatusCode) - return nil, fmt.Errorf(msg) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - msg := fmt.Sprintf("read body meet err=%v", err) - return nil, fmt.Errorf(msg) - } - return body, nil -} - -func longestCommonPrefix(s string, t string) string { - runeS := []rune(s) - runeT := []rune(t) - - i := 0 - for i = 0; i < len(runeS) && i < len(runeT); i++ { - if runeS[i] != runeT[i] { - return string(runeS[:i]) - } - } - return string(runeS[:i]) -} - -func groupByLongestCommonPrefix(strList []string) map[string][]string { - sort.Strings(strList) - - grouped := make(map[string][]string) - - // 先找出有相同前缀的字符串 - for i := 0; i < len(strList); i++ { - for j := i + 1; j < len(strList); j++ { - prefix := longestCommonPrefix(strList[i], strList[j]) - if prefix == "" { - continue - } - if _, ok := grouped[prefix]; !ok { - grouped[prefix] = []string{} - } - } - } - - // 过滤掉有相同前缀的前缀中较短的 - for prefix := range grouped { - for otherPrefix := range grouped { - if prefix == otherPrefix { - continue - } - if strings.HasPrefix(otherPrefix, prefix) { - delete(grouped, prefix) - } - } - } - - // 将字符串分组 - for _, proxy := range strList { - foundPrefix := false - for prefix := range grouped { - if strings.HasPrefix(proxy, prefix) { - grouped[prefix] = append(grouped[prefix], proxy) - foundPrefix = true - break - } - } - // 处理没有相同前缀的字符串,自己是一个分组 - if !foundPrefix { - grouped[proxy] = []string{proxy} - } - } - return grouped -} - -func strInArray(ele string, array []string) bool { - for _, v := range array { - if v == ele { - return true - } - } - return false -} diff --git a/mieru/Makefile b/mieru/Makefile index 59dbdababe..ef57730a54 100644 --- a/mieru/Makefile +++ b/mieru/Makefile @@ -32,7 +32,7 @@ PROJECT_NAME=$(shell basename "${ROOT}") # - pkg/version/current.go # # Use `tools/bump_version.sh` script to change all those files at one shot. -VERSION="3.3.2" +VERSION="3.3.3" # Build binaries and installation packages. .PHONY: build diff --git a/mieru/build/package/mieru/amd64/debian/DEBIAN/control b/mieru/build/package/mieru/amd64/debian/DEBIAN/control index 7e815a0fe7..fe02d47563 100755 --- a/mieru/build/package/mieru/amd64/debian/DEBIAN/control +++ b/mieru/build/package/mieru/amd64/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: mieru -Version: 3.3.2 +Version: 3.3.3 Section: net Priority: optional Architecture: amd64 diff --git a/mieru/build/package/mieru/amd64/rpm/mieru.spec b/mieru/build/package/mieru/amd64/rpm/mieru.spec index 84c6777574..cf2f186db4 100644 --- a/mieru/build/package/mieru/amd64/rpm/mieru.spec +++ b/mieru/build/package/mieru/amd64/rpm/mieru.spec @@ -1,5 +1,5 @@ Name: mieru -Version: 3.3.2 +Version: 3.3.3 Release: 1%{?dist} Summary: Mieru proxy client License: GPLv3+ diff --git a/mieru/build/package/mieru/arm64/debian/DEBIAN/control b/mieru/build/package/mieru/arm64/debian/DEBIAN/control index e97828c884..0fd99582ac 100755 --- a/mieru/build/package/mieru/arm64/debian/DEBIAN/control +++ b/mieru/build/package/mieru/arm64/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: mieru -Version: 3.3.2 +Version: 3.3.3 Section: net Priority: optional Architecture: arm64 diff --git a/mieru/build/package/mieru/arm64/rpm/mieru.spec b/mieru/build/package/mieru/arm64/rpm/mieru.spec index 84c6777574..cf2f186db4 100644 --- a/mieru/build/package/mieru/arm64/rpm/mieru.spec +++ b/mieru/build/package/mieru/arm64/rpm/mieru.spec @@ -1,5 +1,5 @@ Name: mieru -Version: 3.3.2 +Version: 3.3.3 Release: 1%{?dist} Summary: Mieru proxy client License: GPLv3+ diff --git a/mieru/build/package/mita/amd64/debian/DEBIAN/control b/mieru/build/package/mita/amd64/debian/DEBIAN/control index 39cdb73093..1048cce6cb 100755 --- a/mieru/build/package/mita/amd64/debian/DEBIAN/control +++ b/mieru/build/package/mita/amd64/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: mita -Version: 3.3.2 +Version: 3.3.3 Section: net Priority: optional Architecture: amd64 diff --git a/mieru/build/package/mita/amd64/rpm/mita.spec b/mieru/build/package/mita/amd64/rpm/mita.spec index 3560ec47ea..087aab4d1b 100644 --- a/mieru/build/package/mita/amd64/rpm/mita.spec +++ b/mieru/build/package/mita/amd64/rpm/mita.spec @@ -1,5 +1,5 @@ Name: mita -Version: 3.3.2 +Version: 3.3.3 Release: 1%{?dist} Summary: Mieru proxy server License: GPLv3+ diff --git a/mieru/build/package/mita/arm64/debian/DEBIAN/control b/mieru/build/package/mita/arm64/debian/DEBIAN/control index 1481267c6e..7d932130ca 100755 --- a/mieru/build/package/mita/arm64/debian/DEBIAN/control +++ b/mieru/build/package/mita/arm64/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: mita -Version: 3.3.2 +Version: 3.3.3 Section: net Priority: optional Architecture: arm64 diff --git a/mieru/build/package/mita/arm64/rpm/mita.spec b/mieru/build/package/mita/arm64/rpm/mita.spec index ce374e9ca8..977e3cdba6 100644 --- a/mieru/build/package/mita/arm64/rpm/mita.spec +++ b/mieru/build/package/mita/arm64/rpm/mita.spec @@ -1,5 +1,5 @@ Name: mita -Version: 3.3.2 +Version: 3.3.3 Release: 1%{?dist} Summary: Mieru proxy server License: GPLv3+ diff --git a/mieru/docs/server-install.md b/mieru/docs/server-install.md index 7190b167c2..00bee20589 100644 --- a/mieru/docs/server-install.md +++ b/mieru/docs/server-install.md @@ -8,32 +8,32 @@ Before installation and configuration, connect to the server via SSH and then ex ```sh # Debian / Ubuntu - X86_64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.2/mita_3.3.2_amd64.deb +curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.3/mita_3.3.3_amd64.deb # Debian / Ubuntu - ARM 64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.2/mita_3.3.2_arm64.deb +curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.3/mita_3.3.3_arm64.deb # RedHat / CentOS / Rocky Linux - X86_64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.2/mita-3.3.2-1.x86_64.rpm +curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.3/mita-3.3.3-1.x86_64.rpm # RedHat / CentOS / Rocky Linux - ARM 64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.2/mita-3.3.2-1.aarch64.rpm +curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.3/mita-3.3.3-1.aarch64.rpm ``` ## Install mita package ```sh # Debian / Ubuntu - X86_64 -sudo dpkg -i mita_3.3.2_amd64.deb +sudo dpkg -i mita_3.3.3_amd64.deb # Debian / Ubuntu - ARM 64 -sudo dpkg -i mita_3.3.2_arm64.deb +sudo dpkg -i mita_3.3.3_arm64.deb # RedHat / CentOS / Rocky Linux - X86_64 -sudo rpm -Uvh --force mita-3.3.2-1.x86_64.rpm +sudo rpm -Uvh --force mita-3.3.3-1.x86_64.rpm # RedHat / CentOS / Rocky Linux - ARM 64 -sudo rpm -Uvh --force mita-3.3.2-1.aarch64.rpm +sudo rpm -Uvh --force mita-3.3.3-1.aarch64.rpm ``` Those instructions can also be used to upgrade the version of mita software package. diff --git a/mieru/docs/server-install.zh_CN.md b/mieru/docs/server-install.zh_CN.md index 740fa3d547..bf94e1c43f 100644 --- a/mieru/docs/server-install.zh_CN.md +++ b/mieru/docs/server-install.zh_CN.md @@ -8,32 +8,32 @@ ```sh # Debian / Ubuntu - X86_64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.2/mita_3.3.2_amd64.deb +curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.3/mita_3.3.3_amd64.deb # Debian / Ubuntu - ARM 64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.2/mita_3.3.2_arm64.deb +curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.3/mita_3.3.3_arm64.deb # RedHat / CentOS / Rocky Linux - X86_64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.2/mita-3.3.2-1.x86_64.rpm +curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.3/mita-3.3.3-1.x86_64.rpm # RedHat / CentOS / Rocky Linux - ARM 64 -curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.2/mita-3.3.2-1.aarch64.rpm +curl -LSO https://github.com/enfein/mieru/releases/download/v3.3.3/mita-3.3.3-1.aarch64.rpm ``` ## 安装 mita 软件包 ```sh # Debian / Ubuntu - X86_64 -sudo dpkg -i mita_3.3.2_amd64.deb +sudo dpkg -i mita_3.3.3_amd64.deb # Debian / Ubuntu - ARM 64 -sudo dpkg -i mita_3.3.2_arm64.deb +sudo dpkg -i mita_3.3.3_arm64.deb # RedHat / CentOS / Rocky Linux - X86_64 -sudo rpm -Uvh --force mita-3.3.2-1.x86_64.rpm +sudo rpm -Uvh --force mita-3.3.3-1.x86_64.rpm # RedHat / CentOS / Rocky Linux - ARM 64 -sudo rpm -Uvh --force mita-3.3.2-1.aarch64.rpm +sudo rpm -Uvh --force mita-3.3.3-1.aarch64.rpm ``` 上述指令也可以用来升级 mita 软件包的版本。 diff --git a/mieru/pkg/appctl/server.go b/mieru/pkg/appctl/server.go index 1a0cb70701..159ca41653 100644 --- a/mieru/pkg/appctl/server.go +++ b/mieru/pkg/appctl/server.go @@ -126,10 +126,12 @@ func (s *serverLifecycleService) Start(ctx context.Context, req *pb.Empty) (*pb. // Create the egress socks5 server. socks5Config := &socks5.Config{ - AllowLocalDestination: config.GetAdvancedSettings().GetAllowLocalDestination(), - ClientSideAuthentication: true, - EgressController: egress.NewSocks5Controller(config.GetEgress()), - HandshakeTimeout: 10 * time.Second, + AllowLocalDestination: config.GetAdvancedSettings().GetAllowLocalDestination(), + AuthOpts: socks5.Auth{ + ClientSideAuthentication: true, + }, + EgressController: egress.NewSocks5Controller(config.GetEgress()), + HandshakeTimeout: 10 * time.Second, } socks5Server, err := socks5.New(socks5Config) if err != nil { diff --git a/mieru/pkg/appctl/server_test.go b/mieru/pkg/appctl/server_test.go index 6671226a2a..2bb5761e17 100644 --- a/mieru/pkg/appctl/server_test.go +++ b/mieru/pkg/appctl/server_test.go @@ -74,6 +74,7 @@ func TestServerApplyReject(t *testing.T) { "testdata/server_reject_mtu_too_big.json", "testdata/server_reject_mtu_too_small.json", "testdata/server_reject_no_password.json", + "testdata/server_reject_no_port_bindings.json", "testdata/server_reject_no_port.json", "testdata/server_reject_no_protocol.json", "testdata/server_reject_no_user_name.json", diff --git a/mieru/pkg/appctl/testdata/server_reject_no_port_bindings.json b/mieru/pkg/appctl/testdata/server_reject_no_port_bindings.json new file mode 100644 index 0000000000..1e1982e00b --- /dev/null +++ b/mieru/pkg/appctl/testdata/server_reject_no_port_bindings.json @@ -0,0 +1,8 @@ +{ + "users": [ + { + "name": "user1", + "password": "fa7206ed2a94" + } + ] +} diff --git a/mieru/pkg/cli/client.go b/mieru/pkg/cli/client.go index cadf6c803e..48d791dd82 100644 --- a/mieru/pkg/cli/client.go +++ b/mieru/pkg/cli/client.go @@ -506,15 +506,17 @@ var clientRunFunc = func(s []string) error { return fmt.Errorf(stderror.InvalidTransportProtocol) } } - mux.SetEndpoints(endpoints) } + mux.SetEndpoints(endpoints) // Create the local socks5 server. socks5Config := &socks5.Config{ - UseProxy: true, - ClientSideAuthentication: true, - ProxyMux: mux, - HandshakeTimeout: 10 * time.Second, + UseProxy: true, + AuthOpts: socks5.Auth{ + ClientSideAuthentication: true, + }, + ProxyMux: mux, + HandshakeTimeout: 10 * time.Second, } socks5Server, err := socks5.New(socks5Config) if err != nil { diff --git a/mieru/pkg/cli/server.go b/mieru/pkg/cli/server.go index 9fe2d5c48b..8564a1cdbb 100644 --- a/mieru/pkg/cli/server.go +++ b/mieru/pkg/cli/server.go @@ -418,10 +418,12 @@ var serverRunFunc = func(s []string) error { // Create the egress socks5 server. socks5Config := &socks5.Config{ - AllowLocalDestination: config.GetAdvancedSettings().GetAllowLocalDestination(), - ClientSideAuthentication: true, - EgressController: egress.NewSocks5Controller(config.GetEgress()), - HandshakeTimeout: 10 * time.Second, + AllowLocalDestination: config.GetAdvancedSettings().GetAllowLocalDestination(), + AuthOpts: socks5.Auth{ + ClientSideAuthentication: true, + }, + EgressController: egress.NewSocks5Controller(config.GetEgress()), + HandshakeTimeout: 10 * time.Second, } socks5Server, err := socks5.New(socks5Config) if err != nil { diff --git a/mieru/pkg/protocolv2/mux.go b/mieru/pkg/protocolv2/mux.go index 4f47972d0b..6fe70003c1 100644 --- a/mieru/pkg/protocolv2/mux.go +++ b/mieru/pkg/protocolv2/mux.go @@ -172,6 +172,7 @@ func (m *Mux) SetEndpoints(endpoints []UnderlayProperties) *Mux { m.endpoints = new } } + log.Infof("Mux now has %d endpoints", len(m.endpoints)) return m } diff --git a/mieru/pkg/socks5/auth.go b/mieru/pkg/socks5/auth.go new file mode 100644 index 0000000000..df2615d8c4 --- /dev/null +++ b/mieru/pkg/socks5/auth.go @@ -0,0 +1,47 @@ +// Copyright (C) 2024 mieru authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package socks5 + +const ( + noAuth byte = 0 + userPassAuth byte = 2 + userPassAuthVersion byte = 1 + authSuccess byte = 0 + authFailure byte = 1 +) + +// Auth provide authentication settings to socks5 server. +type Auth struct { + // Do socks5 authentication at proxy client side. + ClientSideAuthentication bool + + // Credentials to authenticate incoming requests. + // If empty, username password authentication is not supported. + IngressCredentials map[string]string + + // Credential to dial an outgoing socks5 connection. + // If nil, username password authentication is not used. + EgressCredential *DialCredential +} + +// DialCredential stores socks5 credential for user password authentication. +type DialCredential struct { + // User to dial an outgoing socks5 connection. + User string + + // Password to dial an outgoing socks5 connection. + Password string +} diff --git a/mieru/pkg/socks5/socks5.go b/mieru/pkg/socks5/socks5.go index 3fc272cb3f..2076e00167 100644 --- a/mieru/pkg/socks5/socks5.go +++ b/mieru/pkg/socks5/socks5.go @@ -21,9 +21,6 @@ import ( const ( // socks5 version number. socks5Version byte = 5 - - // No authentication required. - noAuth byte = 0 ) var ( @@ -55,6 +52,9 @@ type Config struct { // BindIP is used for bind or udp associate BindIP net.IP + // Authentication options. + AuthOpts Auth + // Handshake timeout to establish socks5 connection. // Use 0 or negative value to disable the timeout. HandshakeTimeout time.Duration @@ -64,9 +64,6 @@ type Config struct { // Allow using socks5 to access resources served in localhost. AllowLocalDestination bool - - // Do socks5 authentication at proxy client side. - ClientSideAuthentication bool } // Server is responsible for accepting connections and handling @@ -184,7 +181,7 @@ func (s *Server) acceptLoop() { } func (s *Server) clientServeConn(conn net.Conn) error { - if s.config.ClientSideAuthentication { + if s.config.AuthOpts.ClientSideAuthentication { if err := s.handleAuthentication(conn); err != nil { return err } @@ -199,7 +196,7 @@ func (s *Server) clientServeConn(conn net.Conn) error { return fmt.Errorf("mux DialContext() failed: %w", err) } - if !s.config.ClientSideAuthentication { + if !s.config.AuthOpts.ClientSideAuthentication { if err := s.proxySocks5AuthReq(conn, proxyConn); err != nil { HandshakeErrors.Add(1) proxyConn.Close() @@ -226,7 +223,7 @@ func (s *Server) clientServeConn(conn net.Conn) error { } func (s *Server) serverServeConn(conn net.Conn) error { - if !s.config.ClientSideAuthentication { + if !s.config.AuthOpts.ClientSideAuthentication { if err := s.handleAuthentication(conn); err != nil { return err } diff --git a/mieru/pkg/testtool/pipe.go b/mieru/pkg/testtool/pipe.go index 8ff9e31e56..c0086da239 100644 --- a/mieru/pkg/testtool/pipe.go +++ b/mieru/pkg/testtool/pipe.go @@ -19,11 +19,16 @@ import ( "bytes" "errors" "io" + "net" + "runtime" "sync" + "time" + + "github.com/enfein/mieru/pkg/util" ) // BufPipe is like net.Pipe() but with an internal buffer. -func BufPipe() (io.ReadWriteCloser, io.ReadWriteCloser) { +func BufPipe() (net.Conn, net.Conn) { var buf1, buf2 bytes.Buffer var lock1, lock2 sync.Mutex ep1 := &ioEndpoint{ @@ -59,6 +64,8 @@ type ioEndpoint struct { closed bool } +var _ net.Conn = &ioEndpoint{} + func (e *ioEndpoint) Read(b []byte) (n int, err error) { if e.closed { return 0, io.EOF @@ -73,7 +80,10 @@ func (e *ioEndpoint) Read(b []byte) (n int, err error) { e.lock1.Unlock() } if errors.Is(err, io.EOF) { + // io.ReadFull() with partial result will not fail. err = nil + // Allow the writer to catch up. + runtime.Gosched() } return } @@ -98,3 +108,23 @@ func (e *ioEndpoint) Close() error { e.closed = true return nil } + +func (e *ioEndpoint) LocalAddr() net.Addr { + return util.NilNetAddr() +} + +func (e *ioEndpoint) RemoteAddr() net.Addr { + return util.NilNetAddr() +} + +func (e *ioEndpoint) SetDeadline(t time.Time) error { + return nil +} + +func (e *ioEndpoint) SetReadDeadline(t time.Time) error { + return nil +} + +func (e *ioEndpoint) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/mieru/pkg/version/current.go b/mieru/pkg/version/current.go index 770e690d87..894d596056 100644 --- a/mieru/pkg/version/current.go +++ b/mieru/pkg/version/current.go @@ -16,5 +16,5 @@ package version const ( - AppVersion = "3.3.2" + AppVersion = "3.3.3" ) diff --git a/mihomo/rules/provider/mrs_converter.go b/mihomo/rules/provider/mrs_converter.go index edc24e7eea..dbbe51cb29 100644 --- a/mihomo/rules/provider/mrs_converter.go +++ b/mihomo/rules/provider/mrs_converter.go @@ -34,7 +34,7 @@ func ConvertToMrs(buf []byte, behavior P.RuleBehavior, format P.RuleFormat, w io } var encoder *zstd.Encoder - encoder, err = zstd.NewWriter(w) + encoder, err = zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.SpeedBestCompression)) if err != nil { return err } diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index 4f8f1b9051..cd9d8d4a86 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -313,9 +313,11 @@ o.default = "223.5.5.5" o:value("223.5.5.5") o:value("223.6.6.6") o:value("119.29.29.29") +o:value("180.76.76.76") o:value("180.184.1.1") o:value("180.184.2.2") o:value("114.114.114.114") +o:value("114.114.115.115") o:depends("direct_dns_mode", "udp") o = s:taboption("DNS", Value, "direct_dns_tcp", translate("Direct DNS")) @@ -325,6 +327,8 @@ o:value("223.5.5.5") o:value("223.6.6.6") o:value("180.184.1.1") o:value("180.184.2.2") +o:value("114.114.114.114") +o:value("114.114.115.115") o:depends("direct_dns_mode", "tcp") o = s:taboption("DNS", Value, "direct_dns_dot", translate("Direct DNS DoT")) @@ -333,6 +337,8 @@ o:value("tls://dot.pub@1.12.12.12") o:value("tls://dot.pub@120.53.53.53") o:value("tls://dot.360.cn@36.99.170.86") o:value("tls://dot.360.cn@101.198.191.4") +o:value("tls://dns.alidns.com@223.5.5.5") +o:value("tls://dns.alidns.com@223.6.6.6") o:value("tls://dns.alidns.com@2400:3200::1") o:value("tls://dns.alidns.com@2400:3200:baba::1") o.validate = chinadns_dot_validate diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua index f1eb1c0bd1..f80a9f01d6 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua @@ -784,7 +784,7 @@ function to_check(arch, app_name) remote_version = remote_version:gsub(com[app_name].remote_version_str_replace, "") end local has_update = compare_versions(local_version:match("[^v]+"), "<", remote_version:match("[^v]+")) - +--[[ if not has_update then return { code = 0, @@ -792,7 +792,7 @@ function to_check(arch, app_name) remote_version = remote_version } end - +]]-- local asset = {} for _, v in ipairs(json.assets) do if v.name and v.name:match(match_file_name) then @@ -813,7 +813,7 @@ function to_check(arch, app_name) return { code = 0, - has_update = true, + has_update = has_update, local_version = local_version, remote_version = remote_version, html_url = json.html_url, @@ -1003,6 +1003,7 @@ function to_check_self() end local local_version = get_version() local remote_version = sys.exec("echo -n $(grep 'PKG_VERSION' /tmp/passwall_makefile|awk -F '=' '{print $2}')") + exec("/bin/rm", {"-f", tmp_file}) local has_update = compare_versions(local_version, "<", remote_version) if not has_update then diff --git a/openwrt-passwall/luci-app-passwall/luasrc/view/passwall/app_update/app_version.htm b/openwrt-passwall/luci-app-passwall/luasrc/view/passwall/app_update/app_version.htm index c49ab6feea..739c593063 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/view/passwall/app_update/app_version.htm +++ b/openwrt-passwall/luci-app-passwall/luasrc/view/passwall/app_update/app_version.htm @@ -10,6 +10,8 @@ local version = {} var inProgressCount = 0; var tokenStr = '<%=token%>'; var checkUpdateText = '<%:Check update%>'; + var forceUpdateText = '<%:Force update%>'; + var retryText = '<%:Retry%>'; var noUpdateText = '<%:It is the latest version%>'; var updateSuccessText = '<%:Update successful%>'; var clickToUpdateText = '<%:Click to update%>'; @@ -55,7 +57,7 @@ local version = {} function onRequestError(btn, errorMessage) { btn.disabled = false; - btn.value = checkUpdateText; + btn.value = retryText; var ckeckDetailElm = document.getElementById(btn.id + '-detail'); if (errorMessage && ckeckDetailElm) { @@ -90,8 +92,8 @@ local version = {} appInfoList[app] = undefined; onRequestError(btn, json.error); } else { + appInfoList[app] = json; if (json.has_update) { - appInfoList[app] = json; btn.disabled = false; btn.value = clickToUpdateText; btn.placeholder = clickToUpdateText; @@ -109,6 +111,7 @@ local version = {} } else { btn.disabled = true; btn.value = noUpdateText; + window['_' + app + '-force_btn'].style.display = "inline"; } } }, 300); @@ -197,6 +200,8 @@ local version = {} 【 <%=version[k] ~="" and version[k] or translate("Null") %> 】 +
diff --git a/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po b/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po index 82f6325611..269b13d711 100644 --- a/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po +++ b/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po @@ -841,6 +841,9 @@ msgstr "备用" msgid "Check update" msgstr "检查更新" +msgid "Force update" +msgstr "强制更新" + msgid "Manually update" msgstr "手动更新" @@ -934,6 +937,9 @@ msgstr "点击更新" msgid "Updating..." msgstr "更新中" +msgid "Retry" +msgstr "重试" + msgid "Unexpected error" msgstr "意外错误" diff --git a/shadowsocks-rust/Cargo.lock b/shadowsocks-rust/Cargo.lock index b20d24a191..17e68f0582 100644 --- a/shadowsocks-rust/Cargo.lock +++ b/shadowsocks-rust/Cargo.lock @@ -3280,7 +3280,7 @@ dependencies = [ [[package]] name = "shadowsocks" -version = "1.20.2" +version = "1.20.3" dependencies = [ "aes", "arc-swap", diff --git a/shadowsocks-rust/crates/shadowsocks/Cargo.toml b/shadowsocks-rust/crates/shadowsocks/Cargo.toml index f752f82b11..3bd03e8f67 100644 --- a/shadowsocks-rust/crates/shadowsocks/Cargo.toml +++ b/shadowsocks-rust/crates/shadowsocks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shadowsocks" -version = "1.20.2" +version = "1.20.3" authors = ["Shadowsocks Contributors"] description = "shadowsocks is a fast tunnel proxy that helps you bypass firewalls." repository = "https://github.com/shadowsocks/shadowsocks-rust" diff --git a/small/luci-app-homeproxy/po/zh-cn b/small/luci-app-homeproxy/po/zh-cn new file mode 120000 index 0000000000..8d69574ddd --- /dev/null +++ b/small/luci-app-homeproxy/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/small/luci-app-homeproxy/po/zh_Hans/homeproxy.po b/small/luci-app-homeproxy/po/zh_Hans/homeproxy.po index 1af8fc589b..389d6fadd2 100644 --- a/small/luci-app-homeproxy/po/zh_Hans/homeproxy.po +++ b/small/luci-app-homeproxy/po/zh_Hans/homeproxy.po @@ -217,11 +217,11 @@ msgstr "自动配置防火墙" #: htdocs/luci-static/resources/view/homeproxy/client.js:1121 msgid "Auto set backend" -msgstr "" +msgstr "自动设置后端" #: htdocs/luci-static/resources/view/homeproxy/client.js:1122 msgid "Auto set backend address for dashboard." -msgstr "" +msgstr "自动设置面板后端地址" #: htdocs/luci-static/resources/view/homeproxy/node.js:1398 msgid "Auto update" @@ -233,7 +233,7 @@ msgstr "自动更新订阅。" #: htdocs/luci-static/resources/view/homeproxy/client.js:1130 msgid "Automatically generated if empty" -msgstr "" +msgstr "如果为空则自动生成" #: htdocs/luci-static/resources/view/homeproxy/node.js:634 msgid "BBR" @@ -361,17 +361,17 @@ msgstr "思科公共 DNS(208.67.222.222)" #: htdocs/luci-static/resources/view/homeproxy/client.js:1060 msgid "Clash API settings" -msgstr "" +msgstr "Clash API 设置" #: htdocs/luci-static/resources/view/homeproxy/client.js:1090 #: htdocs/luci-static/resources/view/homeproxy/status.js:205 msgid "Clash dashboard version" -msgstr "" +msgstr "Clash 面板版本" #: htdocs/luci-static/resources/view/homeproxy/client.js:619 #: htdocs/luci-static/resources/view/homeproxy/client.js:964 msgid "Clash mode" -msgstr "" +msgstr "Clash 模式" #: htdocs/luci-static/resources/view/homeproxy/status.js:165 msgid "Clean log" @@ -401,7 +401,7 @@ msgstr "收集数据中..." #: htdocs/luci-static/resources/view/homeproxy/client.js:109 msgid "Command failed" -msgstr "" +msgstr "命令执行失败" #: htdocs/luci-static/resources/view/homeproxy/client.js:294 msgid "Common ports only (bypass P2P traffic)" @@ -418,7 +418,7 @@ msgstr "连接检查" #: htdocs/luci-static/resources/view/homeproxy/node.js:1403 msgid "Cron expression" -msgstr "" +msgstr "Cron 表达式" #: htdocs/luci-static/resources/view/homeproxy/client.js:282 msgid "Custom routing" @@ -483,7 +483,7 @@ msgstr "默认 DNS 解析策略" #: htdocs/luci-static/resources/view/homeproxy/node.js:728 msgid "Default Outbound" -msgstr "" +msgstr "默认出站" #: htdocs/luci-static/resources/view/homeproxy/client.js:799 msgid "Default domain strategy for resolving the domain names." @@ -641,7 +641,7 @@ msgstr "" #: htdocs/luci-static/resources/view/homeproxy/node.js:740 msgid "Drop/keep specific nodes from outbounds." -msgstr "" +msgstr "从出站规则中删除/保留 指定节点" #: htdocs/luci-static/resources/view/homeproxy/node.js:1435 msgid "Drop/keep specific nodes from subscriptions." @@ -696,7 +696,7 @@ msgstr "修改节点" #: htdocs/luci-static/resources/view/homeproxy/ruleset.js:82 msgid "Edit ruleset" -msgstr "" +msgstr "编辑规则集" #: htdocs/luci-static/resources/view/homeproxy/server.js:535 msgid "Email" @@ -733,7 +733,7 @@ msgstr "启用 ACME" #: htdocs/luci-static/resources/view/homeproxy/client.js:1065 msgid "Enable Clash API" -msgstr "" +msgstr "启用 clash API" #: htdocs/luci-static/resources/view/homeproxy/node.js:1117 msgid "Enable ECH" @@ -830,7 +830,7 @@ msgstr "外部账户密钥标识符" #: htdocs/luci-static/resources/view/homeproxy/client.js:113 msgid "Failed to execute \"/etc/init.d/%s %s\" action: %s" -msgstr "" +msgstr "执行“/etc/init.d/%s %s”操作失败:%s" #: htdocs/luci-static/resources/homeproxy.js:274 msgid "Failed to upload %s, error: %s." @@ -1047,7 +1047,7 @@ msgstr "空闲超时" #: htdocs/luci-static/resources/view/homeproxy/client.js:1017 msgid "If Any is selected, uncheck others" -msgstr "" +msgstr "如果选择了 '任意',取消勾选其他选项" #: htdocs/luci-static/resources/view/homeproxy/node.js:837 msgid "" @@ -1069,7 +1069,7 @@ msgstr "" #: htdocs/luci-static/resources/view/homeproxy/client.js:1089 msgid "If the selected dashboard is " -msgstr "" +msgstr "如果选定的仪表板是 " #: htdocs/luci-static/resources/view/homeproxy/node.js:822 #: htdocs/luci-static/resources/view/homeproxy/server.js:345 @@ -1099,7 +1099,7 @@ msgstr "导入" #: htdocs/luci-static/resources/view/homeproxy/ruleset.js:173 #: htdocs/luci-static/resources/view/homeproxy/ruleset.js:175 msgid "Import rule-set links" -msgstr "" +msgstr "导入规则集链接" #: htdocs/luci-static/resources/view/homeproxy/node.js:1262 #: htdocs/luci-static/resources/view/homeproxy/node.js:1364 @@ -1113,7 +1113,7 @@ msgstr "独立缓存" #: htdocs/luci-static/resources/view/homeproxy/client.js:1104 msgid "Installed" -msgstr "" +msgstr "已安装" #: htdocs/luci-static/resources/view/homeproxy/client.js:1143 msgid "Interface Control" @@ -1121,15 +1121,15 @@ msgstr "接口控制" #: htdocs/luci-static/resources/view/homeproxy/node.js:782 msgid "Interrupt existing connections" -msgstr "" +msgstr "中断现有连接"" #: htdocs/luci-static/resources/view/homeproxy/node.js:783 msgid "Interrupt existing connections when the selected outbound has changed." -msgstr "" +msgstr "当选定的出站规则更改时,中断现有连接" #: htdocs/luci-static/resources/view/homeproxy/node.js:763 msgid "Interval" -msgstr "" +msgstr "间隔" #: htdocs/luci-static/resources/view/homeproxy/node.js:662 #: htdocs/luci-static/resources/view/homeproxy/server.js:297 @@ -1194,11 +1194,11 @@ msgstr "分配给接口的 IP(v4 或 v6)地址前缀列表。" #: htdocs/luci-static/resources/view/homeproxy/node.js:720 msgid "List of outbound tags." -msgstr "" +msgstr "出站标签列表" #: htdocs/luci-static/resources/view/homeproxy/node.js:709 msgid "List of subscription groups." -msgstr "" +msgstr "订阅组列表" #: htdocs/luci-static/resources/view/homeproxy/node.js:1058 #: htdocs/luci-static/resources/view/homeproxy/server.js:488 @@ -1239,11 +1239,11 @@ msgstr "日志为空。" #: htdocs/luci-static/resources/view/homeproxy/client.js:1078 msgid "Log level" -msgstr "" +msgstr "日志级别" #: htdocs/luci-static/resources/homeproxy.js:237 msgid "Lowercase only" -msgstr "" +msgstr "仅小写" #: htdocs/luci-static/resources/view/homeproxy/node.js:955 msgid "MTU" @@ -1290,7 +1290,7 @@ msgstr "匹配 IP CIDR。" #: htdocs/luci-static/resources/view/homeproxy/client.js:620 #: htdocs/luci-static/resources/view/homeproxy/client.js:965 msgid "Match clash mode." -msgstr "" +msgstr "匹配 Clash 模式" #: htdocs/luci-static/resources/view/homeproxy/client.js:556 #: htdocs/luci-static/resources/view/homeproxy/client.js:901 @@ -1499,7 +1499,7 @@ msgstr "New Reno" #: htdocs/luci-static/resources/view/homeproxy/client.js:1068 msgid "Nginx Support" -msgstr "" +msgstr "Nginx 支持" #: htdocs/luci-static/resources/view/homeproxy/node.js:792 #: htdocs/luci-static/resources/view/homeproxy/node.js:809 @@ -1523,7 +1523,7 @@ msgstr "无订阅节点" #: htdocs/luci-static/resources/view/homeproxy/ruleset.js:130 msgid "No valid rule-set link found." -msgstr "" +msgstr "未找到有效的规则集链接" #: htdocs/luci-static/resources/view/homeproxy/node.js:1301 msgid "No valid share link found." @@ -1577,7 +1577,7 @@ msgstr "仅代理中国大陆" #: htdocs/luci-static/resources/view/homeproxy/client.js:97 msgid "Open Clash Dashboard" -msgstr "" +msgstr " Clash 面板" #: htdocs/luci-static/resources/view/homeproxy/client.js:502 #: htdocs/luci-static/resources/view/homeproxy/client.js:843 @@ -1598,7 +1598,7 @@ msgstr "出站节点" #: htdocs/luci-static/resources/view/homeproxy/node.js:719 msgid "Outbounds" -msgstr "" +msgstr "出站" #: htdocs/luci-static/resources/view/homeproxy/node.js:464 msgid "Override address" @@ -1817,7 +1817,7 @@ msgstr "请求类型" #: htdocs/luci-static/resources/view/homeproxy/client.js:400 msgid "Quick Reload" -msgstr "" +msgstr "快速加载" #: htdocs/luci-static/resources/view/homeproxy/client.js:724 msgid "RDRC timeout" @@ -1880,7 +1880,7 @@ msgstr "区域 ID" #: htdocs/luci-static/resources/view/homeproxy/client.js:401 msgid "Reload" -msgstr "" +msgstr "加载" #: htdocs/luci-static/resources/view/homeproxy/ruleset.js:196 msgid "Remote" @@ -1945,7 +1945,7 @@ msgstr "路由规则" #: htdocs/luci-static/resources/view/homeproxy/client.js:623 #: htdocs/luci-static/resources/view/homeproxy/client.js:968 msgid "Rule" -msgstr "" +msgstr "规则" #: htdocs/luci-static/resources/view/homeproxy/client.js:627 #: htdocs/luci-static/resources/view/homeproxy/client.js:972 @@ -1963,7 +1963,7 @@ msgstr "规则集 URL" #: root/usr/share/luci/menu.d/luci-app-homeproxy.json:30 msgid "Ruleset Settings" -msgstr "" +msgstr "规则集设置" #: htdocs/luci-static/resources/view/homeproxy/client.js:505 #: htdocs/luci-static/resources/view/homeproxy/client.js:846 @@ -2007,15 +2007,15 @@ msgstr "保存订阅设置" #: htdocs/luci-static/resources/view/homeproxy/client.js:1130 msgid "Secret" -msgstr "" +msgstr "密钥" #: htdocs/luci-static/resources/view/homeproxy/client.js:1088 msgid "Select Clash Dashboard" -msgstr "" +msgstr "选择 Clash 面板" #: htdocs/luci-static/resources/view/homeproxy/node.js:412 msgid "Selector" -msgstr "" +msgstr "选择项" #: htdocs/luci-static/resources/view/homeproxy/client.js:1023 #: htdocs/luci-static/resources/view/homeproxy/server.js:89 @@ -2182,7 +2182,7 @@ msgstr "成功导入 %s 个节点,共 %s 个。" #: htdocs/luci-static/resources/view/homeproxy/ruleset.js:132 msgid "Successfully imported %s rule-set of total %s." -msgstr "" +msgstr "成功导入 %s 个规则集,共 %s 个。" #: htdocs/luci-static/resources/view/homeproxy/status.js:85 msgid "Successfully updated." @@ -2290,7 +2290,7 @@ msgstr "腾讯公共 DNS(119.29.29.29)" #: htdocs/luci-static/resources/view/homeproxy/node.js:756 msgid "Test URL" -msgstr "" +msgstr "测试 URL" #: htdocs/luci-static/resources/view/homeproxy/server.js:551 msgid "The ACME CA provider to use." @@ -2313,7 +2313,7 @@ msgstr "用于接收数据的 QUIC 流级流控制窗口。" #: htdocs/luci-static/resources/view/homeproxy/node.js:757 msgid "" "The URL to test. https://www.gstatic.com/generate_204 will be used if empty." -msgstr "" +msgstr "用于测试的 URL。如果为空,将使用 https://www.gstatic.com/generate_204。" #: htdocs/luci-static/resources/view/homeproxy/client.js:757 msgid "The address of the dns server. Support UDP, TCP, DoT, DoH and RCode." @@ -2337,15 +2337,15 @@ msgstr "" #: htdocs/luci-static/resources/view/homeproxy/client.js:1113 #: htdocs/luci-static/resources/view/homeproxy/client.js:1116 msgid "The current API URL is %s" -msgstr "" +msgstr "当前的 API URL 是 `%s`" #: htdocs/luci-static/resources/view/homeproxy/client.js:1133 msgid "The current Secret is " -msgstr "" +msgstr "当前的密钥是 " #: htdocs/luci-static/resources/view/homeproxy/node.js:729 msgid "The default outbound tag. The first outbound will be used if empty." -msgstr "" +msgstr "默认出站标签。如果为空,将使用第一个出站标签。" #: htdocs/luci-static/resources/view/homeproxy/client.js:519 msgid "" @@ -2381,7 +2381,7 @@ msgstr "" #: htdocs/luci-static/resources/view/homeproxy/node.js:1404 msgid "The default value is 2:00 every day" -msgstr "" +msgstr "默认值是每天 2:00" #: htdocs/luci-static/resources/view/homeproxy/client.js:793 msgid "" @@ -2404,7 +2404,7 @@ msgstr "创建或选择现有 ACME 服务器帐户时使用的电子邮件地址 #: htdocs/luci-static/resources/view/homeproxy/node.js:777 msgid "The idle timeout. 30m will be used if empty." -msgstr "" +msgstr "空闲超时。如果为空,将使用 30m" #: htdocs/luci-static/resources/view/homeproxy/node.js:1080 #: htdocs/luci-static/resources/view/homeproxy/server.js:501 @@ -2459,11 +2459,11 @@ msgstr "上游出站的标签。
启用时,其他拨号字段将被忽略 #: htdocs/luci-static/resources/view/homeproxy/node.js:764 msgid "The test interval. 3m will be used if empty." -msgstr "" +msgstr "测试间隔时间。如果为空,将使用 `3m`。" #: htdocs/luci-static/resources/view/homeproxy/node.js:771 msgid "The test tolerance in milliseconds. 50 will be used if empty." -msgstr "" +msgstr "测试灵敏度(毫秒)。如果为空,将使用 50。`。" #: htdocs/luci-static/resources/view/homeproxy/node.js:825 #: htdocs/luci-static/resources/view/homeproxy/server.js:385 @@ -2514,11 +2514,12 @@ msgstr "" msgid "" "To enable this feature you need install luci-nginx and luci-ssl-" "nginx
first" -msgstr "" +msgstr "要启用此功能,您需要安装luci-nginx and luci-ssl-" +"nginx
" #: htdocs/luci-static/resources/view/homeproxy/node.js:770 msgid "Tolerance" -msgstr "" +msgstr "灵敏度" #: htdocs/luci-static/resources/view/homeproxy/node.js:791 #: htdocs/luci-static/resources/view/homeproxy/server.js:319 @@ -2574,7 +2575,7 @@ msgstr "UDP 中继模式" #: htdocs/luci-static/resources/view/homeproxy/node.js:413 msgid "URLTest" -msgstr "" +msgstr "URL测试" #: htdocs/luci-static/resources/view/homeproxy/node.js:623 #: htdocs/luci-static/resources/view/homeproxy/server.js:266 @@ -2652,7 +2653,7 @@ msgstr "使用 ACME TLS 证书颁发机构。" #: htdocs/luci-static/resources/view/homeproxy/client.js:1101 msgid "Use Online Dashboard" -msgstr "" +msgstr "使用在线面板" #: htdocs/luci-static/resources/view/homeproxy/node.js:1053 #: htdocs/luci-static/resources/view/homeproxy/server.js:483 @@ -2778,7 +2779,7 @@ msgstr "" #: htdocs/luci-static/resources/view/homeproxy/node.js:1335 msgid "node" -msgstr "" +msgstr "节点" #: htdocs/luci-static/resources/homeproxy.js:292 #: htdocs/luci-static/resources/homeproxy.js:310 @@ -2823,7 +2824,7 @@ msgstr "sing-box 服务端" #: htdocs/luci-static/resources/view/homeproxy/node.js:1336 msgid "sub" -msgstr "" +msgstr "订阅" #: htdocs/luci-static/resources/view/homeproxy/node.js:1139 msgid "uTLS fingerprint" @@ -2850,7 +2851,7 @@ msgstr "独立 UCI 标识" #: htdocs/luci-static/resources/view/homeproxy/node.js:1353 #: htdocs/luci-static/resources/view/homeproxy/ruleset.js:164 msgid "unique label" -msgstr "" +msgstr "唯一标签" #: htdocs/luci-static/resources/homeproxy.js:301 msgid "unique value" diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index 4f8f1b9051..cd9d8d4a86 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -313,9 +313,11 @@ o.default = "223.5.5.5" o:value("223.5.5.5") o:value("223.6.6.6") o:value("119.29.29.29") +o:value("180.76.76.76") o:value("180.184.1.1") o:value("180.184.2.2") o:value("114.114.114.114") +o:value("114.114.115.115") o:depends("direct_dns_mode", "udp") o = s:taboption("DNS", Value, "direct_dns_tcp", translate("Direct DNS")) @@ -325,6 +327,8 @@ o:value("223.5.5.5") o:value("223.6.6.6") o:value("180.184.1.1") o:value("180.184.2.2") +o:value("114.114.114.114") +o:value("114.114.115.115") o:depends("direct_dns_mode", "tcp") o = s:taboption("DNS", Value, "direct_dns_dot", translate("Direct DNS DoT")) @@ -333,6 +337,8 @@ o:value("tls://dot.pub@1.12.12.12") o:value("tls://dot.pub@120.53.53.53") o:value("tls://dot.360.cn@36.99.170.86") o:value("tls://dot.360.cn@101.198.191.4") +o:value("tls://dns.alidns.com@223.5.5.5") +o:value("tls://dns.alidns.com@223.6.6.6") o:value("tls://dns.alidns.com@2400:3200::1") o:value("tls://dns.alidns.com@2400:3200:baba::1") o.validate = chinadns_dot_validate diff --git a/small/luci-app-passwall/luasrc/passwall/api.lua b/small/luci-app-passwall/luasrc/passwall/api.lua index f1eb1c0bd1..f80a9f01d6 100644 --- a/small/luci-app-passwall/luasrc/passwall/api.lua +++ b/small/luci-app-passwall/luasrc/passwall/api.lua @@ -784,7 +784,7 @@ function to_check(arch, app_name) remote_version = remote_version:gsub(com[app_name].remote_version_str_replace, "") end local has_update = compare_versions(local_version:match("[^v]+"), "<", remote_version:match("[^v]+")) - +--[[ if not has_update then return { code = 0, @@ -792,7 +792,7 @@ function to_check(arch, app_name) remote_version = remote_version } end - +]]-- local asset = {} for _, v in ipairs(json.assets) do if v.name and v.name:match(match_file_name) then @@ -813,7 +813,7 @@ function to_check(arch, app_name) return { code = 0, - has_update = true, + has_update = has_update, local_version = local_version, remote_version = remote_version, html_url = json.html_url, @@ -1003,6 +1003,7 @@ function to_check_self() end local local_version = get_version() local remote_version = sys.exec("echo -n $(grep 'PKG_VERSION' /tmp/passwall_makefile|awk -F '=' '{print $2}')") + exec("/bin/rm", {"-f", tmp_file}) local has_update = compare_versions(local_version, "<", remote_version) if not has_update then diff --git a/small/luci-app-passwall/luasrc/view/passwall/app_update/app_version.htm b/small/luci-app-passwall/luasrc/view/passwall/app_update/app_version.htm index c49ab6feea..739c593063 100644 --- a/small/luci-app-passwall/luasrc/view/passwall/app_update/app_version.htm +++ b/small/luci-app-passwall/luasrc/view/passwall/app_update/app_version.htm @@ -10,6 +10,8 @@ local version = {} var inProgressCount = 0; var tokenStr = '<%=token%>'; var checkUpdateText = '<%:Check update%>'; + var forceUpdateText = '<%:Force update%>'; + var retryText = '<%:Retry%>'; var noUpdateText = '<%:It is the latest version%>'; var updateSuccessText = '<%:Update successful%>'; var clickToUpdateText = '<%:Click to update%>'; @@ -55,7 +57,7 @@ local version = {} function onRequestError(btn, errorMessage) { btn.disabled = false; - btn.value = checkUpdateText; + btn.value = retryText; var ckeckDetailElm = document.getElementById(btn.id + '-detail'); if (errorMessage && ckeckDetailElm) { @@ -90,8 +92,8 @@ local version = {} appInfoList[app] = undefined; onRequestError(btn, json.error); } else { + appInfoList[app] = json; if (json.has_update) { - appInfoList[app] = json; btn.disabled = false; btn.value = clickToUpdateText; btn.placeholder = clickToUpdateText; @@ -109,6 +111,7 @@ local version = {} } else { btn.disabled = true; btn.value = noUpdateText; + window['_' + app + '-force_btn'].style.display = "inline"; } } }, 300); @@ -197,6 +200,8 @@ local version = {} 【 <%=version[k] ~="" and version[k] or translate("Null") %> 】 +
diff --git a/small/luci-app-passwall/po/zh-cn/passwall.po b/small/luci-app-passwall/po/zh-cn/passwall.po index 82f6325611..269b13d711 100644 --- a/small/luci-app-passwall/po/zh-cn/passwall.po +++ b/small/luci-app-passwall/po/zh-cn/passwall.po @@ -841,6 +841,9 @@ msgstr "备用" msgid "Check update" msgstr "检查更新" +msgid "Force update" +msgstr "强制更新" + msgid "Manually update" msgstr "手动更新" @@ -934,6 +937,9 @@ msgstr "点击更新" msgid "Updating..." msgstr "更新中" +msgid "Retry" +msgstr "重试" + msgid "Unexpected error" msgstr "意外错误" diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile index e17955dd7a..1b994c8c83 100644 --- a/small/v2ray-geodata/Makefile +++ b/small/v2ray-geodata/Makefile @@ -21,13 +21,13 @@ define Download/geoip HASH:=944465ad5f3a3cccebf2930624f528cae3ca054f69295979cf4c4e002a575e90 endef -GEOSITE_VER:=20240905162746 +GEOSITE_VER:=20240907043125 GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER) define Download/geosite URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/ URL_FILE:=dlc.dat FILE:=$(GEOSITE_FILE) - HASH:=859306b7bc3a7891d5e0f5c8f38c2eaa8ede776c3a0aa1512b96c4956cf511c1 + HASH:=af7a202728ceab4e049eb38cba31136ef3d7eca7bf56e62fba10eaa7117820c7 endef GEOSITE_IRAN_VER:=202409020032 diff --git a/v2rayn/v2rayN/ServiceLib/Common/FileManager.cs b/v2rayn/v2rayN/ServiceLib/Common/FileManager.cs index 95a17adb3d..2e952bd8cd 100644 --- a/v2rayn/v2rayN/ServiceLib/Common/FileManager.cs +++ b/v2rayn/v2rayN/ServiceLib/Common/FileManager.cs @@ -106,7 +106,12 @@ namespace ServiceLib.Common { try { - ZipFile.CreateFromDirectory(sourceDirectoryName, destinationArchiveFileName); + if (File.Exists(destinationArchiveFileName)) + { + File.Delete(destinationArchiveFileName); + } + + ZipFile.CreateFromDirectory(sourceDirectoryName, destinationArchiveFileName, CompressionLevel.SmallestSize, true); } catch (Exception ex) { @@ -115,5 +120,42 @@ namespace ServiceLib.Common } return true; } + + public static void CopyDirectory(string sourceDir, string destinationDir, bool recursive, string ignoredName) + { + // Get information about the source directory + var dir = new DirectoryInfo(sourceDir); + + // Check if the source directory exists + if (!dir.Exists) + throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); + + // Cache directories before we start copying + DirectoryInfo[] dirs = dir.GetDirectories(); + + // Create the destination directory + Directory.CreateDirectory(destinationDir); + + // Get the files in the source directory and copy to the destination directory + foreach (FileInfo file in dir.GetFiles()) + { + if (!Utils.IsNullOrEmpty(ignoredName) && file.Name.Contains(ignoredName)) + { + continue; + } + string targetFilePath = Path.Combine(destinationDir, file.Name); + file.CopyTo(targetFilePath); + } + + // If recursive and copying subdirectories, recursively call this method + if (recursive) + { + foreach (DirectoryInfo subDir in dirs) + { + string newDestinationDir = Path.Combine(destinationDir, subDir.Name); + CopyDirectory(subDir.FullName, newDestinationDir, true, ignoredName); + } + } + } } } \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs index b55e1d9186..f949e5ce71 100644 --- a/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -204,6 +204,8 @@ namespace ServiceLib.Handler }; } + config.webDavItem ??= new(); + return 0; } diff --git a/v2rayn/v2rayN/ServiceLib/Handler/NoticeHandler.cs b/v2rayn/v2rayN/ServiceLib/Handler/NoticeHandler.cs index 912eda1523..8efb12d781 100644 --- a/v2rayn/v2rayN/ServiceLib/Handler/NoticeHandler.cs +++ b/v2rayn/v2rayN/ServiceLib/Handler/NoticeHandler.cs @@ -22,7 +22,7 @@ namespace ServiceLib.Handler MessageBus.Current.SendMessage(content, Global.CommandSendMsgView); } - public void SendMessageEx(string? content ) + public void SendMessageEx(string? content) { if (content.IsNullOrEmpty()) { diff --git a/v2rayn/v2rayN/ServiceLib/Handler/WebDavHandler.cs b/v2rayn/v2rayN/ServiceLib/Handler/WebDavHandler.cs new file mode 100644 index 0000000000..fd1d5e55c4 --- /dev/null +++ b/v2rayn/v2rayN/ServiceLib/Handler/WebDavHandler.cs @@ -0,0 +1,162 @@ +using System.Net; +using WebDav; + +namespace ServiceLib.Handler +{ + public sealed class WebDavHandler + { + private static readonly Lazy _instance = new(() => new()); + public static WebDavHandler Instance => _instance.Value; + + private Config? _config; + private WebDavClient? _client; + private string? _lastDescription; + private string _webDir = Global.AppName + "_backup"; + private string _webFileName = "backup.zip"; + private string _logTitle = "WebDav--"; + + public WebDavHandler() + { + _config = LazyConfig.Instance.Config; + } + + private async Task GetClient() + { + try + { + if (_config.webDavItem.url.IsNullOrEmpty() + || _config.webDavItem.userName.IsNullOrEmpty() + || _config.webDavItem.password.IsNullOrEmpty()) + { + throw new ArgumentException("webdav parameter error or null"); + } + if (_client != null) + { + _client?.Dispose(); + _client = null; + } + + var clientParams = new WebDavClientParams + { + BaseAddress = new Uri(_config.webDavItem.url), + Credentials = new NetworkCredential(_config.webDavItem.userName, _config.webDavItem.password) + }; + _client = new WebDavClient(clientParams); + } + catch (Exception ex) + { + SaveLog(ex); + return false; + } + return await Task.FromResult(true); + } + + private async Task TryCreateDir() + { + if (_client is null) return false; + try + { + var result2 = await _client.Mkcol(_webDir); + if (result2.IsSuccessful) + { + return true; + } + SaveLog(result2.Description); + } + catch (Exception ex) + { + SaveLog(ex); + } + return false; + } + + private void SaveLog(string desc) + { + _lastDescription = desc; + Logging.SaveLog(_logTitle + desc); + } + + private void SaveLog(Exception ex) + { + _lastDescription = ex.Message; + Logging.SaveLog(_logTitle, ex); + } + + public async Task CheckConnection() + { + if (await GetClient() == false) + { + return false; + } + await TryCreateDir(); + + var testName = "readme_test"; + var myContent = new StringContent(testName); + var result = await _client.PutFile($"{_webDir}/{testName}", myContent); + if (result.IsSuccessful) + { + await _client.Delete($"{_webDir}/{testName}"); + return true; + } + else + { + SaveLog(result.Description); + return false; + } + } + + public async Task PutFile(string fileName) + { + if (await GetClient() == false) + { + return false; + } + await TryCreateDir(); + + try + { + using var fs = File.OpenRead(fileName); + var result = await _client.PutFile($"{_webDir}/{_webFileName}", fs); // upload a resource + if (result.IsSuccessful) + { + return true; + } + + SaveLog(result.Description); + } + catch (Exception ex) + { + SaveLog(ex); + } + return false; + } + + public async Task GetRawFile(string fileName) + { + if (await GetClient() == false) + { + return false; + } + await TryCreateDir(); + + try + { + var response = await _client.GetRawFile($"{_webDir}/{_webFileName}"); + if (!response.IsSuccessful) + { + SaveLog(response.Description); + } + using var outputFileStream = new FileStream(fileName, FileMode.Create); + response.Stream.CopyTo(outputFileStream); + return true; + } + catch (Exception ex) + { + SaveLog(ex); + } + return false; + } + + public string GetLastError() => _lastDescription ?? string.Empty; + } +} \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Models/Config.cs b/v2rayn/v2rayN/ServiceLib/Models/Config.cs index 4062914c40..84de350ac0 100644 --- a/v2rayn/v2rayN/ServiceLib/Models/Config.cs +++ b/v2rayn/v2rayN/ServiceLib/Models/Config.cs @@ -47,6 +47,7 @@ public HysteriaItem hysteriaItem { get; set; } public ClashUIItem clashUIItem { get; set; } public SystemProxyItem systemProxyItem { get; set; } + public WebDavItem webDavItem { get; set; } public List inbound { get; set; } public List globalHotkeys { get; set; } public List coreTypeItem { get; set; } diff --git a/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs index b07c14423e..ec09ab6f93 100644 --- a/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs @@ -248,4 +248,12 @@ public bool notProxyLocalAddress { get; set; } = true; public string systemProxyAdvancedProtocol { get; set; } } + + [Serializable] + public class WebDavItem + { + public string? url { get; set; } + public string? userName { get; set; } + public string? password { get; set; } + } } \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 25f52f6bf3..dadb11ed6e 100644 --- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -582,6 +582,42 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 WebDav Check 的本地化字符串。 + /// + public static string LvWebDavCheck { + get { + return ResourceManager.GetString("LvWebDavCheck", resourceCulture); + } + } + + /// + /// 查找类似 WebDav Password 的本地化字符串。 + /// + public static string LvWebDavPassword { + get { + return ResourceManager.GetString("LvWebDavPassword", resourceCulture); + } + } + + /// + /// 查找类似 WebDav Url 的本地化字符串。 + /// + public static string LvWebDavUrl { + get { + return ResourceManager.GetString("LvWebDavUrl", resourceCulture); + } + } + + /// + /// 查找类似 WebDav User Name 的本地化字符串。 + /// + public static string LvWebDavUserName { + get { + return ResourceManager.GetString("LvWebDavUserName", resourceCulture); + } + } + /// /// 查找类似 Add a custom configuration server 的本地化字符串。 /// @@ -690,6 +726,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Backup and Restore 的本地化字符串。 + /// + public static string menuBackupAndRestore { + get { + return ResourceManager.GetString("menuBackupAndRestore", resourceCulture); + } + } + /// /// 查找类似 Check Update 的本地化字符串。 /// @@ -861,6 +906,33 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Backup to local 的本地化字符串。 + /// + public static string menuLocalBackup { + get { + return ResourceManager.GetString("menuLocalBackup", resourceCulture); + } + } + + /// + /// 查找类似 Local 的本地化字符串。 + /// + public static string menuLocalBackupAndRestore { + get { + return ResourceManager.GetString("menuLocalBackupAndRestore", resourceCulture); + } + } + + /// + /// 查找类似 Restore from local 的本地化字符串。 + /// + public static string menuLocalRestore { + get { + return ResourceManager.GetString("menuLocalRestore", resourceCulture); + } + } + /// /// 查找类似 One-click multi test Latency and speed (Ctrl+E) 的本地化字符串。 /// @@ -1095,6 +1167,33 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Backup to remote (WebDAV) 的本地化字符串。 + /// + public static string menuRemoteBackup { + get { + return ResourceManager.GetString("menuRemoteBackup", resourceCulture); + } + } + + /// + /// 查找类似 Remote (WebDAV) 的本地化字符串。 + /// + public static string menuRemoteBackupAndRestore { + get { + return ResourceManager.GetString("menuRemoteBackupAndRestore", resourceCulture); + } + } + + /// + /// 查找类似 Restore from remote (WebDAV) 的本地化字符串。 + /// + public static string menuRemoteRestore { + get { + return ResourceManager.GetString("menuRemoteRestore", resourceCulture); + } + } + /// /// 查找类似 Remove duplicate servers 的本地化字符串。 /// diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.resx index e40c318f91..87971fbc9c 100644 --- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1279,4 +1279,37 @@ Custom config socks port + + Backup and Restore + + + Backup to local + + + Restore from local + + + Backup to remote (WebDAV) + + + Restore from remote (WebDAV) + + + Local + + + Remote (WebDAV) + + + WebDav Url + + + WebDav User Name + + + WebDav Password + + + WebDav Check + \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index c0e9deb98e..d6ae1fc2cd 100644 --- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1276,4 +1276,37 @@ 自定义配置的Socks端口 + + 备份和还原 + + + 备份到本地 + + + 从本地恢复 + + + 备份到远程 (WebDAV) + + + 从远程恢复 (WebDAV) + + + 本地 + + + 远程 (WebDAV) + + + WebDav 账户 + + + WebDav 可用检查 + + + WebDav 密码 + + + WebDav 服务器地址 + \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 67679f3d03..571243c911 100644 --- a/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayn/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1156,4 +1156,37 @@ 自訂配置的Socks端口 + + 備份和還原 + + + 備份到本地 + + + 從本地恢復 + + + 備份到遠端 (WebDAV) + + + 從遠端恢復 (WebDAV) + + + 本地 + + + 遠端 (WebDAV) + + + WebDav 賬戶 + + + WebDav 可用檢查 + + + WebDav 密碼 + + + WebDav 服務器地址 + \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/ServiceLib.csproj b/v2rayn/v2rayN/ServiceLib/ServiceLib.csproj index 8185bec9d4..79955b48ec 100644 --- a/v2rayn/v2rayN/ServiceLib/ServiceLib.csproj +++ b/v2rayn/v2rayN/ServiceLib/ServiceLib.csproj @@ -13,6 +13,7 @@ + diff --git a/v2rayn/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs b/v2rayn/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs new file mode 100644 index 0000000000..1e3d0d0703 --- /dev/null +++ b/v2rayn/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs @@ -0,0 +1,151 @@ +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Splat; +using System.Reactive; + +namespace ServiceLib.ViewModels +{ + public class BackupAndRestoreViewModel : MyReactiveObject + { + public ReactiveCommand RemoteBackupCmd { get; } + public ReactiveCommand RemoteRestoreCmd { get; } + public ReactiveCommand WebDavCheckCmd { get; } + + [Reactive] + public WebDavItem SelectedSource { get; set; } + + [Reactive] + public string OperationMsg { get; set; } + + public BackupAndRestoreViewModel(Func>? updateView) + { + _config = LazyConfig.Instance.Config; + _updateView = updateView; + _noticeHandler = Locator.Current.GetService(); + + WebDavCheckCmd = ReactiveCommand.CreateFromTask(async () => + { + await WebDavCheck(); + }); + + RemoteBackupCmd = ReactiveCommand.CreateFromTask(async () => + { + await RemoteBackup(); + }); + RemoteRestoreCmd = ReactiveCommand.CreateFromTask(async () => + { + await RemoteRestore(); + }); + + SelectedSource = JsonUtils.DeepCopy(_config.webDavItem); + } + + private void DisplayOperationMsg(string msg = "") + { + OperationMsg = msg; + } + + private async Task WebDavCheck() + { + DisplayOperationMsg(); + _config.webDavItem = SelectedSource; + ConfigHandler.SaveConfig(_config); + + var result = await WebDavHandler.Instance.CheckConnection(); + if (result) + { + DisplayOperationMsg(ResUI.OperationSuccess); + } + else + { + DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + } + } + + private async Task RemoteBackup() + { + DisplayOperationMsg(); + var fileName = Utils.GetBackupPath($"backup_{DateTime.Now:yyyyMMddHHmmss}.zip"); + var result = await CreateZipFileFromDirectory(fileName); + if (result) + { + var result2 = await WebDavHandler.Instance.PutFile(fileName); + if (result2) + { + DisplayOperationMsg(ResUI.OperationSuccess); + return; + } + } + + DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + } + + private async Task RemoteRestore() + { + DisplayOperationMsg(); + var fileName = Utils.GetTempPath(Utils.GetGUID()); + var result = await WebDavHandler.Instance.GetRawFile(fileName); + if (result) + { + await LocalRestore(fileName); + return; + } + + DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + } + + public async Task LocalBackup(string fileName) + { + DisplayOperationMsg(); + var result = await CreateZipFileFromDirectory(fileName); + if (result) + { + DisplayOperationMsg(ResUI.OperationSuccess); + } + else + { + DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + } + + return result; + } + + public async Task LocalRestore(string fileName) + { + DisplayOperationMsg(); + if (Utils.IsNullOrEmpty(fileName)) + { + return; + } + + //backup first + var fileBackup = Utils.GetBackupPath($"backup_{DateTime.Now:yyyyMMddHHmmss}.zip"); + var result = await CreateZipFileFromDirectory(fileBackup); + if (result) + { + Locator.Current.GetService()?.V2rayUpgrade(fileName); + } + else + { + DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + } + } + + private async Task CreateZipFileFromDirectory(string fileName) + { + if (Utils.IsNullOrEmpty(fileName)) + { + return false; + } + + var configDir = Utils.GetConfigPath(); + var configDirZipTemp = Utils.GetTempPath($"v2rayN_{DateTime.Now:yyyyMMddHHmmss}"); + var configDirTemp = Path.Combine(configDirZipTemp, "guiConfigs"); + + await Task.Run(() => FileManager.CopyDirectory(configDir, configDirTemp, true, "cache.db")); + var ret = await Task.Run(() => FileManager.CreateFromDirectory(configDirZipTemp, fileName)); + await Task.Run(() => Directory.Delete(configDirZipTemp, true)); + return ret; + } + } +} \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs b/v2rayn/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs index da18f0cec9..b0966221b7 100644 --- a/v2rayn/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs +++ b/v2rayn/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs @@ -3,7 +3,6 @@ using DynamicData.Binding; using ReactiveUI; using ReactiveUI.Fody.Helpers; using Splat; -using System.Diagnostics; using System.Reactive; namespace ServiceLib.ViewModels @@ -234,21 +233,7 @@ namespace ServiceLib.ViewModels { return; } - - Process process = new() - { - StartInfo = new ProcessStartInfo - { - FileName = "v2rayUpgrade", - Arguments = fileName.AppendQuotes(), - WorkingDirectory = Utils.StartupPath() - } - }; - process.Start(); - if (process.Id > 0) - { - Locator.Current.GetService()?.MyAppExitAsync(false); - } + Locator.Current.GetService()?.V2rayUpgrade(fileName); } catch (Exception ex) { diff --git a/v2rayn/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayn/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index f5a110ad03..c11f60350d 100644 --- a/v2rayn/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayn/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -428,6 +428,24 @@ namespace ServiceLib.ViewModels } } + public async Task V2rayUpgrade(string fileName) + { + Process process = new() + { + StartInfo = new ProcessStartInfo + { + FileName = "v2rayUpgrade", + Arguments = fileName.AppendQuotes(), + WorkingDirectory = Utils.StartupPath() + } + }; + process.Start(); + if (process.Id > 0) + { + await MyAppExitAsync(false); + } + } + #endregion Actions #region Servers && Groups diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/CheckUpdateView.axaml.cs b/v2rayn/v2rayN/v2rayN.Desktop/Views/CheckUpdateView.axaml.cs index ea1c0ddf23..a11a72fadb 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/Views/CheckUpdateView.axaml.cs +++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/CheckUpdateView.axaml.cs @@ -28,7 +28,7 @@ namespace v2rayN.Desktop.Views switch (action) { case EViewAction.DispatcherCheckUpdate: - if (obj is null) return false; + if (obj is null) return false; Dispatcher.UIThread.Post(() => ViewModel?.UpdateViewResult((CheckUpdateItem)obj), DispatcherPriority.Default); diff --git a/v2rayn/v2rayN/v2rayN/Views/BackupAndRestoreView.xaml b/v2rayn/v2rayN/v2rayN/Views/BackupAndRestoreView.xaml new file mode 100644 index 0000000000..fc4a0bb622 --- /dev/null +++ b/v2rayn/v2rayN/v2rayN/Views/BackupAndRestoreView.xaml @@ -0,0 +1,239 @@ + + + + + + + + + + +