From 77d354874efd167bff3070430cc0274b7a586173 Mon Sep 17 00:00:00 2001 From: arm64v8a <48624112+arm64v8a@users.noreply.github.com> Date: Tue, 3 May 2022 19:25:17 +0800 Subject: [PATCH] upload code --- .github/workflows/build-qv2ray-cmake.yml | 146 ++ .gitignore | 82 + .gitmodules | 3 + 3rdparty/QHotkey | 1 + 3rdparty/QThreadCreateThread.hpp | 38 + 3rdparty/QtExtKeySequenceEdit.cpp | 22 + 3rdparty/QtExtKeySequenceEdit.h | 11 + 3rdparty/RunGuard.hpp | 98 ++ 3rdparty/VT100Parser.hpp | 22 + 3rdparty/WinCommander.cpp | 114 ++ 3rdparty/WinCommander.hpp | 42 + 3rdparty/ZxingQtReader.hpp | 386 +++++ 3rdparty/qrcodegen.cpp | 830 ++++++++++ 3rdparty/qrcodegen.hpp | 549 +++++++ CMakeLists.txt | 245 +++ README.md | 126 ++ assets/nekoray.png | Bin 0 -> 80637 bytes assets/qtbase_zh_CN.qm | Bin 0 -> 133919 bytes cmake/fuck_windows/VersionInfo.in | 82 + cmake/fuck_windows/VersionResource.rc | 52 + cmake/fuck_windows/fuck.cmake | 27 + .../generate_product_version.cmake | 107 ++ cmake/myproto.cmake | 14 + cmake/nkr.cmake | 10 + cmake/print.cmake | 43 + core_commit.txt | 1 + db/ConfigBuilder.cpp | 475 ++++++ db/ConfigBuilder.hpp | 48 + db/Database.cpp | 260 +++ db/Database.hpp | 54 + db/Group.hpp | 24 + db/ProfileFilter.cpp | 77 + db/ProfileFilter.hpp | 36 + db/ProxyEntity.hpp | 71 + db/TrafficData.hpp | 38 + db/TrafficLooper.cpp | 120 ++ db/TrafficLooper.hpp | 32 + examples/.gitignore | 1 + examples/build-alpine.md | 5 + examples/netns-root.sh | 30 + examples/netns.sh | 13 + examples/readme.txt | 9 + examples/set-cap.sh | 14 + examples/sing-box-vpn.json | 43 + examples/vpn-run-root.sh | 71 + fmt/AbstractBean.cpp | 36 + fmt/AbstractBean.hpp | 54 + fmt/Bean2CoreObj.cpp | 175 ++ fmt/Bean2External.cpp | 81 + fmt/Bean2Link.cpp | 94 ++ fmt/ChainBean.hpp | 18 + fmt/CustomBean.hpp | 26 + fmt/InsecureHint.cpp | 67 + fmt/Link2Bean.cpp | 147 ++ fmt/NaiveBean.hpp | 36 + fmt/ShadowSocksBean.hpp | 34 + fmt/SocksHttpBean.hpp | 39 + fmt/TrojanVLESSBean.hpp | 35 + fmt/V2RayStreamSettings.hpp | 53 + fmt/VMessBean.hpp | 35 + fmt/includes.h | 9 + go/.gitignore | 6 + go/auth.go | 54 + go/core_rpc.go | 323 ++++ go/gen/libcore.pb.go | 1181 ++++++++++++++ go/gen/libcore.proto | 96 ++ go/gen/libcore_grpc.pb.go | 395 +++++ go/gen/update_proto.sh | 3 + go/go.mod | 93 ++ go/go.sum | 737 +++++++++ go/grpc.go | 223 +++ go/import_extra.go | 8 + go/iphlpapi/callback_windows.go | 23 + go/iphlpapi/dll_windows.go | 14 + go/iphlpapi/route_windows.go | 87 + go/main.go | 58 + go/protect_bindinterface_windows.go | 139 ++ go/protect_fwmark_linux.go | 105 ++ go/protect_server/protect_server_linux.go | 92 ++ go/protect_server/protect_server_other.go | 9 + go/toolbox_linux.go | 67 + go/toolbox_other.go | 6 + go/toolbox_windows.go | 26 + go/tun_linux.go | 62 + go/tun_stub.go | 19 + libs/.gitignore | 2 + libs/README | 6 + libs/build_deps_all.sh | 54 + libs/deploy_common.sh | 46 + libs/deploy_linux64.sh | 24 + libs/deploy_windows64.sh | 29 + libs/dl.sh | 21 + main/Const.hpp | 28 + main/GuiUtils.hpp | 88 + main/NekoRay.cpp | 320 ++++ main/NekoRay.hpp | 6 + main/NekoRay_ConfigItem.hpp | 63 + main/NekoRay_DataStore.hpp | 122 ++ main/NekoRay_Utils.cpp | 103 ++ main/NekoRay_Utils.hpp | 180 +++ main/main.cpp | 125 ++ matsuri_commit.txt | 1 + nekoray_version.txt | 1 + .../components/proxy/QvProxyConfigurator.cpp | 484 ++++++ .../components/proxy/QvProxyConfigurator.hpp | 13 + qv2ray/ui/LogHighlighter.cpp | 148 ++ qv2ray/ui/LogHighlighter.hpp | 95 ++ qv2ray/ui/QvAutoCompleteTextEdit.cpp | 167 ++ qv2ray/ui/QvAutoCompleteTextEdit.hpp | 83 + qv2ray/ui/widgets/common/QJsonModel.cpp | 402 +++++ qv2ray/ui/widgets/common/QJsonModel.hpp | 94 ++ qv2ray/ui/widgets/editors/w_JsonEditor.cpp | 92 ++ qv2ray/ui/widgets/editors/w_JsonEditor.hpp | 32 + qv2ray/ui/widgets/editors/w_JsonEditor.ui | 172 ++ qv2ray/utils/HTTPRequestHelper.cpp | 100 ++ qv2ray/utils/HTTPRequestHelper.hpp | 37 + qv2ray/wrapper.hpp | 38 + res/icon/dialog-question.svg | 29 + res/icon/internet-web-browser.svg | 1 + res/icon/network-server.svg | 35 + res/icon/preferences.svg | 141 ++ res/icon/system-run.svg | 190 +++ res/icon/system-software-update.svg | 32 + res/neko.qrc | 16 + res/nekoray.css | 3 + res/nekoray.ico | Bin 0 -> 83936 bytes res/nekoray.png | 1 + res/theme/feiyangqingyun/qss.qrc | 76 + res/theme/feiyangqingyun/qss/blacksoft.css | 679 ++++++++ .../qss/blacksoft/add_bottom.png | Bin 0 -> 335 bytes .../feiyangqingyun/qss/blacksoft/add_left.png | Bin 0 -> 377 bytes .../qss/blacksoft/add_right.png | Bin 0 -> 352 bytes .../feiyangqingyun/qss/blacksoft/add_top.png | Bin 0 -> 348 bytes .../qss/blacksoft/arrow_bottom.png | Bin 0 -> 335 bytes .../qss/blacksoft/arrow_left.png | Bin 0 -> 370 bytes .../qss/blacksoft/arrow_right.png | Bin 0 -> 354 bytes .../qss/blacksoft/arrow_top.png | Bin 0 -> 349 bytes .../qss/blacksoft/branch_close.png | Bin 0 -> 257 bytes .../qss/blacksoft/branch_open.png | Bin 0 -> 445 bytes .../qss/blacksoft/calendar_nextmonth.png | Bin 0 -> 623 bytes .../qss/blacksoft/calendar_prevmonth.png | Bin 0 -> 667 bytes .../qss/blacksoft/checkbox_checked.png | Bin 0 -> 593 bytes .../blacksoft/checkbox_checked_disable.png | Bin 0 -> 656 bytes .../qss/blacksoft/checkbox_parcial.png | Bin 0 -> 326 bytes .../blacksoft/checkbox_parcial_disable.png | Bin 0 -> 333 bytes .../qss/blacksoft/checkbox_unchecked.png | Bin 0 -> 572 bytes .../blacksoft/checkbox_unchecked_disable.png | Bin 0 -> 624 bytes .../qss/blacksoft/menu_checked.png | Bin 0 -> 501 bytes .../qss/blacksoft/radiobutton_checked.png | Bin 0 -> 1421 bytes .../blacksoft/radiobutton_checked_disable.png | Bin 0 -> 1614 bytes .../qss/blacksoft/radiobutton_unchecked.png | Bin 0 -> 1229 bytes .../radiobutton_unchecked_disable.png | Bin 0 -> 1365 bytes res/theme/feiyangqingyun/qss/flatgray.css | 679 ++++++++ .../qss/flatgray/add_bottom.png | Bin 0 -> 336 bytes .../feiyangqingyun/qss/flatgray/add_left.png | Bin 0 -> 370 bytes .../feiyangqingyun/qss/flatgray/add_right.png | Bin 0 -> 358 bytes .../feiyangqingyun/qss/flatgray/add_top.png | Bin 0 -> 332 bytes .../qss/flatgray/arrow_bottom.png | Bin 0 -> 337 bytes .../qss/flatgray/arrow_left.png | Bin 0 -> 376 bytes .../qss/flatgray/arrow_right.png | Bin 0 -> 360 bytes .../feiyangqingyun/qss/flatgray/arrow_top.png | Bin 0 -> 361 bytes .../qss/flatgray/branch_close.png | Bin 0 -> 263 bytes .../qss/flatgray/branch_open.png | Bin 0 -> 444 bytes .../qss/flatgray/calendar_nextmonth.png | Bin 0 -> 655 bytes .../qss/flatgray/calendar_prevmonth.png | Bin 0 -> 740 bytes .../qss/flatgray/checkbox_checked.png | Bin 0 -> 616 bytes .../qss/flatgray/checkbox_checked_disable.png | Bin 0 -> 639 bytes .../qss/flatgray/checkbox_parcial.png | Bin 0 -> 341 bytes .../qss/flatgray/checkbox_parcial_disable.png | Bin 0 -> 331 bytes .../qss/flatgray/checkbox_unchecked.png | Bin 0 -> 612 bytes .../flatgray/checkbox_unchecked_disable.png | Bin 0 -> 646 bytes .../qss/flatgray/menu_checked.png | Bin 0 -> 542 bytes .../qss/flatgray/radiobutton_checked.png | Bin 0 -> 1513 bytes .../flatgray/radiobutton_checked_disable.png | Bin 0 -> 1628 bytes .../qss/flatgray/radiobutton_unchecked.png | Bin 0 -> 1294 bytes .../radiobutton_unchecked_disable.png | Bin 0 -> 1374 bytes res/theme/feiyangqingyun/qss/lightblue.css | 679 ++++++++ .../qss/lightblue/add_bottom.png | Bin 0 -> 348 bytes .../feiyangqingyun/qss/lightblue/add_left.png | Bin 0 -> 383 bytes .../qss/lightblue/add_right.png | Bin 0 -> 364 bytes .../feiyangqingyun/qss/lightblue/add_top.png | Bin 0 -> 367 bytes .../qss/lightblue/arrow_bottom.png | Bin 0 -> 348 bytes .../qss/lightblue/arrow_left.png | Bin 0 -> 383 bytes .../qss/lightblue/arrow_right.png | Bin 0 -> 364 bytes .../qss/lightblue/arrow_top.png | Bin 0 -> 361 bytes .../qss/lightblue/branch_close.png | Bin 0 -> 269 bytes .../qss/lightblue/branch_open.png | Bin 0 -> 462 bytes .../qss/lightblue/calendar_nextmonth.png | Bin 0 -> 670 bytes .../qss/lightblue/calendar_prevmonth.png | Bin 0 -> 758 bytes .../qss/lightblue/checkbox_checked.png | Bin 0 -> 636 bytes .../lightblue/checkbox_checked_disable.png | Bin 0 -> 664 bytes .../qss/lightblue/checkbox_parcial.png | Bin 0 -> 351 bytes .../lightblue/checkbox_parcial_disable.png | Bin 0 -> 344 bytes .../qss/lightblue/checkbox_unchecked.png | Bin 0 -> 613 bytes .../lightblue/checkbox_unchecked_disable.png | Bin 0 -> 648 bytes .../qss/lightblue/menu_checked.png | Bin 0 -> 554 bytes .../qss/lightblue/radiobutton_checked.png | Bin 0 -> 1516 bytes .../lightblue/radiobutton_checked_disable.png | Bin 0 -> 1645 bytes .../qss/lightblue/radiobutton_unchecked.png | Bin 0 -> 1298 bytes .../radiobutton_unchecked_disable.png | Bin 0 -> 1385 bytes rpc/gRPC.cpp | 292 ++++ rpc/gRPC.h | 49 + sub/GroupUpdater.cpp | 354 ++++ sub/GroupUpdater.hpp | 33 + sys/AutoRun.cpp | 69 + sys/AutoRun.hpp | 5 + sys/ExternalProcess.cpp | 70 + sys/ExternalProcess.hpp | 37 + sys/windows/MiniDump.cpp | 68 + sys/windows/MiniDump.h | 11 + test/test-qt6-build.sh | 5 + translations/translations.qrc | 5 + translations/zh_CN.ts | 1185 ++++++++++++++ ui/GroupSort.hpp | 21 + ui/ThemeManager.cpp | 84 + ui/ThemeManager.hpp | 14 + ui/dialog_basic_settings.cpp | 221 +++ ui/dialog_basic_settings.h | 33 + ui/dialog_basic_settings.ui | 593 +++++++ ui/dialog_hotkey.cpp | 24 + ui/dialog_hotkey.h | 20 + ui/dialog_hotkey.ui | 107 ++ ui/dialog_manage_groups.cpp | 97 ++ ui/dialog_manage_groups.h | 32 + ui/dialog_manage_groups.ui | 55 + ui/dialog_manage_routes.cpp | 207 +++ ui/dialog_manage_routes.h | 57 + ui/dialog_manage_routes.ui | 392 +++++ ui/edit/dialog_edit_group.cpp | 66 + ui/edit/dialog_edit_group.h | 22 + ui/edit/dialog_edit_group.ui | 180 +++ ui/edit/dialog_edit_profile.cpp | 298 ++++ ui/edit/dialog_edit_profile.h | 52 + ui/edit/dialog_edit_profile.ui | 489 ++++++ ui/edit/edit_chain.cpp | 56 + ui/edit/edit_chain.h | 31 + ui/edit/edit_chain.ui | 57 + ui/edit/edit_custom.cpp | 66 + ui/edit/edit_custom.h | 34 + ui/edit/edit_custom.ui | 86 + ui/edit/edit_naive.cpp | 67 + ui/edit/edit_naive.h | 39 + ui/edit/edit_naive.ui | 126 ++ ui/edit/edit_shadowsocks.cpp | 39 + ui/edit/edit_shadowsocks.h | 28 + ui/edit/edit_shadowsocks.ui | 192 +++ ui/edit/edit_socks_http.cpp | 48 + ui/edit/edit_socks_http.h | 25 + ui/edit/edit_socks_http.ui | 70 + ui/edit/edit_trojan_vless.cpp | 26 + ui/edit/edit_trojan_vless.h | 28 + ui/edit/edit_trojan_vless.ui | 38 + ui/edit/edit_vmess.cpp | 32 + ui/edit/edit_vmess.h | 28 + ui/edit/edit_vmess.ui | 87 + ui/edit/profile_editor.h | 21 + ui/mainwindow.cpp | 1436 +++++++++++++++++ ui/mainwindow.h | 175 ++ ui/mainwindow.ui | 770 +++++++++ ui/mainwindow_grpc.cpp | 384 +++++ ui/widget/GroupItem.cpp | 120 ++ ui/widget/GroupItem.h | 39 + ui/widget/GroupItem.ui | 128 ++ ui/widget/MyLineEdit.h | 15 + ui/widget/MyTableWidget.h | 125 ++ ui/widget/ProxyItem.cpp | 41 + ui/widget/ProxyItem.h | 32 + ui/widget/ProxyItem.ui | 99 ++ updater/.gitignore | 2 + updater/go.mod | 11 + updater/go.sum | 19 + updater/launcher_linux.go | 77 + updater/launcher_windows.go | 10 + updater/main.go | 58 + updater/updater.go | 149 ++ 275 files changed, 25135 insertions(+) create mode 100644 .github/workflows/build-qv2ray-cmake.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 160000 3rdparty/QHotkey create mode 100644 3rdparty/QThreadCreateThread.hpp create mode 100644 3rdparty/QtExtKeySequenceEdit.cpp create mode 100644 3rdparty/QtExtKeySequenceEdit.h create mode 100644 3rdparty/RunGuard.hpp create mode 100644 3rdparty/VT100Parser.hpp create mode 100644 3rdparty/WinCommander.cpp create mode 100644 3rdparty/WinCommander.hpp create mode 100644 3rdparty/ZxingQtReader.hpp create mode 100644 3rdparty/qrcodegen.cpp create mode 100644 3rdparty/qrcodegen.hpp create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 assets/nekoray.png create mode 100644 assets/qtbase_zh_CN.qm create mode 100644 cmake/fuck_windows/VersionInfo.in create mode 100644 cmake/fuck_windows/VersionResource.rc create mode 100644 cmake/fuck_windows/fuck.cmake create mode 100644 cmake/fuck_windows/generate_product_version.cmake create mode 100644 cmake/myproto.cmake create mode 100644 cmake/nkr.cmake create mode 100644 cmake/print.cmake create mode 100644 core_commit.txt create mode 100644 db/ConfigBuilder.cpp create mode 100644 db/ConfigBuilder.hpp create mode 100644 db/Database.cpp create mode 100644 db/Database.hpp create mode 100644 db/Group.hpp create mode 100644 db/ProfileFilter.cpp create mode 100644 db/ProfileFilter.hpp create mode 100644 db/ProxyEntity.hpp create mode 100644 db/TrafficData.hpp create mode 100644 db/TrafficLooper.cpp create mode 100644 db/TrafficLooper.hpp create mode 100644 examples/.gitignore create mode 100644 examples/build-alpine.md create mode 100755 examples/netns-root.sh create mode 100755 examples/netns.sh create mode 100644 examples/readme.txt create mode 100755 examples/set-cap.sh create mode 100644 examples/sing-box-vpn.json create mode 100755 examples/vpn-run-root.sh create mode 100644 fmt/AbstractBean.cpp create mode 100644 fmt/AbstractBean.hpp create mode 100644 fmt/Bean2CoreObj.cpp create mode 100644 fmt/Bean2External.cpp create mode 100644 fmt/Bean2Link.cpp create mode 100644 fmt/ChainBean.hpp create mode 100644 fmt/CustomBean.hpp create mode 100644 fmt/InsecureHint.cpp create mode 100644 fmt/Link2Bean.cpp create mode 100644 fmt/NaiveBean.hpp create mode 100644 fmt/ShadowSocksBean.hpp create mode 100644 fmt/SocksHttpBean.hpp create mode 100644 fmt/TrojanVLESSBean.hpp create mode 100644 fmt/V2RayStreamSettings.hpp create mode 100644 fmt/VMessBean.hpp create mode 100644 fmt/includes.h create mode 100644 go/.gitignore create mode 100644 go/auth.go create mode 100644 go/core_rpc.go create mode 100644 go/gen/libcore.pb.go create mode 100644 go/gen/libcore.proto create mode 100644 go/gen/libcore_grpc.pb.go create mode 100644 go/gen/update_proto.sh create mode 100644 go/go.mod create mode 100644 go/go.sum create mode 100644 go/grpc.go create mode 100644 go/import_extra.go create mode 100644 go/iphlpapi/callback_windows.go create mode 100644 go/iphlpapi/dll_windows.go create mode 100644 go/iphlpapi/route_windows.go create mode 100644 go/main.go create mode 100644 go/protect_bindinterface_windows.go create mode 100644 go/protect_fwmark_linux.go create mode 100644 go/protect_server/protect_server_linux.go create mode 100644 go/protect_server/protect_server_other.go create mode 100644 go/toolbox_linux.go create mode 100644 go/toolbox_other.go create mode 100644 go/toolbox_windows.go create mode 100644 go/tun_linux.go create mode 100644 go/tun_stub.go create mode 100644 libs/.gitignore create mode 100644 libs/README create mode 100755 libs/build_deps_all.sh create mode 100644 libs/deploy_common.sh create mode 100755 libs/deploy_linux64.sh create mode 100755 libs/deploy_windows64.sh create mode 100755 libs/dl.sh create mode 100644 main/Const.hpp create mode 100644 main/GuiUtils.hpp create mode 100644 main/NekoRay.cpp create mode 100644 main/NekoRay.hpp create mode 100644 main/NekoRay_ConfigItem.hpp create mode 100644 main/NekoRay_DataStore.hpp create mode 100644 main/NekoRay_Utils.cpp create mode 100644 main/NekoRay_Utils.hpp create mode 100644 main/main.cpp create mode 100644 matsuri_commit.txt create mode 100644 nekoray_version.txt create mode 100644 qv2ray/components/proxy/QvProxyConfigurator.cpp create mode 100644 qv2ray/components/proxy/QvProxyConfigurator.hpp create mode 100644 qv2ray/ui/LogHighlighter.cpp create mode 100644 qv2ray/ui/LogHighlighter.hpp create mode 100644 qv2ray/ui/QvAutoCompleteTextEdit.cpp create mode 100644 qv2ray/ui/QvAutoCompleteTextEdit.hpp create mode 100644 qv2ray/ui/widgets/common/QJsonModel.cpp create mode 100644 qv2ray/ui/widgets/common/QJsonModel.hpp create mode 100644 qv2ray/ui/widgets/editors/w_JsonEditor.cpp create mode 100644 qv2ray/ui/widgets/editors/w_JsonEditor.hpp create mode 100644 qv2ray/ui/widgets/editors/w_JsonEditor.ui create mode 100644 qv2ray/utils/HTTPRequestHelper.cpp create mode 100644 qv2ray/utils/HTTPRequestHelper.hpp create mode 100644 qv2ray/wrapper.hpp create mode 100644 res/icon/dialog-question.svg create mode 100644 res/icon/internet-web-browser.svg create mode 100644 res/icon/network-server.svg create mode 100644 res/icon/preferences.svg create mode 100644 res/icon/system-run.svg create mode 100644 res/icon/system-software-update.svg create mode 100644 res/neko.qrc create mode 100644 res/nekoray.css create mode 100644 res/nekoray.ico create mode 120000 res/nekoray.png create mode 100644 res/theme/feiyangqingyun/qss.qrc create mode 100644 res/theme/feiyangqingyun/qss/blacksoft.css create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/add_bottom.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/add_left.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/add_right.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/add_top.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/arrow_bottom.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/arrow_left.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/arrow_right.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/arrow_top.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/branch_close.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/branch_open.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/calendar_nextmonth.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/calendar_prevmonth.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/checkbox_checked.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/checkbox_checked_disable.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/checkbox_parcial.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/checkbox_parcial_disable.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/checkbox_unchecked.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/checkbox_unchecked_disable.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/menu_checked.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/radiobutton_checked.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/radiobutton_checked_disable.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/radiobutton_unchecked.png create mode 100644 res/theme/feiyangqingyun/qss/blacksoft/radiobutton_unchecked_disable.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray.css create mode 100644 res/theme/feiyangqingyun/qss/flatgray/add_bottom.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/add_left.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/add_right.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/add_top.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/arrow_bottom.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/arrow_left.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/arrow_right.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/arrow_top.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/branch_close.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/branch_open.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/calendar_nextmonth.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/calendar_prevmonth.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/checkbox_checked.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/checkbox_checked_disable.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/checkbox_parcial.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/checkbox_parcial_disable.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/checkbox_unchecked.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/checkbox_unchecked_disable.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/menu_checked.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/radiobutton_checked.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/radiobutton_checked_disable.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/radiobutton_unchecked.png create mode 100644 res/theme/feiyangqingyun/qss/flatgray/radiobutton_unchecked_disable.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue.css create mode 100644 res/theme/feiyangqingyun/qss/lightblue/add_bottom.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/add_left.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/add_right.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/add_top.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/arrow_bottom.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/arrow_left.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/arrow_right.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/arrow_top.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/branch_close.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/branch_open.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/calendar_nextmonth.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/calendar_prevmonth.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/checkbox_checked.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/checkbox_checked_disable.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/checkbox_parcial.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/checkbox_parcial_disable.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/checkbox_unchecked.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/checkbox_unchecked_disable.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/menu_checked.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/radiobutton_checked.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/radiobutton_checked_disable.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/radiobutton_unchecked.png create mode 100644 res/theme/feiyangqingyun/qss/lightblue/radiobutton_unchecked_disable.png create mode 100644 rpc/gRPC.cpp create mode 100644 rpc/gRPC.h create mode 100644 sub/GroupUpdater.cpp create mode 100644 sub/GroupUpdater.hpp create mode 100644 sys/AutoRun.cpp create mode 100644 sys/AutoRun.hpp create mode 100644 sys/ExternalProcess.cpp create mode 100644 sys/ExternalProcess.hpp create mode 100644 sys/windows/MiniDump.cpp create mode 100644 sys/windows/MiniDump.h create mode 100644 test/test-qt6-build.sh create mode 100644 translations/translations.qrc create mode 100644 translations/zh_CN.ts create mode 100644 ui/GroupSort.hpp create mode 100644 ui/ThemeManager.cpp create mode 100644 ui/ThemeManager.hpp create mode 100644 ui/dialog_basic_settings.cpp create mode 100644 ui/dialog_basic_settings.h create mode 100644 ui/dialog_basic_settings.ui create mode 100644 ui/dialog_hotkey.cpp create mode 100644 ui/dialog_hotkey.h create mode 100644 ui/dialog_hotkey.ui create mode 100644 ui/dialog_manage_groups.cpp create mode 100644 ui/dialog_manage_groups.h create mode 100644 ui/dialog_manage_groups.ui create mode 100644 ui/dialog_manage_routes.cpp create mode 100644 ui/dialog_manage_routes.h create mode 100644 ui/dialog_manage_routes.ui create mode 100644 ui/edit/dialog_edit_group.cpp create mode 100644 ui/edit/dialog_edit_group.h create mode 100644 ui/edit/dialog_edit_group.ui create mode 100644 ui/edit/dialog_edit_profile.cpp create mode 100644 ui/edit/dialog_edit_profile.h create mode 100644 ui/edit/dialog_edit_profile.ui create mode 100644 ui/edit/edit_chain.cpp create mode 100644 ui/edit/edit_chain.h create mode 100644 ui/edit/edit_chain.ui create mode 100644 ui/edit/edit_custom.cpp create mode 100644 ui/edit/edit_custom.h create mode 100644 ui/edit/edit_custom.ui create mode 100644 ui/edit/edit_naive.cpp create mode 100644 ui/edit/edit_naive.h create mode 100644 ui/edit/edit_naive.ui create mode 100644 ui/edit/edit_shadowsocks.cpp create mode 100644 ui/edit/edit_shadowsocks.h create mode 100644 ui/edit/edit_shadowsocks.ui create mode 100644 ui/edit/edit_socks_http.cpp create mode 100644 ui/edit/edit_socks_http.h create mode 100644 ui/edit/edit_socks_http.ui create mode 100644 ui/edit/edit_trojan_vless.cpp create mode 100644 ui/edit/edit_trojan_vless.h create mode 100644 ui/edit/edit_trojan_vless.ui create mode 100644 ui/edit/edit_vmess.cpp create mode 100644 ui/edit/edit_vmess.h create mode 100644 ui/edit/edit_vmess.ui create mode 100644 ui/edit/profile_editor.h create mode 100644 ui/mainwindow.cpp create mode 100644 ui/mainwindow.h create mode 100644 ui/mainwindow.ui create mode 100644 ui/mainwindow_grpc.cpp create mode 100644 ui/widget/GroupItem.cpp create mode 100644 ui/widget/GroupItem.h create mode 100644 ui/widget/GroupItem.ui create mode 100644 ui/widget/MyLineEdit.h create mode 100644 ui/widget/MyTableWidget.h create mode 100644 ui/widget/ProxyItem.cpp create mode 100644 ui/widget/ProxyItem.h create mode 100644 ui/widget/ProxyItem.ui create mode 100644 updater/.gitignore create mode 100644 updater/go.mod create mode 100644 updater/go.sum create mode 100644 updater/launcher_linux.go create mode 100644 updater/launcher_windows.go create mode 100644 updater/main.go create mode 100644 updater/updater.go diff --git a/.github/workflows/build-qv2ray-cmake.yml b/.github/workflows/build-qv2ray-cmake.yml new file mode 100644 index 0000000..cc8f37c --- /dev/null +++ b/.github/workflows/build-qv2ray-cmake.yml @@ -0,0 +1,146 @@ +name: Nekoray build matrix - cmake + +on: + workflow_dispatch: + inputs: + tag: + description: 'Release Tag' + required: true + publish: + description: 'Publish: If want ignore' + required: false +jobs: + build: + strategy: + matrix: + platform: [ windows-2022, ubuntu-18.04 ] + arch: [ x64 ] + build_type: [ Release ] + qt_version: [ 5.15.2 ] + include: + - platform: windows-2022 + arch: x64 + qtarch: win64_msvc2019_64 + fail-fast: false + + runs-on: ${{ matrix.platform }} + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + + steps: + - name: Checking out sources + uses: actions/checkout@v2 + with: + submodules: "recursive" + - name: Install MSVC compiler + if: matrix.platform == 'windows-2022' + uses: ilammy/msvc-dev-cmd@v1 + with: + # 14.1 is for vs2017, 14.2 is vs2019, following the upstream vcpkg build from Qv2ray-deps repo + toolset: 14.2 + arch: ${{ matrix.arch }} + - name: Cache Qt + id: cache-qt + uses: actions/cache@v3 + with: + path: ${{ runner.workspace }}/Qt + key: QtCache-${{ matrix.platform }}-${{ matrix.arch }}-${{ matrix.qt_version }} + - name: Install Qt + uses: jurplel/install-qt-action@v2.14.0 + with: + version: ${{ matrix.qt_version }} + py7zrversion: ' ' + aqtversion: ' ' + setup-python: false + cached: ${{ steps.cache-qt.outputs.cache-hit }} + # ========================================================================================================= Other install + - name: Windows - ${{ matrix.arch }} - ${{ matrix.qt_version }} - Setup Ninja + if: matrix.platform == 'windows-2022' + uses: ashutoshvarma/setup-ninja@master + with: + # ninja version to download. Default: 1.10.0 + version: 1.10.0 + - name: Linux - ${{ matrix.arch }} - ${{ matrix.qt_version }} - Setup Ninja + shell: bash + if: matrix.platform == 'ubuntu-18.04' + run: | + sudo apt-get update + sudo apt-get install -y ninja-build + - name: Install Golang + uses: actions/setup-go@v2 + with: + stable: false + go-version: 1.18.5 + # ========================================================================================================= 编译与 Qt 无关的依赖 + - name: Cache Download + id: cache-deps + uses: actions/cache@v2 + with: + path: libs/deps + key: ${{ hashFiles('libs/build*.sh') }} + - name: Build Dependencies + shell: bash + if: steps.cache-deps.outputs.cache-hit != 'true' + run: | + ./libs/build_deps_all.sh + # ========================================================================================================= Generate MakeFile and Build + - name: Windows - ${{ matrix.qt_version }} - Generate MakeFile and Build + shell: bash + if: matrix.platform == 'windows-2022' + env: + CC: cl.exe + CXX: cl.exe + run: | + mkdir build + cd build + cmake .. -GNinja \ + -DCMAKE_PREFIX_PATH=./libs/deps/built \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + cmake --build . --parallel $(nproc) + cd .. + ./libs/deploy_windows64.sh + - name: Linux - ${{ matrix.qt_version }} - Generate MakeFile and Build + shell: bash + if: matrix.platform == 'ubuntu-18.04' + run: | + mkdir build + cd build + cmake .. -GNinja \ + -DCMAKE_PREFIX_PATH=./libs/deps/built \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + cmake --build . --parallel $(nproc) + cd .. + ./libs/deploy_linux64.sh + # ========================================================================================================= Deployments + - name: Uploading Artifact + uses: actions/upload-artifact@master + with: + name: NekoRay-${{ github.sha }}-${{ matrix.platform }}-${{ matrix.arch }} + path: ./deployment/ + publish: + name: Publish Release + if: github.event.inputs.publish != 'y' + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Donwload Artifacts + uses: actions/download-artifact@v2 + with: + name: NekoRay-${{ github.sha }}-ubuntu-18.04-x64 + path: artifacts-linux + - name: Donwload Artifacts + uses: actions/download-artifact@v2 + with: + name: NekoRay-${{ github.sha }}-windows-2022-x64 + path: artifacts-windows + - name: Release + run: | + wget -O ghr.tar.gz https://github.com/tcnksm/ghr/releases/download/v0.13.0/ghr_v0.13.0_linux_amd64.tar.gz + tar -xvf ghr.tar.gz + mv ghr*linux_amd64/ghr . + mkdir apks + find artifacts-linux -name "*.tar.gz" -exec cp {} apks \; + find artifacts-windows -name "*.zip" -exec cp {} apks \; + ./ghr -delete -t "${{ github.token }}" -n "${{ github.event.inputs.tag }}" "${{ github.event.inputs.tag }}" apks diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a148a15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,82 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + +# Custom +/nekoray/ +/build/ +CMakeLists.txt.user* +/cmake-build-* +/build-* +.vscode +.idea + +# Deploy +/deployment diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..72a74b8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "3rdparty/QHotkey"] + path = 3rdparty/QHotkey + url = https://github.com/Skycoder42/QHotkey.git diff --git a/3rdparty/QHotkey b/3rdparty/QHotkey new file mode 160000 index 0000000..52e25ac --- /dev/null +++ b/3rdparty/QHotkey @@ -0,0 +1 @@ +Subproject commit 52e25acf221e5ac86ce648f6922620fb2d6a7121 diff --git a/3rdparty/QThreadCreateThread.hpp b/3rdparty/QThreadCreateThread.hpp new file mode 100644 index 0000000..2822ba8 --- /dev/null +++ b/3rdparty/QThreadCreateThread.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +// FOR OLD QT + +class QThreadCreateThread : public QThread { +public: + explicit QThreadCreateThread(std::future &&future) + : m_future(std::move(future)) { + // deleteLater + connect(this, &QThread::finished, this, &QThread::deleteLater); + } + +private: + void run() override { + m_future.get(); + } + + std::future m_future; +}; + +inline QThread *createThreadImpl(std::future &&future) { + return new QThreadCreateThread(std::move(future)); +} + +template +QThread *createQThread(Function &&f, Args &&... args) { + using DecayedFunction = typename std::decay::type; + auto threadFunction = + [f = static_cast(std::forward(f))](auto &&... largs) mutable -> void { + (void) std::invoke(std::move(f), std::forward(largs)...); + }; + + return createThreadImpl(std::async(std::launch::deferred, + std::move(threadFunction), + std::forward(args)...)); +} diff --git a/3rdparty/QtExtKeySequenceEdit.cpp b/3rdparty/QtExtKeySequenceEdit.cpp new file mode 100644 index 0000000..02bb80c --- /dev/null +++ b/3rdparty/QtExtKeySequenceEdit.cpp @@ -0,0 +1,22 @@ +#include "QtExtKeySequenceEdit.h" + +QtExtKeySequenceEdit::QtExtKeySequenceEdit(QWidget *parent) + : QKeySequenceEdit(parent) { +} + +QtExtKeySequenceEdit::~QtExtKeySequenceEdit() { +} + +void QtExtKeySequenceEdit::keyPressEvent(QKeyEvent *pEvent) { + QKeySequenceEdit::keyPressEvent(pEvent); + + QKeySequence keySeq = keySequence(); + if (keySeq.count() <= 0) { + return; + } + int key = keySeq[0]; + if (key == Qt::Key_Backspace || key == Qt::Key_Delete) { + key = 0; + } + setKeySequence(key); +} diff --git a/3rdparty/QtExtKeySequenceEdit.h b/3rdparty/QtExtKeySequenceEdit.h new file mode 100644 index 0000000..ebeb848 --- /dev/null +++ b/3rdparty/QtExtKeySequenceEdit.h @@ -0,0 +1,11 @@ +#include + +class QtExtKeySequenceEdit : public QKeySequenceEdit { +public: + QtExtKeySequenceEdit(QWidget *parent); + + ~QtExtKeySequenceEdit(); + +protected: + virtual void keyPressEvent(QKeyEvent *pEvent); +}; diff --git a/3rdparty/RunGuard.hpp b/3rdparty/RunGuard.hpp new file mode 100644 index 0000000..3702610 --- /dev/null +++ b/3rdparty/RunGuard.hpp @@ -0,0 +1,98 @@ +#ifndef RUNGUARD_H +#define RUNGUARD_H + +#include +#include +#include +#include + +class RunGuard { + +public: + RunGuard(const QString &key); + + ~RunGuard(); + + bool isAnotherRunning(); + + bool tryToRun(); + + void release(); + +private: + const QString key; + const QString memLockKey; + const QString sharedmemKey; + + QSharedMemory sharedMem; + QSystemSemaphore memLock; + + Q_DISABLE_COPY(RunGuard) +}; + +namespace { + + QString generateKeyHash(const QString &key, const QString &salt) { + QByteArray data; + + data.append(key.toUtf8()); + data.append(salt.toUtf8()); + data = QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex(); + + return data; + } + +} + + +RunGuard::RunGuard(const QString &key) + : key(key), memLockKey(generateKeyHash(key, "_memLockKey")), + sharedmemKey(generateKeyHash(key, "_sharedmemKey")), sharedMem(sharedmemKey), memLock(memLockKey, 1) { + memLock.acquire(); + { + QSharedMemory fix(sharedmemKey); // Fix for *nix: http://habrahabr.ru/post/173281/ + fix.attach(); + } + memLock.release(); +} + +RunGuard::~RunGuard() { + release(); +} + +bool RunGuard::isAnotherRunning() { + if (sharedMem.isAttached()) + return false; + + memLock.acquire(); + const bool isRunning = sharedMem.attach(); + if (isRunning) + sharedMem.detach(); + memLock.release(); + + return isRunning; +} + +bool RunGuard::tryToRun() { + if (isAnotherRunning()) // Extra check + return false; + + memLock.acquire(); + const bool result = sharedMem.create(sizeof(quint64)); + memLock.release(); + if (!result) { + release(); + return false; + } + + return true; +} + +void RunGuard::release() { + memLock.acquire(); + if (sharedMem.isAttached()) + sharedMem.detach(); + memLock.release(); +} + +#endif // RUNGUARD_H diff --git a/3rdparty/VT100Parser.hpp b/3rdparty/VT100Parser.hpp new file mode 100644 index 0000000..9887a3f --- /dev/null +++ b/3rdparty/VT100Parser.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +inline QString cleanVT100String(const QString &in) { + QString out; + bool in_033 = false; + for (auto &&chr: in) { + if (chr == '\033') { + in_033 = true; + continue; + } + if (in_033) { + if (chr == 'm') { + in_033 = false; + } + continue; + } + out += chr; + } + return out; +} diff --git a/3rdparty/WinCommander.cpp b/3rdparty/WinCommander.cpp new file mode 100644 index 0000000..7d2245b --- /dev/null +++ b/3rdparty/WinCommander.cpp @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2014 UpdateNode UG (haftungsbeschränkt) +** Contact: code@updatenode.com +** +** This file is part of the UpdateNode Client. +** +** Commercial License Usage +** Licensees holding valid commercial UpdateNode license may use this file +** under the terms of the the Apache License, Version 2.0 +** Full license description file: LICENSE.COM +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation. Please review the following information to ensure the +** GNU General Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** Full license description file: LICENSE.GPL +** +****************************************************************************/ + +#include "WinCommander.hpp" + +#include +#include + +#ifdef Q_OS_WIN +#include +#include +#include +#define MAX_KEY_LENGTH 255 +#define MAX_VALUE_NAME 16383 +#endif + + +/*! +Executes a command elevated specified by \apath , using paramters \aparameters. +\n +Parameter /aaWait decides if the function should return immediatelly after it's\n +execution or wait for the exit of the launched process +\n +Returns the return value of the executed command +*/ +uint WinCommander::runProcessElevated(const QString &path, + const QStringList ¶meters, + const QString &workingDir, bool aWait) { + uint result = 0; + +#ifdef Q_OS_WIN + QString params; + HWND hwnd = NULL; + LPCTSTR pszPath = (LPCTSTR)path.utf16(); + foreach(QString item, parameters) + params += "\"" + item + "\" "; + + LPCTSTR pszParameters = (LPCTSTR)params.utf16(); + QString dir; + if (workingDir.isEmpty()) + dir = QDir::toNativeSeparators(QDir::currentPath()); + else + dir = QDir::toNativeSeparators(workingDir); + LPCTSTR pszDirectory = (LPCTSTR)dir.utf16(); + + SHELLEXECUTEINFO shex; + DWORD dwCode = 0; + + ZeroMemory(&shex, sizeof(shex)); + + shex.cbSize = sizeof(shex); + shex.fMask = SEE_MASK_NOCLOSEPROCESS; + shex.hwnd = hwnd; + shex.lpVerb = TEXT("runas"); + shex.lpFile = pszPath; + shex.lpParameters = pszParameters; + shex.lpDirectory = pszDirectory; + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow + shex.nShow = SW_SHOWMINIMIZED; + + ShellExecuteEx(&shex); + if (shex.hProcess) + { + if(aWait) + { + WaitForSingleObject(shex.hProcess, INFINITE ); + GetExitCodeProcess(shex.hProcess, &dwCode); + } + CloseHandle (shex.hProcess) ; + } + else + return -1; + + result = (uint)dwCode; +#else + Q_UNUSED(path); + Q_UNUSED(parameters); + Q_UNUSED(workingDir); + Q_UNUSED(aWait); +#endif + return result; +} + +/*! +Executes a command elevated specified by \apath , using paramters \aparameters. +\n +Parameter /aaWait decides if the function should return immediatelly after it's\n +execution or wait for the exit of the launched process +\n +Returns the return value of the executed command +*/ +uint WinCommander::runProcessElevated(const QString &path, const QString ¶meters, const QString &workingDir, + bool aWait) { + return runProcessElevated(path, QStringList() << parameters, workingDir, aWait); +} diff --git a/3rdparty/WinCommander.hpp b/3rdparty/WinCommander.hpp new file mode 100644 index 0000000..0b6c728 --- /dev/null +++ b/3rdparty/WinCommander.hpp @@ -0,0 +1,42 @@ +/**************************************************************************** +** +** Copyright (C) 2014 UpdateNode UG (haftungsbeschränkt) +** Contact: code@updatenode.com +** +** This file is part of the UpdateNode Client. +** +** Commercial License Usage +** Licensees holding valid commercial UpdateNode license may use this file +** under the terms of the the Apache License, Version 2.0 +** Full license description file: LICENSE.COM +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation. Please review the following information to ensure the +** GNU General Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** Full license description file: LICENSE.GPL +** +****************************************************************************/ + +#ifndef WINCOMMANDER_H +#define WINCOMMANDER_H + +#include +#include + +class WinCommander { +public: + static uint runProcessElevated(const QString &path, + const QStringList ¶meters = QStringList(), + const QString &workingDir = QString(), + bool aWait = true); + + static uint runProcessElevated(const QString &path, + const QString ¶meters = QString(), + const QString &workingDir = QString(), + bool aWait = true); +}; + +#endif // WINCOMMANDER_H \ No newline at end of file diff --git a/3rdparty/ZxingQtReader.hpp b/3rdparty/ZxingQtReader.hpp new file mode 100644 index 0000000..423c756 --- /dev/null +++ b/3rdparty/ZxingQtReader.hpp @@ -0,0 +1,386 @@ +#pragma once +/* + * Copyright 2020 Axel Waggershauser + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ZXing/ReadBarcode.h" + +#include +#include +#include + +#ifdef QT_MULTIMEDIA_LIB +#include +#include +#endif + +// This is some sample code to start a discussion about how a minimal and header-only Qt wrapper/helper could look like. + +namespace ZXingQt { + + Q_NAMESPACE + +//TODO: find a better way to export these enums to QML than to duplicate their definition +// #ifdef Q_MOC_RUN produces meta information in the moc output but it does end up working in qml +#ifdef QT_QML_LIB + enum class BarcodeFormat +{ + None = 0, ///< Used as a return value if no valid barcode has been detected + Aztec = (1 << 0), ///< Aztec (2D) + Codabar = (1 << 1), ///< Codabar (1D) + Code39 = (1 << 2), ///< Code39 (1D) + Code93 = (1 << 3), ///< Code93 (1D) + Code128 = (1 << 4), ///< Code128 (1D) + DataBar = (1 << 5), ///< GS1 DataBar, formerly known as RSS 14 + DataBarExpanded = (1 << 6), ///< GS1 DataBar Expanded, formerly known as RSS EXPANDED + DataMatrix = (1 << 7), ///< DataMatrix (2D) + EAN8 = (1 << 8), ///< EAN-8 (1D) + EAN13 = (1 << 9), ///< EAN-13 (1D) + ITF = (1 << 10), ///< ITF (Interleaved Two of Five) (1D) + MaxiCode = (1 << 11), ///< MaxiCode (2D) + PDF417 = (1 << 12), ///< PDF417 (1D) or (2D) + QRCode = (1 << 13), ///< QR Code (2D) + UPCA = (1 << 14), ///< UPC-A (1D) + UPCE = (1 << 15), ///< UPC-E (1D) + MicroQRCode = (1 << 16), ///< Micro QR Code (2D) + + OneDCodes = Codabar | Code39 | Code93 | Code128 | EAN8 | EAN13 | ITF | DataBar | DataBarExpanded | UPCA | UPCE, + TwoDCodes = Aztec | DataMatrix | MaxiCode | PDF417 | QRCode | MicroQRCode, +}; + +enum class DecodeStatus +{ + NoError = 0, + NotFound, + FormatError, + ChecksumError, +}; +#else + using ZXing::BarcodeFormat; + using ZXing::DecodeStatus; +#endif + + using ZXing::DecodeHints; + using ZXing::Binarizer; + using ZXing::BarcodeFormats; + + Q_ENUM_NS(BarcodeFormat) + Q_ENUM_NS(DecodeStatus) + + template + QDebug operator<<(QDebug dbg, const T& v) + { + return dbg.noquote() << QString::fromStdString(ToString(v)); + } + + class Position : public ZXing::Quadrilateral + { + Q_GADGET + + Q_PROPERTY(QPoint topLeft READ topLeft) + Q_PROPERTY(QPoint topRight READ topRight) + Q_PROPERTY(QPoint bottomRight READ bottomRight) + Q_PROPERTY(QPoint bottomLeft READ bottomLeft) + + using Base = ZXing::Quadrilateral; + + public: + using Base::Base; + }; + + class Result : private ZXing::Result + { + Q_GADGET + + Q_PROPERTY(BarcodeFormat format READ format) + Q_PROPERTY(QString formatName READ formatName) + Q_PROPERTY(QString text READ text) + Q_PROPERTY(QByteArray rawBytes READ rawBytes) + Q_PROPERTY(bool isValid READ isValid) + Q_PROPERTY(DecodeStatus status READ status) + Q_PROPERTY(Position position READ position) + + QString _text; + QByteArray _rawBytes; + Position _position; + + public: + Result() : ZXing::Result(ZXing::DecodeStatus::NotFound) {} // required for qmetatype machinery + + explicit Result(ZXing::Result&& r) : ZXing::Result(std::move(r)) { + _text = QString::fromWCharArray(ZXing::Result::text().c_str()); + _rawBytes = QByteArray(reinterpret_cast(ZXing::Result::rawBytes().data()), + Size(ZXing::Result::rawBytes())); + auto& pos = ZXing::Result::position(); + auto qp = [&pos](int i) { return QPoint(pos[i].x, pos[i].y); }; + _position = {qp(0), qp(1), qp(2), qp(3)}; + } + + using ZXing::Result::isValid; + + BarcodeFormat format() const { return static_cast(ZXing::Result::format()); } + DecodeStatus status() const { return static_cast(ZXing::Result::status()); } + QString formatName() const { return QString::fromStdString(ZXing::ToString(ZXing::Result::format())); } + const QString& text() const { return _text; } + const QByteArray& rawBytes() const { return _rawBytes; } + const Position& position() const { return _position; } + + // For debugging/development + int runTime = 0; + Q_PROPERTY(int runTime MEMBER runTime) + }; + + inline Result ReadBarcode(const QImage& img, const DecodeHints& hints = {}) + { + using namespace ZXing; + + auto ImgFmtFromQImg = [](const QImage& img) { + switch (img.format()) { + case QImage::Format_ARGB32: + case QImage::Format_RGB32: +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + return ImageFormat::BGRX; +#else + return ImageFormat::XRGB; +#endif + case QImage::Format_RGB888: return ImageFormat::RGB; + case QImage::Format_RGBX8888: + case QImage::Format_RGBA8888: return ImageFormat::RGBX; + case QImage::Format_Grayscale8: return ImageFormat::Lum; + default: return ImageFormat::None; + } + }; + + auto exec = [&](const QImage& img) { + return Result(ZXing::ReadBarcode( + {img.bits(), img.width(), img.height(), ImgFmtFromQImg(img), static_cast(img.bytesPerLine())}, hints)); + }; + + return ImgFmtFromQImg(img) == ImageFormat::None ? exec(img.convertToFormat(QImage::Format_Grayscale8)) : exec(img); + } + +#ifdef QT_MULTIMEDIA_LIB + inline Result ReadBarcode(const QVideoFrame& frame, const DecodeHints& hints = {}) +{ + using namespace ZXing; + + auto img = frame; // shallow copy just get access to non-const map() function + if (!frame.isValid() || !img.map(QAbstractVideoBuffer::ReadOnly)){ + qWarning() << "invalid QVideoFrame: could not map memory"; + return {}; + } + //TODO c++17: SCOPE_EXIT([&] { img.unmap(); }); + + ImageFormat fmt = ImageFormat::None; + int pixStride = 0; + int pixOffset = 0; + + switch (img.pixelFormat()) { + case QVideoFrame::Format_ARGB32: + case QVideoFrame::Format_ARGB32_Premultiplied: + case QVideoFrame::Format_RGB32: +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + fmt = ImageFormat::BGRX; +#else + fmt = ImageFormat::XRGB; +#endif + break; + + case QVideoFrame::Format_RGB24: fmt = ImageFormat::RGB; break; + + case QVideoFrame::Format_BGRA32: + case QVideoFrame::Format_BGRA32_Premultiplied: + case QVideoFrame::Format_BGR32: +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + fmt = ImageFormat::RGBX; +#else + fmt = ImageFormat::XBGR; +#endif + break; + + case QVideoFrame::Format_BGR24: fmt = ImageFormat::BGR; break; + + case QVideoFrame::Format_AYUV444: + case QVideoFrame::Format_AYUV444_Premultiplied: +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + fmt = ImageFormat::Lum, pixStride = 4, pixOffset = 3; +#else + fmt = ImageFormat::Lum, pixStride = 4, pixOffset = 2; +#endif + break; + + case QVideoFrame::Format_YUV444: fmt = ImageFormat::Lum, pixStride = 3; break; + case QVideoFrame::Format_YUV420P: + case QVideoFrame::Format_NV12: + case QVideoFrame::Format_NV21: + case QVideoFrame::Format_IMC1: + case QVideoFrame::Format_IMC2: + case QVideoFrame::Format_IMC3: + case QVideoFrame::Format_IMC4: + case QVideoFrame::Format_YV12: fmt = ImageFormat::Lum; break; + case QVideoFrame::Format_UYVY: fmt = ImageFormat::Lum, pixStride = 2, pixOffset = 1; break; + case QVideoFrame::Format_YUYV: fmt = ImageFormat::Lum, pixStride = 2; break; + + case QVideoFrame::Format_Y8: fmt = ImageFormat::Lum; break; + case QVideoFrame::Format_Y16: fmt = ImageFormat::Lum, pixStride = 2, pixOffset = 1; break; + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) + case QVideoFrame::Format_ABGR32: +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + fmt = ImageFormat::RGBX; +#else + fmt = ImageFormat::XBGR; +#endif + break; +#endif +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + case QVideoFrame::Format_YUV422P: fmt = ImageFormat::Lum; break; +#endif + default: break; + } + + Result res; + if (fmt != ImageFormat::None) { + res = Result( + ZXing::ReadBarcode({img.bits() + pixOffset, img.width(), img.height(), fmt, img.bytesPerLine(), pixStride}, + hints)); + } else { + if (QVideoFrame::imageFormatFromPixelFormat(img.pixelFormat()) != QImage::Format_Invalid) + res = ReadBarcode(img.image(), hints); + } + + img.unmap(); + + return res; +} + +#define ZQ_PROPERTY(Type, name, setter) \ +public: \ + Q_PROPERTY(Type name READ name WRITE setter NOTIFY name##Changed) \ + Type name() const noexcept { return DecodeHints::name(); } \ + Q_SLOT void setter(const Type& newVal) \ + { \ + if (name() != newVal) { \ + DecodeHints::setter(newVal); \ + emit name##Changed(); \ + } \ + } \ + Q_SIGNAL void name##Changed(); + +class VideoFilter : public QAbstractVideoFilter, private DecodeHints +{ + Q_OBJECT + +public: + VideoFilter(QObject* parent = nullptr) : QAbstractVideoFilter(parent) {} + + QVideoFilterRunnable* createFilterRunnable() override; + + // TODO: find out how to properly expose QFlags to QML + // simply using ZQ_PROPERTY(BarcodeFormats, formats, setFormats) + // results in the runtime error "can't assign int to formats" + Q_PROPERTY(int formats READ formats WRITE setFormats NOTIFY formatsChanged) + int formats() const noexcept + { + auto fmts = DecodeHints::formats(); + return *reinterpret_cast(&fmts); + } + Q_SLOT void setFormats(int newVal) + { + if (formats() != newVal) { + DecodeHints::setFormats(static_cast(newVal)); + emit formatsChanged(); + qDebug() << DecodeHints::formats(); + } + } + Q_SIGNAL void formatsChanged(); + + ZQ_PROPERTY(bool, tryRotate, setTryRotate) + ZQ_PROPERTY(bool, tryHarder, setTryHarder) + +public slots: + Result process(const QVideoFrame& image) + { + QElapsedTimer t; + t.start(); + + auto res = ReadBarcode(image, *this); + + res.runTime = t.elapsed(); + + emit newResult(res); + if (res.isValid()) + emit foundBarcode(res); + return res; + } + +signals: + void newResult(Result result); + void foundBarcode(Result result); +}; + +#undef ZX_PROPERTY + +class VideoFilterRunnable : public QVideoFilterRunnable +{ + VideoFilter* _filter = nullptr; + +public: + explicit VideoFilterRunnable(VideoFilter* filter) : _filter(filter) {} + + QVideoFrame run(QVideoFrame* input, const QVideoSurfaceFormat& /*surfaceFormat*/, RunFlags /*flags*/) override + { + _filter->process(*input); + return *input; + } +}; + +inline QVideoFilterRunnable* VideoFilter::createFilterRunnable() +{ + return new VideoFilterRunnable(this); +} + +#endif // QT_MULTIMEDIA_LIB + +} // namespace ZXingQt + + +Q_DECLARE_METATYPE(ZXingQt::Position) +Q_DECLARE_METATYPE(ZXingQt::Result) + +#ifdef QT_QML_LIB + +#include + +namespace ZXingQt { + +inline void registerQmlAndMetaTypes() +{ + qRegisterMetaType("BarcodeFormat"); + qRegisterMetaType("DecodeStatus"); + + // supposedly the Q_DECLARE_METATYPE should be used with the overload without a custom name + // but then the qml side complains about "unregistered type" + qRegisterMetaType("Position"); + qRegisterMetaType("Result"); + + qmlRegisterUncreatableMetaObject( + ZXingQt::staticMetaObject, "ZXing", 1, 0, "ZXing", "Access to enums & flags only"); + qmlRegisterType("ZXing", 1, 0, "VideoFilter"); +} + +} // namespace ZXingQt + +#endif // QT_QML_LIB \ No newline at end of file diff --git a/3rdparty/qrcodegen.cpp b/3rdparty/qrcodegen.cpp new file mode 100644 index 0000000..0957b79 --- /dev/null +++ b/3rdparty/qrcodegen.cpp @@ -0,0 +1,830 @@ +/* + * QR Code generator library (C++) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "qrcodegen.hpp" + +using std::int8_t; +using std::uint8_t; +using std::size_t; +using std::vector; + + +namespace qrcodegen { + +/*---- Class QrSegment ----*/ + +QrSegment::Mode::Mode(int mode, int cc0, int cc1, int cc2) : + modeBits(mode) { + numBitsCharCount[0] = cc0; + numBitsCharCount[1] = cc1; + numBitsCharCount[2] = cc2; +} + + +int QrSegment::Mode::getModeBits() const { + return modeBits; +} + + +int QrSegment::Mode::numCharCountBits(int ver) const { + return numBitsCharCount[(ver + 7) / 17]; +} + + +const QrSegment::Mode QrSegment::Mode::NUMERIC (0x1, 10, 12, 14); +const QrSegment::Mode QrSegment::Mode::ALPHANUMERIC(0x2, 9, 11, 13); +const QrSegment::Mode QrSegment::Mode::BYTE (0x4, 8, 16, 16); +const QrSegment::Mode QrSegment::Mode::KANJI (0x8, 8, 10, 12); +const QrSegment::Mode QrSegment::Mode::ECI (0x7, 0, 0, 0); + + +QrSegment QrSegment::makeBytes(const vector &data) { + if (data.size() > static_cast(INT_MAX)) + throw std::length_error("Data too long"); + BitBuffer bb; + for (uint8_t b : data) + bb.appendBits(b, 8); + return QrSegment(Mode::BYTE, static_cast(data.size()), std::move(bb)); +} + + +QrSegment QrSegment::makeNumeric(const char *digits) { + BitBuffer bb; + int accumData = 0; + int accumCount = 0; + int charCount = 0; + for (; *digits != '\0'; digits++, charCount++) { + char c = *digits; + if (c < '0' || c > '9') + throw std::domain_error("String contains non-numeric characters"); + accumData = accumData * 10 + (c - '0'); + accumCount++; + if (accumCount == 3) { + bb.appendBits(static_cast(accumData), 10); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 or 2 digits remaining + bb.appendBits(static_cast(accumData), accumCount * 3 + 1); + return QrSegment(Mode::NUMERIC, charCount, std::move(bb)); +} + + +QrSegment QrSegment::makeAlphanumeric(const char *text) { + BitBuffer bb; + int accumData = 0; + int accumCount = 0; + int charCount = 0; + for (; *text != '\0'; text++, charCount++) { + const char *temp = std::strchr(ALPHANUMERIC_CHARSET, *text); + if (temp == nullptr) + throw std::domain_error("String contains unencodable characters in alphanumeric mode"); + accumData = accumData * 45 + static_cast(temp - ALPHANUMERIC_CHARSET); + accumCount++; + if (accumCount == 2) { + bb.appendBits(static_cast(accumData), 11); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 character remaining + bb.appendBits(static_cast(accumData), 6); + return QrSegment(Mode::ALPHANUMERIC, charCount, std::move(bb)); +} + + +vector QrSegment::makeSegments(const char *text) { + // Select the most efficient segment encoding automatically + vector result; + if (*text == '\0'); // Leave result empty + else if (isNumeric(text)) + result.push_back(makeNumeric(text)); + else if (isAlphanumeric(text)) + result.push_back(makeAlphanumeric(text)); + else { + vector bytes; + for (; *text != '\0'; text++) + bytes.push_back(static_cast(*text)); + result.push_back(makeBytes(bytes)); + } + return result; +} + + +QrSegment QrSegment::makeEci(long assignVal) { + BitBuffer bb; + if (assignVal < 0) + throw std::domain_error("ECI assignment value out of range"); + else if (assignVal < (1 << 7)) + bb.appendBits(static_cast(assignVal), 8); + else if (assignVal < (1 << 14)) { + bb.appendBits(2, 2); + bb.appendBits(static_cast(assignVal), 14); + } else if (assignVal < 1000000L) { + bb.appendBits(6, 3); + bb.appendBits(static_cast(assignVal), 21); + } else + throw std::domain_error("ECI assignment value out of range"); + return QrSegment(Mode::ECI, 0, std::move(bb)); +} + + +QrSegment::QrSegment(const Mode &md, int numCh, const std::vector &dt) : + mode(&md), + numChars(numCh), + data(dt) { + if (numCh < 0) + throw std::domain_error("Invalid value"); +} + + +QrSegment::QrSegment(const Mode &md, int numCh, std::vector &&dt) : + mode(&md), + numChars(numCh), + data(std::move(dt)) { + if (numCh < 0) + throw std::domain_error("Invalid value"); +} + + +int QrSegment::getTotalBits(const vector &segs, int version) { + int result = 0; + for (const QrSegment &seg : segs) { + int ccbits = seg.mode->numCharCountBits(version); + if (seg.numChars >= (1L << ccbits)) + return -1; // The segment's length doesn't fit the field's bit width + if (4 + ccbits > INT_MAX - result) + return -1; // The sum will overflow an int type + result += 4 + ccbits; + if (seg.data.size() > static_cast(INT_MAX - result)) + return -1; // The sum will overflow an int type + result += static_cast(seg.data.size()); + } + return result; +} + + +bool QrSegment::isNumeric(const char *text) { + for (; *text != '\0'; text++) { + char c = *text; + if (c < '0' || c > '9') + return false; + } + return true; +} + + +bool QrSegment::isAlphanumeric(const char *text) { + for (; *text != '\0'; text++) { + if (std::strchr(ALPHANUMERIC_CHARSET, *text) == nullptr) + return false; + } + return true; +} + + +const QrSegment::Mode &QrSegment::getMode() const { + return *mode; +} + + +int QrSegment::getNumChars() const { + return numChars; +} + + +const std::vector &QrSegment::getData() const { + return data; +} + + +const char *QrSegment::ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; + + + +/*---- Class QrCode ----*/ + +int QrCode::getFormatBits(Ecc ecl) { + switch (ecl) { + case Ecc::LOW : return 1; + case Ecc::MEDIUM : return 0; + case Ecc::QUARTILE: return 3; + case Ecc::HIGH : return 2; + default: throw std::logic_error("Unreachable"); + } +} + + +QrCode QrCode::encodeText(const char *text, Ecc ecl) { + vector segs = QrSegment::makeSegments(text); + return encodeSegments(segs, ecl); +} + + +QrCode QrCode::encodeBinary(const vector &data, Ecc ecl) { + vector segs{QrSegment::makeBytes(data)}; + return encodeSegments(segs, ecl); +} + + +QrCode QrCode::encodeSegments(const vector &segs, Ecc ecl, + int minVersion, int maxVersion, int mask, bool boostEcl) { + if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7) + throw std::invalid_argument("Invalid value"); + + // Find the minimal version number to use + int version, dataUsedBits; + for (version = minVersion; ; version++) { + int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available + dataUsedBits = QrSegment::getTotalBits(segs, version); + if (dataUsedBits != -1 && dataUsedBits <= dataCapacityBits) + break; // This version number is found to be suitable + if (version >= maxVersion) { // All versions in the range could not fit the given data + std::ostringstream sb; + if (dataUsedBits == -1) + sb << "Segment too long"; + else { + sb << "Data length = " << dataUsedBits << " bits, "; + sb << "Max capacity = " << dataCapacityBits << " bits"; + } + throw data_too_long(sb.str()); + } + } + assert(dataUsedBits != -1); + + // Increase the error correction level while the data still fits in the current version number + for (Ecc newEcl : {Ecc::MEDIUM, Ecc::QUARTILE, Ecc::HIGH}) { // From low to high + if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8) + ecl = newEcl; + } + + // Concatenate all segments to create the data bit string + BitBuffer bb; + for (const QrSegment &seg : segs) { + bb.appendBits(static_cast(seg.getMode().getModeBits()), 4); + bb.appendBits(static_cast(seg.getNumChars()), seg.getMode().numCharCountBits(version)); + bb.insert(bb.end(), seg.getData().begin(), seg.getData().end()); + } + assert(bb.size() == static_cast(dataUsedBits)); + + // Add terminator and pad up to a byte if applicable + size_t dataCapacityBits = static_cast(getNumDataCodewords(version, ecl)) * 8; + assert(bb.size() <= dataCapacityBits); + bb.appendBits(0, std::min(4, static_cast(dataCapacityBits - bb.size()))); + bb.appendBits(0, (8 - static_cast(bb.size() % 8)) % 8); + assert(bb.size() % 8 == 0); + + // Pad with alternating bytes until data capacity is reached + for (uint8_t padByte = 0xEC; bb.size() < dataCapacityBits; padByte ^= 0xEC ^ 0x11) + bb.appendBits(padByte, 8); + + // Pack bits into bytes in big endian + vector dataCodewords(bb.size() / 8); + for (size_t i = 0; i < bb.size(); i++) + dataCodewords.at(i >> 3) |= (bb.at(i) ? 1 : 0) << (7 - (i & 7)); + + // Create the QR Code object + return QrCode(version, ecl, dataCodewords, mask); +} + + +QrCode::QrCode(int ver, Ecc ecl, const vector &dataCodewords, int msk) : + // Initialize fields and check arguments + version(ver), + errorCorrectionLevel(ecl) { + if (ver < MIN_VERSION || ver > MAX_VERSION) + throw std::domain_error("Version value out of range"); + if (msk < -1 || msk > 7) + throw std::domain_error("Mask value out of range"); + size = ver * 4 + 17; + size_t sz = static_cast(size); + modules = vector >(sz, vector(sz)); // Initially all light + isFunction = vector >(sz, vector(sz)); + + // Compute ECC, draw modules + drawFunctionPatterns(); + const vector allCodewords = addEccAndInterleave(dataCodewords); + drawCodewords(allCodewords); + + // Do masking + if (msk == -1) { // Automatically choose best mask + long minPenalty = LONG_MAX; + for (int i = 0; i < 8; i++) { + applyMask(i); + drawFormatBits(i); + long penalty = getPenaltyScore(); + if (penalty < minPenalty) { + msk = i; + minPenalty = penalty; + } + applyMask(i); // Undoes the mask due to XOR + } + } + assert(0 <= msk && msk <= 7); + mask = msk; + applyMask(msk); // Apply the final choice of mask + drawFormatBits(msk); // Overwrite old format bits + + isFunction.clear(); + isFunction.shrink_to_fit(); +} + + +int QrCode::getVersion() const { + return version; +} + + +int QrCode::getSize() const { + return size; +} + + +QrCode::Ecc QrCode::getErrorCorrectionLevel() const { + return errorCorrectionLevel; +} + + +int QrCode::getMask() const { + return mask; +} + + +bool QrCode::getModule(int x, int y) const { + return 0 <= x && x < size && 0 <= y && y < size && module(x, y); +} + + +void QrCode::drawFunctionPatterns() { + // Draw horizontal and vertical timing patterns + for (int i = 0; i < size; i++) { + setFunctionModule(6, i, i % 2 == 0); + setFunctionModule(i, 6, i % 2 == 0); + } + + // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules) + drawFinderPattern(3, 3); + drawFinderPattern(size - 4, 3); + drawFinderPattern(3, size - 4); + + // Draw numerous alignment patterns + const vector alignPatPos = getAlignmentPatternPositions(); + size_t numAlign = alignPatPos.size(); + for (size_t i = 0; i < numAlign; i++) { + for (size_t j = 0; j < numAlign; j++) { + // Don't draw on the three finder corners + if (!((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0))) + drawAlignmentPattern(alignPatPos.at(i), alignPatPos.at(j)); + } + } + + // Draw configuration data + drawFormatBits(0); // Dummy mask value; overwritten later in the constructor + drawVersion(); +} + + +void QrCode::drawFormatBits(int msk) { + // Calculate error correction code and pack bits + int data = getFormatBits(errorCorrectionLevel) << 3 | msk; // errCorrLvl is uint2, msk is uint3 + int rem = data; + for (int i = 0; i < 10; i++) + rem = (rem << 1) ^ ((rem >> 9) * 0x537); + int bits = (data << 10 | rem) ^ 0x5412; // uint15 + assert(bits >> 15 == 0); + + // Draw first copy + for (int i = 0; i <= 5; i++) + setFunctionModule(8, i, getBit(bits, i)); + setFunctionModule(8, 7, getBit(bits, 6)); + setFunctionModule(8, 8, getBit(bits, 7)); + setFunctionModule(7, 8, getBit(bits, 8)); + for (int i = 9; i < 15; i++) + setFunctionModule(14 - i, 8, getBit(bits, i)); + + // Draw second copy + for (int i = 0; i < 8; i++) + setFunctionModule(size - 1 - i, 8, getBit(bits, i)); + for (int i = 8; i < 15; i++) + setFunctionModule(8, size - 15 + i, getBit(bits, i)); + setFunctionModule(8, size - 8, true); // Always dark +} + + +void QrCode::drawVersion() { + if (version < 7) + return; + + // Calculate error correction code and pack bits + int rem = version; // version is uint6, in the range [7, 40] + for (int i = 0; i < 12; i++) + rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); + long bits = static_cast(version) << 12 | rem; // uint18 + assert(bits >> 18 == 0); + + // Draw two copies + for (int i = 0; i < 18; i++) { + bool bit = getBit(bits, i); + int a = size - 11 + i % 3; + int b = i / 3; + setFunctionModule(a, b, bit); + setFunctionModule(b, a, bit); + } +} + + +void QrCode::drawFinderPattern(int x, int y) { + for (int dy = -4; dy <= 4; dy++) { + for (int dx = -4; dx <= 4; dx++) { + int dist = std::max(std::abs(dx), std::abs(dy)); // Chebyshev/infinity norm + int xx = x + dx, yy = y + dy; + if (0 <= xx && xx < size && 0 <= yy && yy < size) + setFunctionModule(xx, yy, dist != 2 && dist != 4); + } + } +} + + +void QrCode::drawAlignmentPattern(int x, int y) { + for (int dy = -2; dy <= 2; dy++) { + for (int dx = -2; dx <= 2; dx++) + setFunctionModule(x + dx, y + dy, std::max(std::abs(dx), std::abs(dy)) != 1); + } +} + + +void QrCode::setFunctionModule(int x, int y, bool isDark) { + size_t ux = static_cast(x); + size_t uy = static_cast(y); + modules .at(uy).at(ux) = isDark; + isFunction.at(uy).at(ux) = true; +} + + +bool QrCode::module(int x, int y) const { + return modules.at(static_cast(y)).at(static_cast(x)); +} + + +vector QrCode::addEccAndInterleave(const vector &data) const { + if (data.size() != static_cast(getNumDataCodewords(version, errorCorrectionLevel))) + throw std::invalid_argument("Invalid argument"); + + // Calculate parameter numbers + int numBlocks = NUM_ERROR_CORRECTION_BLOCKS[static_cast(errorCorrectionLevel)][version]; + int blockEccLen = ECC_CODEWORDS_PER_BLOCK [static_cast(errorCorrectionLevel)][version]; + int rawCodewords = getNumRawDataModules(version) / 8; + int numShortBlocks = numBlocks - rawCodewords % numBlocks; + int shortBlockLen = rawCodewords / numBlocks; + + // Split data into blocks and append ECC to each block + vector > blocks; + const vector rsDiv = reedSolomonComputeDivisor(blockEccLen); + for (int i = 0, k = 0; i < numBlocks; i++) { + vector dat(data.cbegin() + k, data.cbegin() + (k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1))); + k += static_cast(dat.size()); + const vector ecc = reedSolomonComputeRemainder(dat, rsDiv); + if (i < numShortBlocks) + dat.push_back(0); + dat.insert(dat.end(), ecc.cbegin(), ecc.cend()); + blocks.push_back(std::move(dat)); + } + + // Interleave (not concatenate) the bytes from every block into a single sequence + vector result; + for (size_t i = 0; i < blocks.at(0).size(); i++) { + for (size_t j = 0; j < blocks.size(); j++) { + // Skip the padding byte in short blocks + if (i != static_cast(shortBlockLen - blockEccLen) || j >= static_cast(numShortBlocks)) + result.push_back(blocks.at(j).at(i)); + } + } + assert(result.size() == static_cast(rawCodewords)); + return result; +} + + +void QrCode::drawCodewords(const vector &data) { + if (data.size() != static_cast(getNumRawDataModules(version) / 8)) + throw std::invalid_argument("Invalid argument"); + + size_t i = 0; // Bit index into the data + // Do the funny zigzag scan + for (int right = size - 1; right >= 1; right -= 2) { // Index of right column in each column pair + if (right == 6) + right = 5; + for (int vert = 0; vert < size; vert++) { // Vertical counter + for (int j = 0; j < 2; j++) { + size_t x = static_cast(right - j); // Actual x coordinate + bool upward = ((right + 1) & 2) == 0; + size_t y = static_cast(upward ? size - 1 - vert : vert); // Actual y coordinate + if (!isFunction.at(y).at(x) && i < data.size() * 8) { + modules.at(y).at(x) = getBit(data.at(i >> 3), 7 - static_cast(i & 7)); + i++; + } + // If this QR Code has any remainder bits (0 to 7), they were assigned as + // 0/false/light by the constructor and are left unchanged by this method + } + } + } + assert(i == data.size() * 8); +} + + +void QrCode::applyMask(int msk) { + if (msk < 0 || msk > 7) + throw std::domain_error("Mask value out of range"); + size_t sz = static_cast(size); + for (size_t y = 0; y < sz; y++) { + for (size_t x = 0; x < sz; x++) { + bool invert; + switch (msk) { + case 0: invert = (x + y) % 2 == 0; break; + case 1: invert = y % 2 == 0; break; + case 2: invert = x % 3 == 0; break; + case 3: invert = (x + y) % 3 == 0; break; + case 4: invert = (x / 3 + y / 2) % 2 == 0; break; + case 5: invert = x * y % 2 + x * y % 3 == 0; break; + case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break; + case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break; + default: throw std::logic_error("Unreachable"); + } + modules.at(y).at(x) = modules.at(y).at(x) ^ (invert & !isFunction.at(y).at(x)); + } + } +} + + +long QrCode::getPenaltyScore() const { + long result = 0; + + // Adjacent modules in row having same color, and finder-like patterns + for (int y = 0; y < size; y++) { + bool runColor = false; + int runX = 0; + std::array runHistory = {}; + for (int x = 0; x < size; x++) { + if (module(x, y) == runColor) { + runX++; + if (runX == 5) + result += PENALTY_N1; + else if (runX > 5) + result++; + } else { + finderPenaltyAddHistory(runX, runHistory); + if (!runColor) + result += finderPenaltyCountPatterns(runHistory) * PENALTY_N3; + runColor = module(x, y); + runX = 1; + } + } + result += finderPenaltyTerminateAndCount(runColor, runX, runHistory) * PENALTY_N3; + } + // Adjacent modules in column having same color, and finder-like patterns + for (int x = 0; x < size; x++) { + bool runColor = false; + int runY = 0; + std::array runHistory = {}; + for (int y = 0; y < size; y++) { + if (module(x, y) == runColor) { + runY++; + if (runY == 5) + result += PENALTY_N1; + else if (runY > 5) + result++; + } else { + finderPenaltyAddHistory(runY, runHistory); + if (!runColor) + result += finderPenaltyCountPatterns(runHistory) * PENALTY_N3; + runColor = module(x, y); + runY = 1; + } + } + result += finderPenaltyTerminateAndCount(runColor, runY, runHistory) * PENALTY_N3; + } + + // 2*2 blocks of modules having same color + for (int y = 0; y < size - 1; y++) { + for (int x = 0; x < size - 1; x++) { + bool color = module(x, y); + if ( color == module(x + 1, y) && + color == module(x, y + 1) && + color == module(x + 1, y + 1)) + result += PENALTY_N2; + } + } + + // Balance of dark and light modules + int dark = 0; + for (const vector &row : modules) { + for (bool color : row) { + if (color) + dark++; + } + } + int total = size * size; // Note that size is odd, so dark/total != 1/2 + // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)% + int k = static_cast((std::abs(dark * 20L - total * 10L) + total - 1) / total) - 1; + assert(0 <= k && k <= 9); + result += k * PENALTY_N4; + assert(0 <= result && result <= 2568888L); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4 + return result; +} + + +vector QrCode::getAlignmentPatternPositions() const { + if (version == 1) + return vector(); + else { + int numAlign = version / 7 + 2; + int step = (version == 32) ? 26 : + (version * 4 + numAlign * 2 + 1) / (numAlign * 2 - 2) * 2; + vector result; + for (int i = 0, pos = size - 7; i < numAlign - 1; i++, pos -= step) + result.insert(result.begin(), pos); + result.insert(result.begin(), 6); + return result; + } +} + + +int QrCode::getNumRawDataModules(int ver) { + if (ver < MIN_VERSION || ver > MAX_VERSION) + throw std::domain_error("Version number out of range"); + int result = (16 * ver + 128) * ver + 64; + if (ver >= 2) { + int numAlign = ver / 7 + 2; + result -= (25 * numAlign - 10) * numAlign - 55; + if (ver >= 7) + result -= 36; + } + assert(208 <= result && result <= 29648); + return result; +} + + +int QrCode::getNumDataCodewords(int ver, Ecc ecl) { + return getNumRawDataModules(ver) / 8 + - ECC_CODEWORDS_PER_BLOCK [static_cast(ecl)][ver] + * NUM_ERROR_CORRECTION_BLOCKS[static_cast(ecl)][ver]; +} + + +vector QrCode::reedSolomonComputeDivisor(int degree) { + if (degree < 1 || degree > 255) + throw std::domain_error("Degree out of range"); + // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1. + // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array {255, 8, 93}. + vector result(static_cast(degree)); + result.at(result.size() - 1) = 1; // Start off with the monomial x^0 + + // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}), + // and drop the highest monomial term which is always 1x^degree. + // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D). + uint8_t root = 1; + for (int i = 0; i < degree; i++) { + // Multiply the current product by (x - r^i) + for (size_t j = 0; j < result.size(); j++) { + result.at(j) = reedSolomonMultiply(result.at(j), root); + if (j + 1 < result.size()) + result.at(j) ^= result.at(j + 1); + } + root = reedSolomonMultiply(root, 0x02); + } + return result; +} + + +vector QrCode::reedSolomonComputeRemainder(const vector &data, const vector &divisor) { + vector result(divisor.size()); + for (uint8_t b : data) { // Polynomial division + uint8_t factor = b ^ result.at(0); + result.erase(result.begin()); + result.push_back(0); + for (size_t i = 0; i < result.size(); i++) + result.at(i) ^= reedSolomonMultiply(divisor.at(i), factor); + } + return result; +} + + +uint8_t QrCode::reedSolomonMultiply(uint8_t x, uint8_t y) { + // Russian peasant multiplication + int z = 0; + for (int i = 7; i >= 0; i--) { + z = (z << 1) ^ ((z >> 7) * 0x11D); + z ^= ((y >> i) & 1) * x; + } + assert(z >> 8 == 0); + return static_cast(z); +} + + +int QrCode::finderPenaltyCountPatterns(const std::array &runHistory) const { + int n = runHistory.at(1); + assert(n <= size * 3); + bool core = n > 0 && runHistory.at(2) == n && runHistory.at(3) == n * 3 && runHistory.at(4) == n && runHistory.at(5) == n; + return (core && runHistory.at(0) >= n * 4 && runHistory.at(6) >= n ? 1 : 0) + + (core && runHistory.at(6) >= n * 4 && runHistory.at(0) >= n ? 1 : 0); +} + + +int QrCode::finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, std::array &runHistory) const { + if (currentRunColor) { // Terminate dark run + finderPenaltyAddHistory(currentRunLength, runHistory); + currentRunLength = 0; + } + currentRunLength += size; // Add light border to final run + finderPenaltyAddHistory(currentRunLength, runHistory); + return finderPenaltyCountPatterns(runHistory); +} + + +void QrCode::finderPenaltyAddHistory(int currentRunLength, std::array &runHistory) const { + if (runHistory.at(0) == 0) + currentRunLength += size; // Add light border to initial run + std::copy_backward(runHistory.cbegin(), runHistory.cend() - 1, runHistory.end()); + runHistory.at(0) = currentRunLength; +} + + +bool QrCode::getBit(long x, int i) { + return ((x >> i) & 1) != 0; +} + + +/*---- Tables of constants ----*/ + +const int QrCode::PENALTY_N1 = 3; +const int QrCode::PENALTY_N2 = 3; +const int QrCode::PENALTY_N3 = 40; +const int QrCode::PENALTY_N4 = 10; + + +const int8_t QrCode::ECC_CODEWORDS_PER_BLOCK[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Low + {-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28}, // Medium + {-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Quartile + {-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // High +}; + +const int8_t QrCode::NUM_ERROR_CORRECTION_BLOCKS[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low + {-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium + {-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile + {-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High +}; + + +data_too_long::data_too_long(const std::string &msg) : + std::length_error(msg) {} + + + +/*---- Class BitBuffer ----*/ + +BitBuffer::BitBuffer() + : std::vector() {} + + +void BitBuffer::appendBits(std::uint32_t val, int len) { + if (len < 0 || len > 31 || val >> len != 0) + throw std::domain_error("Value out of range"); + for (int i = len - 1; i >= 0; i--) // Append bit by bit + this->push_back(((val >> i) & 1) != 0); +} + +} diff --git a/3rdparty/qrcodegen.hpp b/3rdparty/qrcodegen.hpp new file mode 100644 index 0000000..9448982 --- /dev/null +++ b/3rdparty/qrcodegen.hpp @@ -0,0 +1,549 @@ +/* + * QR Code generator library (C++) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +#pragma once + +#include +#include +#include +#include +#include + + +namespace qrcodegen { + +/* + * A segment of character/binary/control data in a QR Code symbol. + * Instances of this class are immutable. + * The mid-level way to create a segment is to take the payload data + * and call a static factory function such as QrSegment::makeNumeric(). + * The low-level way to create a segment is to custom-make the bit buffer + * and call the QrSegment() constructor with appropriate values. + * This segment class imposes no length restrictions, but QR Codes have restrictions. + * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data. + * Any segment longer than this is meaningless for the purpose of generating QR Codes. + */ +class QrSegment final { + + /*---- Public helper enumeration ----*/ + + /* + * Describes how a segment's data bits are interpreted. Immutable. + */ + public: class Mode final { + + /*-- Constants --*/ + + public: static const Mode NUMERIC; + public: static const Mode ALPHANUMERIC; + public: static const Mode BYTE; + public: static const Mode KANJI; + public: static const Mode ECI; + + + /*-- Fields --*/ + + // The mode indicator bits, which is a uint4 value (range 0 to 15). + private: int modeBits; + + // Number of character count bits for three different version ranges. + private: int numBitsCharCount[3]; + + + /*-- Constructor --*/ + + private: Mode(int mode, int cc0, int cc1, int cc2); + + + /*-- Methods --*/ + + /* + * (Package-private) Returns the mode indicator bits, which is an unsigned 4-bit value (range 0 to 15). + */ + public: int getModeBits() const; + + /* + * (Package-private) Returns the bit width of the character count field for a segment in + * this mode in a QR Code at the given version number. The result is in the range [0, 16]. + */ + public: int numCharCountBits(int ver) const; + + }; + + + + /*---- Static factory functions (mid level) ----*/ + + /* + * Returns a segment representing the given binary data encoded in + * byte mode. All input byte vectors are acceptable. Any text string + * can be converted to UTF-8 bytes and encoded as a byte mode segment. + */ + public: static QrSegment makeBytes(const std::vector &data); + + + /* + * Returns a segment representing the given string of decimal digits encoded in numeric mode. + */ + public: static QrSegment makeNumeric(const char *digits); + + + /* + * Returns a segment representing the given text string encoded in alphanumeric mode. + * The characters allowed are: 0 to 9, A to Z (uppercase only), space, + * dollar, percent, asterisk, plus, hyphen, period, slash, colon. + */ + public: static QrSegment makeAlphanumeric(const char *text); + + + /* + * Returns a list of zero or more segments to represent the given text string. The result + * may use various segment modes and switch modes to optimize the length of the bit stream. + */ + public: static std::vector makeSegments(const char *text); + + + /* + * Returns a segment representing an Extended Channel Interpretation + * (ECI) designator with the given assignment value. + */ + public: static QrSegment makeEci(long assignVal); + + + /*---- Public static helper functions ----*/ + + /* + * Tests whether the given string can be encoded as a segment in numeric mode. + * A string is encodable iff each character is in the range 0 to 9. + */ + public: static bool isNumeric(const char *text); + + + /* + * Tests whether the given string can be encoded as a segment in alphanumeric mode. + * A string is encodable iff each character is in the following set: 0 to 9, A to Z + * (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon. + */ + public: static bool isAlphanumeric(const char *text); + + + + /*---- Instance fields ----*/ + + /* The mode indicator of this segment. Accessed through getMode(). */ + private: const Mode *mode; + + /* The length of this segment's unencoded data. Measured in characters for + * numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode. + * Always zero or positive. Not the same as the data's bit length. + * Accessed through getNumChars(). */ + private: int numChars; + + /* The data bits of this segment. Accessed through getData(). */ + private: std::vector data; + + + /*---- Constructors (low level) ----*/ + + /* + * Creates a new QR Code segment with the given attributes and data. + * The character count (numCh) must agree with the mode and the bit buffer length, + * but the constraint isn't checked. The given bit buffer is copied and stored. + */ + public: QrSegment(const Mode &md, int numCh, const std::vector &dt); + + + /* + * Creates a new QR Code segment with the given parameters and data. + * The character count (numCh) must agree with the mode and the bit buffer length, + * but the constraint isn't checked. The given bit buffer is moved and stored. + */ + public: QrSegment(const Mode &md, int numCh, std::vector &&dt); + + + /*---- Methods ----*/ + + /* + * Returns the mode field of this segment. + */ + public: const Mode &getMode() const; + + + /* + * Returns the character count field of this segment. + */ + public: int getNumChars() const; + + + /* + * Returns the data bits of this segment. + */ + public: const std::vector &getData() const; + + + // (Package-private) Calculates the number of bits needed to encode the given segments at + // the given version. Returns a non-negative number if successful. Otherwise returns -1 if a + // segment has too many characters to fit its length field, or the total bits exceeds INT_MAX. + public: static int getTotalBits(const std::vector &segs, int version); + + + /*---- Private constant ----*/ + + /* The set of all legal characters in alphanumeric mode, where + * each character value maps to the index in the string. */ + private: static const char *ALPHANUMERIC_CHARSET; + +}; + + + +/* + * A QR Code symbol, which is a type of two-dimension barcode. + * Invented by Denso Wave and described in the ISO/IEC 18004 standard. + * Instances of this class represent an immutable square grid of dark and light cells. + * The class provides static factory functions to create a QR Code from text or binary data. + * The class covers the QR Code Model 2 specification, supporting all versions (sizes) + * from 1 to 40, all 4 error correction levels, and 4 character encoding modes. + * + * Ways to create a QR Code object: + * - High level: Take the payload data and call QrCode::encodeText() or QrCode::encodeBinary(). + * - Mid level: Custom-make the list of segments and call QrCode::encodeSegments(). + * - Low level: Custom-make the array of data codeword bytes (including + * segment headers and final padding, excluding error correction codewords), + * supply the appropriate version number, and call the QrCode() constructor. + * (Note that all ways require supplying the desired error correction level.) + */ +class QrCode final { + + /*---- Public helper enumeration ----*/ + + /* + * The error correction level in a QR Code symbol. + */ + public: enum class Ecc { + LOW = 0 , // The QR Code can tolerate about 7% erroneous codewords + MEDIUM , // The QR Code can tolerate about 15% erroneous codewords + QUARTILE, // The QR Code can tolerate about 25% erroneous codewords + HIGH , // The QR Code can tolerate about 30% erroneous codewords + }; + + + // Returns a value in the range 0 to 3 (unsigned 2-bit integer). + private: static int getFormatBits(Ecc ecl); + + + + /*---- Static factory functions (high level) ----*/ + + /* + * Returns a QR Code representing the given Unicode text string at the given error correction level. + * As a conservative upper bound, this function is guaranteed to succeed for strings that have 2953 or fewer + * UTF-8 code units (not Unicode code points) if the low error correction level is used. The smallest possible + * QR Code version is automatically chosen for the output. The ECC level of the result may be higher than + * the ecl argument if it can be done without increasing the version. + */ + public: static QrCode encodeText(const char *text, Ecc ecl); + + + /* + * Returns a QR Code representing the given binary data at the given error correction level. + * This function always encodes using the binary segment mode, not any text mode. The maximum number of + * bytes allowed is 2953. The smallest possible QR Code version is automatically chosen for the output. + * The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version. + */ + public: static QrCode encodeBinary(const std::vector &data, Ecc ecl); + + + /*---- Static factory functions (mid level) ----*/ + + /* + * Returns a QR Code representing the given segments with the given encoding parameters. + * The smallest possible QR Code version within the given range is automatically + * chosen for the output. Iff boostEcl is true, then the ECC level of the result + * may be higher than the ecl argument if it can be done without increasing the + * version. The mask number is either between 0 to 7 (inclusive) to force that + * mask, or -1 to automatically choose an appropriate mask (which may be slow). + * This function allows the user to create a custom sequence of segments that switches + * between modes (such as alphanumeric and byte) to encode text in less space. + * This is a mid-level API; the high-level API is encodeText() and encodeBinary(). + */ + public: static QrCode encodeSegments(const std::vector &segs, Ecc ecl, + int minVersion=1, int maxVersion=40, int mask=-1, bool boostEcl=true); // All optional parameters + + + + /*---- Instance fields ----*/ + + // Immutable scalar parameters: + + /* The version number of this QR Code, which is between 1 and 40 (inclusive). + * This determines the size of this barcode. */ + private: int version; + + /* The width and height of this QR Code, measured in modules, between + * 21 and 177 (inclusive). This is equal to version * 4 + 17. */ + private: int size; + + /* The error correction level used in this QR Code. */ + private: Ecc errorCorrectionLevel; + + /* The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive). + * Even if a QR Code is created with automatic masking requested (mask = -1), + * the resulting object still has a mask value between 0 and 7. */ + private: int mask; + + // Private grids of modules/pixels, with dimensions of size*size: + + // The modules of this QR Code (false = light, true = dark). + // Immutable after constructor finishes. Accessed through getModule(). + private: std::vector > modules; + + // Indicates function modules that are not subjected to masking. Discarded when constructor finishes. + private: std::vector > isFunction; + + + + /*---- Constructor (low level) ----*/ + + /* + * Creates a new QR Code with the given version number, + * error correction level, data codeword bytes, and mask number. + * This is a low-level API that most users should not use directly. + * A mid-level API is the encodeSegments() function. + */ + public: QrCode(int ver, Ecc ecl, const std::vector &dataCodewords, int msk); + + + + /*---- Public instance methods ----*/ + + /* + * Returns this QR Code's version, in the range [1, 40]. + */ + public: int getVersion() const; + + + /* + * Returns this QR Code's size, in the range [21, 177]. + */ + public: int getSize() const; + + + /* + * Returns this QR Code's error correction level. + */ + public: Ecc getErrorCorrectionLevel() const; + + + /* + * Returns this QR Code's mask, in the range [0, 7]. + */ + public: int getMask() const; + + + /* + * Returns the color of the module (pixel) at the given coordinates, which is false + * for light or true for dark. The top left corner has the coordinates (x=0, y=0). + * If the given coordinates are out of bounds, then false (light) is returned. + */ + public: bool getModule(int x, int y) const; + + + + /*---- Private helper methods for constructor: Drawing function modules ----*/ + + // Reads this object's version field, and draws and marks all function modules. + private: void drawFunctionPatterns(); + + + // Draws two copies of the format bits (with its own error correction code) + // based on the given mask and this object's error correction level field. + private: void drawFormatBits(int msk); + + + // Draws two copies of the version bits (with its own error correction code), + // based on this object's version field, iff 7 <= version <= 40. + private: void drawVersion(); + + + // Draws a 9*9 finder pattern including the border separator, + // with the center module at (x, y). Modules can be out of bounds. + private: void drawFinderPattern(int x, int y); + + + // Draws a 5*5 alignment pattern, with the center module + // at (x, y). All modules must be in bounds. + private: void drawAlignmentPattern(int x, int y); + + + // Sets the color of a module and marks it as a function module. + // Only used by the constructor. Coordinates must be in bounds. + private: void setFunctionModule(int x, int y, bool isDark); + + + // Returns the color of the module at the given coordinates, which must be in range. + private: bool module(int x, int y) const; + + + /*---- Private helper methods for constructor: Codewords and masking ----*/ + + // Returns a new byte string representing the given data with the appropriate error correction + // codewords appended to it, based on this object's version and error correction level. + private: std::vector addEccAndInterleave(const std::vector &data) const; + + + // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire + // data area of this QR Code. Function modules need to be marked off before this is called. + private: void drawCodewords(const std::vector &data); + + + // XORs the codeword modules in this QR Code with the given mask pattern. + // The function modules must be marked and the codeword bits must be drawn + // before masking. Due to the arithmetic of XOR, calling applyMask() with + // the same mask value a second time will undo the mask. A final well-formed + // QR Code needs exactly one (not zero, two, etc.) mask applied. + private: void applyMask(int msk); + + + // Calculates and returns the penalty score based on state of this QR Code's current modules. + // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score. + private: long getPenaltyScore() const; + + + + /*---- Private helper functions ----*/ + + // Returns an ascending list of positions of alignment patterns for this version number. + // Each position is in the range [0,177), and are used on both the x and y axes. + // This could be implemented as lookup table of 40 variable-length lists of unsigned bytes. + private: std::vector getAlignmentPatternPositions() const; + + + // Returns the number of data bits that can be stored in a QR Code of the given version number, after + // all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8. + // The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table. + private: static int getNumRawDataModules(int ver); + + + // Returns the number of 8-bit data (i.e. not error correction) codewords contained in any + // QR Code of the given version number and error correction level, with remainder bits discarded. + // This stateless pure function could be implemented as a (40*4)-cell lookup table. + private: static int getNumDataCodewords(int ver, Ecc ecl); + + + // Returns a Reed-Solomon ECC generator polynomial for the given degree. This could be + // implemented as a lookup table over all possible parameter values, instead of as an algorithm. + private: static std::vector reedSolomonComputeDivisor(int degree); + + + // Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials. + private: static std::vector reedSolomonComputeRemainder(const std::vector &data, const std::vector &divisor); + + + // Returns the product of the two given field elements modulo GF(2^8/0x11D). + // All inputs are valid. This could be implemented as a 256*256 lookup table. + private: static std::uint8_t reedSolomonMultiply(std::uint8_t x, std::uint8_t y); + + + // Can only be called immediately after a light run is added, and + // returns either 0, 1, or 2. A helper function for getPenaltyScore(). + private: int finderPenaltyCountPatterns(const std::array &runHistory) const; + + + // Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore(). + private: int finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, std::array &runHistory) const; + + + // Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore(). + private: void finderPenaltyAddHistory(int currentRunLength, std::array &runHistory) const; + + + // Returns true iff the i'th bit of x is set to 1. + private: static bool getBit(long x, int i); + + + /*---- Constants and tables ----*/ + + // The minimum version number supported in the QR Code Model 2 standard. + public: static constexpr int MIN_VERSION = 1; + + // The maximum version number supported in the QR Code Model 2 standard. + public: static constexpr int MAX_VERSION = 40; + + + // For use in getPenaltyScore(), when evaluating which mask is best. + private: static const int PENALTY_N1; + private: static const int PENALTY_N2; + private: static const int PENALTY_N3; + private: static const int PENALTY_N4; + + + private: static const std::int8_t ECC_CODEWORDS_PER_BLOCK[4][41]; + private: static const std::int8_t NUM_ERROR_CORRECTION_BLOCKS[4][41]; + +}; + + + +/*---- Public exception class ----*/ + +/* + * Thrown when the supplied data does not fit any QR Code version. Ways to handle this exception include: + * - Decrease the error correction level if it was greater than Ecc::LOW. + * - If the encodeSegments() function was called with a maxVersion argument, then increase + * it if it was less than QrCode::MAX_VERSION. (This advice does not apply to the other + * factory functions because they search all versions up to QrCode::MAX_VERSION.) + * - Split the text data into better or optimal segments in order to reduce the number of bits required. + * - Change the text or binary data to be shorter. + * - Change the text to fit the character set of a particular segment mode (e.g. alphanumeric). + * - Propagate the error upward to the caller/user. + */ +class data_too_long : public std::length_error { + + public: explicit data_too_long(const std::string &msg); + +}; + + + +/* + * An appendable sequence of bits (0s and 1s). Mainly used by QrSegment. + */ +class BitBuffer final : public std::vector { + + /*---- Constructor ----*/ + + // Creates an empty bit buffer (length 0). + public: BitBuffer(); + + + + /*---- Method ----*/ + + // Appends the given number of low-order bits of the given value + // to this buffer. Requires 0 <= len <= 31 and val < 2^len. + public: void appendBits(std::uint32_t val, int len); + +}; + +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..89e3cb0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,245 @@ +cmake_minimum_required(VERSION 3.5) + +project(nekoray VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# WINDOWS PDB FILE +if (WIN32) + if (MSVC) + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /Zi") + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF") + endif () +endif () + +# Find Qt + +if (NOT QT_VERSION_MAJOR) + set(QT_VERSION_MAJOR 5) +endif () +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Network Svg LinguistTools) + +if (NKR_CROSS) + set_property(TARGET Qt5::moc PROPERTY IMPORTED_LOCATION /usr/bin/moc) + set_property(TARGET Qt5::uic PROPERTY IMPORTED_LOCATION /usr/bin/uic) + set_property(TARGET Qt5::rcc PROPERTY IMPORTED_LOCATION /usr/bin/rcc) + set_property(TARGET Qt5::lrelease PROPERTY IMPORTED_LOCATION /usr/bin/lrelease) + set_property(TARGET Qt5::lupdate PROPERTY IMPORTED_LOCATION /usr/bin/lupdate) +endif () + +# Windows +include("cmake/fuck_windows/fuck.cmake") + +# My dependencies +include("cmake/print.cmake") +set(MY_DEPS_DIR "${CMAKE_SOURCE_DIR}/libs/deps/built") +list(APPEND CMAKE_PREFIX_PATH ${MY_DEPS_DIR}) + +# NKR +include("cmake/nkr.cmake") + +find_package(Threads) + +if (NKR_NO_EXTERNAL) + add_compile_definitions(NKR_NO_EXTERNAL) +else () + if (NKR_NO_GRPC) + add_compile_definitions(NKR_NO_GRPC) + else () + # My proto + include("cmake/myproto.cmake") + list(APPEND NKR_EXTERNAL_TARGETS myproto) + endif () + + # yaml-cpp (static) + find_package(yaml-cpp CONFIG REQUIRED) # only Release is built + list(APPEND NKR_EXTERNAL_TARGETS yaml-cpp) + + # zxing-cpp + find_package(ZXing CONFIG REQUIRED) + list(APPEND NKR_EXTERNAL_TARGETS ZXing::ZXing) + + # QHotkey (static submodule) + set(QHOTKEY_INSTALL OFF) + add_subdirectory(3rdparty/QHotkey) + list(APPEND NKR_EXTERNAL_TARGETS qhotkey) +endif () + +# debug print +if (DBG_CMAKE) + print_all_variables() + print_target_properties(myproto) + print_target_properties(yaml-cpp) + print_target_properties(ZXing::ZXing) + set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CMAKE_COMMAND} -E time") + set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "${CMAKE_COMMAND} -E time") +endif () + +# Sources +set(PROJECT_SOURCES + ${PLATFORM_FUCKING_SOURCES} + + main/main.cpp + main/NekoRay.cpp + main/NekoRay_Utils.cpp + + 3rdparty/qrcodegen.cpp + 3rdparty/QtExtKeySequenceEdit.cpp + + qv2ray/ui/LogHighlighter.cpp + qv2ray/ui/QvAutoCompleteTextEdit.cpp + qv2ray/utils/HTTPRequestHelper.cpp + qv2ray/components/proxy/QvProxyConfigurator.cpp + qv2ray/ui/widgets/common/QJsonModel.cpp + + qv2ray/ui/widgets/editors/w_JsonEditor.cpp + qv2ray/ui/widgets/editors/w_JsonEditor.hpp + qv2ray/ui/widgets/editors/w_JsonEditor.ui + + rpc/gRPC.cpp + + db/Database.cpp + db/TrafficLooper.cpp + db/ProfileFilter.cpp + + fmt/AbstractBean.cpp + fmt/Bean2CoreObj.cpp + fmt/Bean2External.cpp + fmt/Bean2Link.cpp + fmt/InsecureHint.cpp + fmt/Link2Bean.cpp + db/ConfigBuilder.cpp + fmt/ChainBean.hpp # translate + + sub/GroupUpdater.cpp + + sys/ExternalProcess.cpp + sys/AutoRun.cpp + + ui/ThemeManager.cpp + + ui/mainwindow_grpc.cpp + ui/mainwindow.cpp + ui/mainwindow.h + ui/mainwindow.ui + + ui/edit/dialog_edit_profile.h + ui/edit/dialog_edit_profile.cpp + ui/edit/dialog_edit_profile.ui + ui/edit/dialog_edit_group.h + ui/edit/dialog_edit_group.cpp + ui/edit/dialog_edit_group.ui + + ui/edit/edit_chain.h + ui/edit/edit_chain.cpp + ui/edit/edit_chain.ui + ui/edit/edit_socks_http.h + ui/edit/edit_socks_http.cpp + ui/edit/edit_socks_http.ui + ui/edit/edit_shadowsocks.h + ui/edit/edit_shadowsocks.cpp + ui/edit/edit_shadowsocks.ui + ui/edit/edit_vmess.h + ui/edit/edit_vmess.cpp + ui/edit/edit_vmess.ui + ui/edit/edit_trojan_vless.h + ui/edit/edit_trojan_vless.cpp + ui/edit/edit_trojan_vless.ui + + ui/edit/edit_naive.h + ui/edit/edit_naive.cpp + ui/edit/edit_naive.ui + + ui/edit/edit_custom.h + ui/edit/edit_custom.cpp + ui/edit/edit_custom.ui + + ui/dialog_basic_settings.cpp + ui/dialog_basic_settings.h + ui/dialog_basic_settings.ui + + ui/dialog_manage_groups.cpp + ui/dialog_manage_groups.h + ui/dialog_manage_groups.ui + + ui/dialog_manage_routes.cpp + ui/dialog_manage_routes.h + ui/dialog_manage_routes.ui + + ui/dialog_hotkey.cpp + ui/dialog_hotkey.h + ui/dialog_hotkey.ui + + ui/widget/ProxyItem.cpp + ui/widget/ProxyItem.h + ui/widget/ProxyItem.ui + ui/widget/GroupItem.cpp + ui/widget/GroupItem.h + ui/widget/GroupItem.ui + + res/neko.qrc + res/theme/feiyangqingyun/qss.qrc + ${QV2RAY_RC} + ) + +# Translations +set(TS_FILES + translations/zh_CN.ts + ) +qt_create_translation(QM_FILES ${PROJECT_SOURCES} ${TS_FILES} OPTIONS -locations none) +configure_file(translations/translations.qrc ${CMAKE_BINARY_DIR} COPYONLY) +set(PROJECT_SOURCES ${PROJECT_SOURCES} ${TS_FILES} ${QM_FILES} ${CMAKE_BINARY_DIR}/translations.qrc) + +# Qt exe +if (${QT_VERSION_MAJOR} GREATER_EQUAL 6) + qt_add_executable(nekoray + MANUAL_FINALIZATION + ${PROJECT_SOURCES} + ) + # Define target properties for Android with Qt 6 as: + # set_property(TARGET nekoray APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR + # ${CMAKE_CURRENT_SOURCE_DIR}/android) + # For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation +else () + if (ANDROID) + add_library(nekoray SHARED + ${PROJECT_SOURCES} + ) + # Define properties for Android with Qt 5 after find_package() calls as: + # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") + else () + add_executable(nekoray + ${PROJECT_SOURCES} + ) + endif () +endif () + +# Target + +set_property(TARGET nekoray PROPERTY AUTOUIC ON) +set_property(TARGET nekoray PROPERTY AUTOMOC ON) +set_property(TARGET nekoray PROPERTY AUTORCC ON) + +# Target Link + +target_link_libraries(nekoray PRIVATE + Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Svg + Threads::Threads + ${NKR_EXTERNAL_TARGETS} + ${PLATFORM_FUCKING_LIBRARIES} + ) + +set_target_properties(nekoray PROPERTIES + MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE + ) + +if (QT_VERSION_MAJOR EQUAL 6) + qt_finalize_executable(nekoray) +endif () diff --git a/README.md b/README.md new file mode 100644 index 0000000..446f1e2 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +# NekoRay + +基于 Qt/C++ 的跨平台代理配置管理器( 使用 Matsuri 定制版 v2ray-core ) + +目前支持 Windows / Linux amd64 开箱即用 + +Qt/C++ based cross-platform proxy configuration manager ( Use Matsuri custom version of v2ray-core ) + +Support Windows / Linux amd64 out of the box now. + +## 下载 Download + +便携格式,无安装器。转到 Releases 下载预编译的二进制文件,解压后即可使用。 + +### GitHub Releases 下载 + +[![GitHub All Releases](https://img.shields.io/github/downloads/Matsuridayo/nekoray/total?label=downloads-total&logo=github&style=flat-square)](https://github.com/Matsuridayo/nekoray/releases) + +## 更改记录 & 发布频道 Changelog & Telegram channel + +https://t.me/Matsuridayo + +## 项目主页 & 文档 Homepage & Documents + +https://matsuridayo.github.io + +### 运行参数 + +- `-many` 无视同目录正在运行的实例,强行开启新的实例 (0.11+) +- `-appdata` 开启后配置文件会放在共享目录,无法多开和自动升级 (0.11+) + +### 代理 + +| 协议 | 状态 | 配置编辑 | 分享链接生成 | 分享链接解析 | Clash 配置解析 | +|--------------|--------|------|--------|-----------|------------| +| Socks | ✅ | ✅ | ✅ | ✅ | ✅ | +| HTTP | ✅ | ✅ | ✅ | ✅ | ✅ | +| Shadowsocks | ✅ (经典) | ✅ | ✅ | 常见格式 | ✅ | +| VMess | ✅ | ✅ | ✅ | v2rayN 格式 | ✅ | +| Trojan | ✅ | ✅ | ✅ | 标准&常见格式 | ✅ | +| VLESS | ✅ | ✅ | ✅ | ✅ | 不适用 | +| NaïveProxy | ✅ | ✅ | ✅ | ✅ | 不适用 | +| Hysteria | ✅ | ❌ | ❌ | ❌ | 不适用 | + +## Linux 运行 & 简易编译教程 + +**使用 Linux 系统相信您已具备基本的排错能力, +本项目不提供特定发行版/架构的支持,预编译文件不能满足您的需求时,请自行编译/适配。** + +系统要求: Qt5 运行环境,一般桌面 Linux 已经安装,如果没有请用包管理器安装,如 + +`apt install libqt5gui5 libqt5x11extras5` + +运行: `./launcher` 或 部分系统可双击打开 + +launcher 参数 + +* `./launcher -- -appdata` `--` 后的参数传递给主程序 +* `-debug` Debug mode +* `-theme` Use local QT theme (unstable) (1.0+) + +已知部分 x86_64 Linux 发行版无法使用预编译版、非 x86_64 暂无适配,可以尝试自行编译。 + +### 编译 + +准备工作 + +``` +git submodule init +git submodule update +``` + +| CMake 参数 | 默认值 | 含义 | +|-------------------------|-----|-------------------------| +| QT_VERSION_MAJOR | 5 | QT版本 | +| NKR_NO_EXTERNAL | | 不包含外部C++依赖(如ZXing/gRPC) | +| NKR_NO_GRPC | | 不包含gRPC | +| NKR_CROSS | | | + +### 简单编译法 + +条件: + +1. C++ 依赖: `qt5 protobuf yaml-cpp zxing-cpp` 已用包管理器安装,并符合版本要求 +2. Qt 版本必须大于等于 5.15 +3. 系统为 `x86-64-linux-gnu` + +``` +mkdir build +cd build +cmake -GNinja .. +ninja +``` + +编译完成后得到 `nekoray` + +解压 Release 的压缩包,替换其中的 `nekoray`,删除 `launcher` 即可使用。 + +### 复杂编译法 + +C++ 部分 + +当您的发行版没有上面几个 C++ 依赖包,或者版本不符合要求时,可以参考 libs 文件夹内的默认编译脚本自行编译。 + +依赖搜寻 prefix 为 `libs/deps/bulit` + +编译完成后得到 `nekoray` + +Go 部分 + +1. 把 `Matsuridayo/Matsuri` `Matsuridayo/v2ray-core` 置于 `../` +2. 进入 `go` 文件夹 `go build` 得到 `nekoray_core`。 + +非官方构建无需编译 `updater` `launcher` + +## Credits + +- [v2fly/v2ray-core](https://github.com/v2fly/v2ray-core) +- [MatsuriDayo/Matsuri](https://github.com/MatsuriDayo/Matsuri) +- [MatsuriDayo/v2ray-core](https://github.com/MatsuriDayo/v2ray-core) +- [SagerNet/sing-box](https://github.com/SagerNet/sing-box) +- [Qt](https://www.qt.io/) +- [protobuf](https://github.com/protocolbuffers/protobuf) +- [yaml-cpp](https://github.com/jbeder/yaml-cpp) +- [zxing-cpp](https://github.com/nu-book/zxing-cpp) +- [QHotkey](https://github.com/Skycoder42/QHotkey) diff --git a/assets/nekoray.png b/assets/nekoray.png new file mode 100644 index 0000000000000000000000000000000000000000..26248606e85305501b2a883637b17ac2453a6fff GIT binary patch literal 80637 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 P00000NkvXXu0mjffO9;$ literal 0 HcmV?d00001 diff --git a/assets/qtbase_zh_CN.qm b/assets/qtbase_zh_CN.qm new file mode 100644 index 0000000000000000000000000000000000000000..8f4354d4b36cfc7a1566c01477a7ab4bc297a621 GIT binary patch literal 133919 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 h%93J+rxn{9!e9u1Zj10sAzh(1p) + #include + #else + #include + #include + #endif +#else + // MSVC, Windows SDK + #include +#endif + +IDI_ICON1 ICON PRODUCT_ICON + +LANGUAGE LANG_RUSSIAN, SUBLANG_DEFAULT + +VS_VERSION_INFO VERSIONINFO + FILEVERSION FILE_VERSION_RESOURCE + PRODUCTVERSION PRODUCT_VERSION_RESOURCE + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x1L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "000904b0" + BEGIN + VALUE "Comments", PRODUCT_COMMENTS + VALUE "CompanyName", PRODUCT_COMPANY_NAME + VALUE "FileDescription", PRODUCT_FILE_DESCRIPTION + VALUE "FileVersion", FILE_VERSION_RESOURCE_STR + VALUE "InternalName", PRODUCT_INTERNAL_NAME + VALUE "LegalCopyright", PRODUCT_COMPANY_COPYRIGHT + VALUE "OriginalFilename", PRODUCT_ORIGINAL_FILENAME + VALUE "ProductName", PRODUCT_BUNDLE + VALUE "ProductVersion", PRODUCT_VERSION_RESOURCE_STR + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x9, 1200 + END +END diff --git a/cmake/fuck_windows/fuck.cmake b/cmake/fuck_windows/fuck.cmake new file mode 100644 index 0000000..e9b455d --- /dev/null +++ b/cmake/fuck_windows/fuck.cmake @@ -0,0 +1,27 @@ +if (WIN32) + set(PLATFORM_FUCKING_SOURCES 3rdparty/WinCommander.cpp) + + include(cmake/fuck_windows/generate_product_version.cmake) + generate_product_version( + QV2RAY_RC + NAME "Nekoray" + BUNDLE "Nekoray Project Family" + ICON "${CMAKE_SOURCE_DIR}/res/nekoray.ico" + COMPANY_NAME "Nekoray Workgroup" + COMPANY_COPYRIGHT "Nekoray Workgroup" + FILE_DESCRIPTION "Nekoray Main Application" + ) + add_definitions(-DUNICODE -D_UNICODE -DNOMINMAX) + set(GUI_TYPE WIN32) + if (MINGW) + if (NOT DEFINED MinGW_ROOT) + set(MinGW_ROOT "C:/msys64/mingw64") + endif () + else () + add_compile_options("/utf-8") + add_compile_options("/std:c++17") + add_definitions(-D_WIN32_WINNT=0x600 -D_SCL_SECURE_NO_WARNINGS -D_CRT_SECURE_NO_WARNINGS) + set(PLATFORM_FUCKING_LIBRARIES wininet wsock32 ws2_32 user32 Rasapi32 Iphlpapi) + list(APPEND PLATFORM_FUCKING_SOURCES sys/windows/MiniDump.cpp) + endif () +endif () diff --git a/cmake/fuck_windows/generate_product_version.cmake b/cmake/fuck_windows/generate_product_version.cmake new file mode 100644 index 0000000..bc395d4 --- /dev/null +++ b/cmake/fuck_windows/generate_product_version.cmake @@ -0,0 +1,107 @@ +include (CMakeParseArguments) + +set (GenerateProductVersionCurrentDir ${CMAKE_CURRENT_LIST_DIR}) + +# generate_product_version() function +# +# This function uses VersionInfo.in template file and VersionResource.rc file +# to generate WIN32 resource with version information and general resource strings. +# +# Usage: +# generate_product_version( +# SomeOutputResourceVariable +# NAME MyGreatProject +# ICON ${PATH_TO_APP_ICON} +# VERSION_MAJOR 2 +# VERSION_MINOR 3 +# VERSION_PATCH ${BUILD_COUNTER} +# VERSION_REVISION ${BUILD_REVISION} +# ) +# where BUILD_COUNTER and BUILD_REVISION could be values from your CI server. +# +# You can use generated resource for your executable targets: +# add_executable(target-name ${target-files} ${SomeOutputResourceVariable}) +# +# You can specify resource strings in arguments: +# NAME - name of executable (no defaults, ex: Microsoft Word) +# BUNDLE - bundle (${NAME} is default, ex: Microsoft Office) +# ICON - path to application icon (${CMAKE_SOURCE_DIR}/product.ico by default) +# VERSION_MAJOR - 1 is default +# VERSION_MINOR - 0 is default +# VERSION_PATCH - 0 is default +# VERSION_REVISION - 0 is default +# COMPANY_NAME - your company name (no defaults) +# COMPANY_COPYRIGHT - ${COMPANY_NAME} (C) Copyright ${CURRENT_YEAR} is default +# COMMENTS - ${NAME} v${VERSION_MAJOR}.${VERSION_MINOR} is default +# ORIGINAL_FILENAME - ${NAME} is default +# INTERNAL_NAME - ${NAME} is default +# FILE_DESCRIPTION - ${NAME} is default +function(generate_product_version outfiles) + set (options) + set (oneValueArgs + NAME + BUNDLE + ICON + VERSION_MAJOR + VERSION_MINOR + VERSION_PATCH + VERSION_REVISION + COMPANY_NAME + COMPANY_COPYRIGHT + COMMENTS + ORIGINAL_FILENAME + INTERNAL_NAME + FILE_DESCRIPTION) + set (multiValueArgs) + cmake_parse_arguments(PRODUCT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if (NOT PRODUCT_BUNDLE OR "${PRODUCT_BUNDLE}" STREQUAL "") + set(PRODUCT_BUNDLE "${PRODUCT_NAME}") + endif() + if (NOT PRODUCT_ICON OR "${PRODUCT_ICON}" STREQUAL "") + set(PRODUCT_ICON "${CMAKE_SOURCE_DIR}/product.ico") + endif() + + if (NOT PRODUCT_VERSION_MAJOR EQUAL 0 AND (NOT PRODUCT_VERSION_MAJOR OR "${PRODUCT_VERSION_MAJOR}" STREQUAL "")) + set(PRODUCT_VERSION_MAJOR 1) + endif() + if (NOT PRODUCT_VERSION_MINOR EQUAL 0 AND (NOT PRODUCT_VERSION_MINOR OR "${PRODUCT_VERSION_MINOR}" STREQUAL "")) + set(PRODUCT_VERSION_MINOR 0) + endif() + if (NOT PRODUCT_VERSION_PATCH EQUAL 0 AND (NOT PRODUCT_VERSION_PATCH OR "${PRODUCT_VERSION_PATCH}" STREQUAL "")) + set(PRODUCT_VERSION_PATCH 0) + endif() + if (NOT PRODUCT_VERSION_REVISION EQUAL 0 AND (NOT PRODUCT_VERSION_REVISION OR "${PRODUCT_VERSION_REVISION}" STREQUAL "")) + set(PRODUCT_VERSION_REVISION 0) + endif() + + if (NOT PRODUCT_COMPANY_COPYRIGHT OR "${PRODUCT_COMPANY_COPYRIGHT}" STREQUAL "") + string(TIMESTAMP PRODUCT_CURRENT_YEAR "%Y") + set(PRODUCT_COMPANY_COPYRIGHT "${PRODUCT_COMPANY_NAME} (C) Copyright ${PRODUCT_CURRENT_YEAR}") + endif() + if (NOT PRODUCT_COMMENTS OR "${PRODUCT_COMMENTS}" STREQUAL "") + set(PRODUCT_COMMENTS "${PRODUCT_NAME} v${PRODUCT_VERSION_MAJOR}.${PRODUCT_VERSION_MINOR}") + endif() + if (NOT PRODUCT_ORIGINAL_FILENAME OR "${PRODUCT_ORIGINAL_FILENAME}" STREQUAL "") + set(PRODUCT_ORIGINAL_FILENAME "${PRODUCT_NAME}") + endif() + if (NOT PRODUCT_INTERNAL_NAME OR "${PRODUCT_INTERNAL_NAME}" STREQUAL "") + set(PRODUCT_INTERNAL_NAME "${PRODUCT_NAME}") + endif() + if (NOT PRODUCT_FILE_DESCRIPTION OR "${PRODUCT_FILE_DESCRIPTION}" STREQUAL "") + set(PRODUCT_FILE_DESCRIPTION "${PRODUCT_NAME}") + endif() + + set (_VersionInfoFile ${CMAKE_CURRENT_BINARY_DIR}/VersionInfo.h) + set (_VersionResourceFile ${CMAKE_CURRENT_BINARY_DIR}/VersionResource.rc) + configure_file( + ${GenerateProductVersionCurrentDir}/VersionInfo.in + ${_VersionInfoFile} + @ONLY) + configure_file( + ${GenerateProductVersionCurrentDir}/VersionResource.rc + ${_VersionResourceFile} + COPYONLY) + list(APPEND ${outfiles} ${_VersionInfoFile} ${_VersionResourceFile}) + set (${outfiles} ${${outfiles}} PARENT_SCOPE) +endfunction() diff --git a/cmake/myproto.cmake b/cmake/myproto.cmake new file mode 100644 index 0000000..89ddb70 --- /dev/null +++ b/cmake/myproto.cmake @@ -0,0 +1,14 @@ +find_package(Protobuf CONFIG REQUIRED) + +set(PROTO_FILES + go/gen/libcore.proto + ) + +add_library(myproto ${PROTO_FILES}) +target_link_libraries(myproto + PUBLIC + protobuf::libprotobuf + ) +target_include_directories(myproto PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) + +protobuf_generate(TARGET myproto LANGUAGE cpp) diff --git a/cmake/nkr.cmake b/cmake/nkr.cmake new file mode 100644 index 0000000..6ae6652 --- /dev/null +++ b/cmake/nkr.cmake @@ -0,0 +1,10 @@ +# Release +file(STRINGS nekoray_version.txt NKR_VERSION) +add_compile_definitions(NKR_VERSION=\"${NKR_VERSION}\") + +# Debug +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DNKR_DEBUG") + +if (NKR_USE_APPDATA) + add_compile_definitions(NKR_USE_APPDATA) +endif () diff --git a/cmake/print.cmake b/cmake/print.cmake new file mode 100644 index 0000000..89705d2 --- /dev/null +++ b/cmake/print.cmake @@ -0,0 +1,43 @@ +macro(print_all_variables) + message(STATUS "print_all_variables------------------------------------------{") + get_cmake_property(_variableNames VARIABLES) + foreach (_variableName ${_variableNames}) + message(STATUS "${_variableName}=${${_variableName}}") + endforeach() + message(STATUS "print_all_variables------------------------------------------}") +endmacro() + +# Get all propreties that cmake supports +if(NOT CMAKE_PROPERTY_LIST) + execute_process(COMMAND cmake --help-property-list OUTPUT_VARIABLE CMAKE_PROPERTY_LIST) + + # Convert command output into a CMake list + string(REGEX REPLACE ";" "\\\\;" CMAKE_PROPERTY_LIST "${CMAKE_PROPERTY_LIST}") + string(REGEX REPLACE "\n" ";" CMAKE_PROPERTY_LIST "${CMAKE_PROPERTY_LIST}") +endif() + +function(print_properties) + message("CMAKE_PROPERTY_LIST = ${CMAKE_PROPERTY_LIST}") +endfunction() + +function(print_target_properties target) + if(NOT TARGET ${target}) + message(STATUS "There is no target named '${target}'") + return() + endif() + + foreach(property ${CMAKE_PROPERTY_LIST}) + string(REPLACE "" "${CMAKE_BUILD_TYPE}" property ${property}) + + # Fix https://stackoverflow.com/questions/32197663/how-can-i-remove-the-the-location-property-may-not-be-read-from-target-error-i + if(property STREQUAL "LOCATION" OR property MATCHES "^LOCATION_" OR property MATCHES "_LOCATION$") + continue() + endif() + + get_property(was_set TARGET ${target} PROPERTY ${property} SET) + if(was_set) + get_target_property(value ${target} ${property}) + message("${target} ${property} = ${value}") + endif() + endforeach() +endfunction() diff --git a/core_commit.txt b/core_commit.txt new file mode 100644 index 0000000..55ceba3 --- /dev/null +++ b/core_commit.txt @@ -0,0 +1 @@ +edf4fcff77a0dfd40b20076faf45767a3394f5eb diff --git a/db/ConfigBuilder.cpp b/db/ConfigBuilder.cpp new file mode 100644 index 0000000..23f9e6a --- /dev/null +++ b/db/ConfigBuilder.cpp @@ -0,0 +1,475 @@ +#include "db/ConfigBuilder.hpp" +#include "db/Database.hpp" +#include "fmt/includes.h" + +namespace NekoRay { + + void ApplyCustomOutboundJsonSettings(const QJsonObject &custom, QJsonObject &outbound) { + // 合并 + if (custom.isEmpty()) return; + for (const auto &key: custom.keys()) { + if (outbound.contains(key)) { + auto v = custom[key]; + auto v_orig = outbound[key]; + if (v.isObject() && v_orig.isObject()) {// isObject 则合并? + auto vo = v.toObject(); + QJsonObject vo_orig = v_orig.toObject(); + ApplyCustomOutboundJsonSettings(vo, vo_orig); + outbound[key] = vo_orig; + } else { + outbound[key] = v; + } + } else { + outbound[key] = custom[key]; + } + } + } + + QSharedPointer BuildConfig(const QSharedPointer &ent, bool forTest) { + auto result = QSharedPointer(new BuildConfigResult); + auto status = QSharedPointer(new BuildConfigStatus); + status->result = result; + + // Log + auto logObj = QJsonObject{{"loglevel", dataStore->log_level}}; + result->coreConfig.insert("log", logObj); + + // Inbounds + QJsonObject sniffing{{"destOverride", dataStore->fake_dns ? + QJsonArray{"fakedns", "http", "tls", "quic"} + : QJsonArray{"http", "tls", "quic"}}, + {"enabled", true}, + {"metadataOnly", false}, + {"routeOnly", dataStore->sniffing_mode == SniffingMode::FOR_ROUTING},}; + + // socks-in + if (InRange(dataStore->inbound_socks_port, 0, 65535) && !forTest) { + QJsonObject socksInbound; + socksInbound["tag"] = "socks-in"; + socksInbound["protocol"] = "socks"; + socksInbound["listen"] = dataStore->inbound_address; + socksInbound["port"] = dataStore->inbound_socks_port; + socksInbound["settings"] = QJsonObject({{"auth", "noauth"}, + {"udp", true},}); + if (dataStore->fake_dns || dataStore->sniffing_mode != SniffingMode::DISABLE) { + socksInbound["sniffing"] = sniffing; + } + status->inbounds += socksInbound; + } + // http-in + if (InRange(dataStore->inbound_http_port, 0, 65535) && !forTest) { + QJsonObject socksInbound; + socksInbound["tag"] = "http-in"; + socksInbound["protocol"] = "http"; + socksInbound["listen"] = dataStore->inbound_address; + socksInbound["port"] = dataStore->inbound_http_port; + if (dataStore->sniffing_mode != SniffingMode::DISABLE) { + socksInbound["sniffing"] = sniffing; + } + status->inbounds += socksInbound; + } + + // Outbounds + QList> ents; + if (ent->type == "chain") { + auto list = ent->ChainBean()->list; + std::reverse(std::begin(list), std::end(list)); + for (auto id: list) { + ents += profileManager->GetProfile(id); + if (ents.last() == nullptr) { + result->error = QString("chain missing ent: %1").arg(id); + return result; + } + if (ents.last()->type == "chain") { + result->error = QString("chain in chain is not allowed: %1").arg(id); + return result; + } + } + } else { + ents += ent; + } + status->currentEnt = ent.get(); + QString tagProxy = BuildChain(0, ents, status); + if (!result->error.isEmpty()) return result; + + // direct & bypass & block + status->outbounds += QJsonObject{{"protocol", "freedom"}, + {"tag", "direct"},}; + status->outbounds += QJsonObject{{"protocol", "freedom"}, + {"tag", "bypass"},}; + status->outbounds += QJsonObject{{"protocol", "blackhole"}, + {"tag", "block"},}; + + // block for tun + if (!forTest) { + status->routingRules += QJsonObject{{"type", "field"}, + {"ip", QJsonArray{"224.0.0.0/3", "169.254.0.0/16",},}, + {"outboundTag", "block"},}; + status->routingRules += QJsonObject{{"type", "field"}, + {"port", "135-139"}, + {"outboundTag", "block"},}; + } + + // DNS Routing (tun2socks 用到,防污染) + if (dataStore->dns_routing && !forTest) { + QJsonObject dnsOut; + dnsOut["protocol"] = "dns"; + dnsOut["tag"] = "dns-out"; + QJsonObject dnsOut_settings; + dnsOut_settings["network"] = "tcp"; + dnsOut_settings["port"] = 53; + dnsOut_settings["address"] = "8.8.8.8"; + dnsOut_settings["userLevel"] = 1; + dnsOut["settings"] = dnsOut_settings; + dnsOut["proxySettings"] = QJsonObject{ + {"tag", tagProxy}, + {"transportLayer", true} + }; + + status->outbounds += dnsOut; + status->routingRules += QJsonObject{ + {"type", "field"}, + {"port", "53"}, + {"inboundTag", QJsonArray{"socks-in", "http-in"}}, + {"outboundTag", "dns-out"}, + }; + status->routingRules += QJsonObject{ + {"type", "field"}, + {"inboundTag", QJsonArray{"dns-in"}}, + {"outboundTag", "dns-out"}, + }; + } + + // custom inbound + QJSONARRAY_ADD(status->inbounds, QString2QJsonObject(dataStore->custom_inbound)["inbounds"].toArray()) + + result->coreConfig.insert("inbounds", status->inbounds); + result->coreConfig.insert("outbounds", status->outbounds); + + // dns domain user rule + for (const auto &line: SplitLines(dataStore->routing->proxy_domain)) { + if (line.startsWith("#")) continue; + if (dataStore->dns_routing) status->domainListDNSRemote += line; + status->domainListRemote += line; + } + for (const auto &line: SplitLines(dataStore->routing->direct_domain)) { + if (line.startsWith("#")) continue; + if (dataStore->dns_routing) status->domainListDNSDirect += line; + status->domainListDirect += line; + } + for (const auto &line: SplitLines(dataStore->routing->block_domain)) { + if (line.startsWith("#")) continue; + status->domainListBlock += line; + } + + // final add DNS + QJsonObject dns; + QJsonArray dnsServers; + + // FakeDNS + QJsonObject dnsServerFake; + dnsServerFake["address"] = "fakedns"; + dnsServerFake["domains"] = status->domainListDNSRemote; + if (dataStore->fake_dns && !forTest) dnsServers += dnsServerFake; + + // remote + QJsonObject dnsServerRemote; + dnsServerRemote["address"] = dataStore->remote_dns; + dnsServerRemote["domains"] = status->domainListDNSRemote; + if (!forTest) dnsServers += dnsServerRemote; + + //direct + auto directDnsAddress = dataStore->direct_dns; + if (directDnsAddress.contains("://")) { + auto directDnsIp = SubStrBefore(SubStrAfter(directDnsAddress, "://"), "/"); + if (IsIpAddress(directDnsIp)) { + status->routingRules.push_front(QJsonObject{ + {"type", "field"}, + {"ip", QJsonArray{directDnsIp}}, + {"outboundTag", "direct"}, + }); + } else { + status->routingRules.push_front(QJsonObject{ + {"type", "field"}, + {"domain", QJsonArray{directDnsIp}}, + {"outboundTag", "direct"}, + }); + } + } else if (directDnsAddress != "localhost") { + status->routingRules.push_front(QJsonObject{ + {"type", "field"}, + {"ip", QJsonArray{directDnsAddress}}, + {"outboundTag", "direct"}, + }); + } + dnsServers += QJsonObject{{"address", directDnsAddress}, + {"domains", status->domainListDNSDirect}, + {"skipFallback", true},}; + + dns["disableFallbackIfMatch"] = true; + dns["hosts"] = status->hosts; + dns["servers"] = dnsServers; + dns["tag"] = "dns"; + result->coreConfig.insert("dns", dns); + + // Routing + QJsonObject routing; + routing["domainStrategy"] = dataStore->domain_strategy; + routing["domainMatcher"] = dataStore->domain_matcher == DomainMatcher::MPH ? "mph" : "linear"; + + // ip user rule + QJsonObject routingRule_tmp; + routingRule_tmp["type"] = "field"; + + // block + routingRule_tmp["outboundTag"] = "block"; + for (const auto &line: SplitLines(dataStore->routing->block_ip)) { + if (line.startsWith("#")) continue; + status->ipListBlock += line; + } + // final add block route + if (!status->ipListBlock.isEmpty()) { + auto tmp = routingRule_tmp; + tmp["ip"] = status->ipListBlock; + status->routingRules += tmp; + } + if (!status->domainListBlock.isEmpty()) { + auto tmp = routingRule_tmp; + tmp["domain"] = status->domainListBlock; + status->routingRules += tmp; + } + + // proxy + routingRule_tmp["outboundTag"] = tagProxy; + for (const auto &line: SplitLines(dataStore->routing->proxy_ip)) { + if (line.startsWith("#")) continue; + status->ipListRemote += line; + } + // final add proxy route + if (!status->ipListRemote.isEmpty()) { + auto tmp = routingRule_tmp; + tmp["ip"] = status->ipListRemote; + status->routingRules += tmp; + } + if (!status->domainListRemote.isEmpty()) { + auto tmp = routingRule_tmp; + tmp["domain"] = status->domainListRemote; + status->routingRules += tmp; + } + + // bypass + routingRule_tmp["outboundTag"] = "bypass"; + for (const auto &line: SplitLines(dataStore->routing->direct_ip)) { + if (line.startsWith("#")) continue; + status->ipListDirect += line; + } + // final add bypass route + if (!status->ipListDirect.isEmpty()) { + auto tmp = routingRule_tmp; + tmp["ip"] = status->ipListDirect; + status->routingRules += tmp; + } + if (!status->domainListDirect.isEmpty()) { + auto tmp = routingRule_tmp; + tmp["domain"] = status->domainListDirect; + status->routingRules += tmp; + } + + // final add routing rule + // custom routing rule + auto routingRules = QString2QJsonObject(dataStore->routing->custom)["rules"].toArray(); + QJSONARRAY_ADD(routingRules, QString2QJsonObject(dataStore->custom_route_global)["rules"].toArray()) + QJSONARRAY_ADD(routingRules, status->routingRules) + routing["rules"] = routingRules; + result->coreConfig.insert("routing", routing); + + // Policy & stats + QJsonObject policy; + QJsonObject levels; + QJsonObject level1; + level1["connIdle"] = 30; + levels["1"] = level1; + policy["levels"] = levels; + + QJsonObject policySystem; + policySystem["statsOutboundDownlink"] = true; + policySystem["statsOutboundUplink"] = true; + policy["system"] = policySystem; + result->coreConfig.insert("policy", policy); + result->coreConfig.insert("stats", QJsonObject()); + + return result; + } + + QString BuildChain(int chainId, const QList> &ents, + const QSharedPointer &status) { + QString chainTag = "c-" + Int2String(chainId); + bool muxApplied = false; + + QString pastTag; + int index = 0; + + for (const auto &ent: ents) { + // tagOut: v2ray outbound tag for a profile + // profile2 (in) (global) tag g-(id) + // profile1 tag (chainTag)-(id) + // profile0 (out) tag (chainTag)-(id) / single: chainTag=g-(id) + auto tagOut = chainTag + "-" + Int2String(ent->id); + + // needGlobal: can only contain one? + bool needGlobal = false; + + // first profile set as global + if (index == ents.length() - 1) { + needGlobal = true; + tagOut = "g-" + Int2String(ent->id); + } + + if (needGlobal) { + if (status->globalProfiles.contains(ent->id)) { + continue; + } + status->globalProfiles += ent->id; + } + + if (index > 0) { + // chain rules: past + if (!ents[index - 1]->bean->NeedExternal()) { + auto replaced = status->outbounds.last().toObject(); + replaced["proxySettings"] = QJsonObject{ + {"tag", tagOut}, + {"transportLayer", true}, + }; + status->outbounds.removeLast(); + status->outbounds += replaced; + } else { + status->routingRules += QJsonObject{ + {"type", "field"}, + {"inboundTag", QJsonArray{pastTag + "-mapping"}}, + {"outboundTag", tagOut}, + }; + } + } else { + // index == 0 means last profile in chain / not chain + chainTag = tagOut; + status->result->outboundStat = ent->traffic_data; + } + + // chain rules: this + auto mapping_port = MkPort(); + if (ent->bean->NeedExternal()) { + status->inbounds += QJsonObject{ + {"protocol", "dokodemo-door"}, + {"tag", tagOut + "-mapping"}, + {"listen", "127.0.0.1"}, + {"port", mapping_port}, + {"settings", QJsonObject{ // to + {"address", ent->bean->serverAddress}, + {"port", ent->bean->serverPort}, + {"network", "tcp,udp"}, + }}, + }; + // no chain rule and not outbound, so need to set to direct + if (index == ents.length() - 1) { + status->routingRules += QJsonObject{ + {"type", "field"}, + {"inboundTag", QJsonArray{tagOut + "-mapping"}}, + {"outboundTag", "direct"}, + }; + } + } + + // Outbound + + QJsonObject outbound; + fmt::CoreObjOutboundBuildResult coreR; + fmt::ExternalBuildResult extR; + + if (ent->bean->NeedExternal()) { + auto ext_socks_port = MkPort(); + extR = ent->bean->BuildExternal(mapping_port, ext_socks_port); + if (!extR.error.isEmpty()) { // rejected + status->result->error = extR.error; + return ""; + } + + // SOCKS OUTBOUND + outbound["protocol"] = "socks"; + QJsonObject settings; + QJsonArray servers; + QJsonObject server; + server["address"] = "127.0.0.1"; + server["port"] = ext_socks_port; + servers.push_back(server); + settings["servers"] = servers; + outbound["settings"] = settings; + + // EXTERNAL PROCESS + auto extC = new sys::ExternalProcess(ent->bean->DisplayType(), + extR.program, extR.arguments, extR.env); + status->result->ext += extC; + } else { + coreR = ent->bean->BuildCoreObj(); + if (!coreR.error.isEmpty()) { // rejected + status->result->error = coreR.error; + return ""; + } + outbound = coreR.outbound; + } + + // outbound misc + outbound["tag"] = tagOut; + outbound["domainStrategy"] = dataStore->outbound_domain_strategy; + ent->traffic_data->id = ent->id; + ent->traffic_data->tag = tagOut.toStdString(); + status->result->outboundStats += ent->traffic_data; + + // apply mux + if (dataStore->mux_cool > 0 && !muxApplied) { + // TODO refactor mux settings + if (ent->type == "vmess" || ent->type == "trojan" || ent->type == "vless") { + auto muxObj = QJsonObject{ + {"enabled", true}, + {"concurrency", dataStore->mux_cool}, + }; + auto stream = GetStreamSettings(ent->bean); + if (stream != nullptr && !stream->packet_encoding.isEmpty()) { + muxObj["packetEncoding"] = stream->packet_encoding; + } + outbound["mux"] = muxObj; + muxApplied = true; + } + } + + // apply custom outbound settings + auto custom_item = ent->bean->_get("custom"); + if (custom_item != nullptr) { + ApplyCustomOutboundJsonSettings(QString2QJsonObject(*((QString *) custom_item->ptr)), outbound); + } + + // Bypass Lookup for the first profile + if (index == ents.length() - 1 && !IsIpAddress(ent->bean->serverAddress)) { + if (dataStore->enhance_resolve_server_domain) { + status->result->tryDomains += ent->bean->serverAddress; + } else { + status->domainListDNSDirect += "full:" + ent->bean->serverAddress; + } + } + + status->outbounds += outbound; + pastTag = tagOut; + index++; + } + + // this is a chain + if (ents.length() > 1) { + // Chain ent traffic stat + status->currentEnt->traffic_data->id = status->currentEnt->id; + status->currentEnt->traffic_data->tag = chainTag.toStdString(); + status->result->outboundStats += status->currentEnt->traffic_data; + } + + return chainTag; + } + +} \ No newline at end of file diff --git a/db/ConfigBuilder.hpp b/db/ConfigBuilder.hpp new file mode 100644 index 0000000..ff86ff6 --- /dev/null +++ b/db/ConfigBuilder.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "ProxyEntity.hpp" +#include "sys/ExternalProcess.hpp" + +namespace NekoRay { + class BuildConfigResult { + public: + QString error; + QJsonObject coreConfig; + QStringList tryDomains; + + QList> outboundStats; // all, but not including "bypass" "block" + QSharedPointer outboundStat; // main + + QList ext; + }; + + class BuildConfigStatus { + public: + QSharedPointer result; + + QJsonArray domainListDNSRemote; + QJsonArray domainListDNSDirect; + QJsonArray domainListRemote; + QJsonArray domainListDirect; + QJsonArray ipListRemote; + QJsonArray ipListDirect; + + QJsonArray domainListBlock; + QJsonArray ipListBlock; + + QJsonArray routingRules; + QJsonObject hosts; + + QJsonArray inbounds; + QJsonArray outbounds; + + QList globalProfiles; + + ProxyEntity *currentEnt; + }; + + QSharedPointer BuildConfig(const QSharedPointer &ent, bool forTest); + + QString BuildChain(int chainId, const QList> &ents, + const QSharedPointer &status); +} diff --git a/db/Database.cpp b/db/Database.cpp new file mode 100644 index 0000000..07b0fb4 --- /dev/null +++ b/db/Database.cpp @@ -0,0 +1,260 @@ +#include "Database.hpp" + +#include "fmt/includes.h" + +#include + +namespace NekoRay { + + ProfileManager *profileManager = new ProfileManager(); + + ProfileManager::ProfileManager() : JsonStore("groups/pm.json") { + _hooks_after_load.push_back([=]() { LoadManager(); }); + _hooks_before_save.push_back([=]() { SaveManager(); }); + _add(new configItem("profiles", &_profiles, itemType::integerList)); + _add(new configItem("groups", &_groups, itemType::integerList)); + } + + void ProfileManager::LoadManager() { + for (auto id: _profiles) { + profiles[id] = LoadProxyEntity(QString("profiles/%1.json").arg(id)); + } + for (auto id: _groups) { + groups[id] = LoadGroup(QString("groups/%1.json").arg(id)); + } + } + + void ProfileManager::SaveManager() { + } + + QSharedPointer ProfileManager::LoadProxyEntity(const QString &jsonPath) { + // Load type + ProxyEntity ent0(nullptr, nullptr); + ent0.fn = jsonPath; + auto validJson = ent0.Load(); + auto type = ent0.type; + + // Load content + QSharedPointer ent; + bool validType = validJson; + + if (validType) { + ent = NewProxyEntity(type); + validType = ent->bean->version != -114514; + } + + if (validType) { + // 加载前设置好 fn + ent->load_control_force = true; + ent->fn = jsonPath; + ent->Load(); + return ent; + } else { + // 返回一个假的? + ent->bean->name = "[Load Error]"; + return ent; + } + } + + // 新建的不给 fn 和 id + + QSharedPointer ProfileManager::NewProxyEntity(const QString &type) { + fmt::AbstractBean *bean; + + if (type == "socks") { + bean = new fmt::SocksHttpBean(NekoRay::fmt::SocksHttpBean::type_Socks5); + } else if (type == "http") { + bean = new fmt::SocksHttpBean(NekoRay::fmt::SocksHttpBean::type_HTTP); + } else if (type == "shadowsocks") { + bean = new fmt::ShadowSocksBean(); + } else if (type == "chain") { + bean = new fmt::ChainBean(); + } else if (type == "vmess") { + bean = new fmt::VMessBean(); + } else if (type == "trojan") { + bean = new fmt::TrojanVLESSBean(fmt::TrojanVLESSBean::proxy_Trojan); + } else if (type == "vless") { + bean = new fmt::TrojanVLESSBean(fmt::TrojanVLESSBean::proxy_VLESS); + } else if (type == "naive") { + bean = new fmt::NaiveBean(); + } else if (type == "custom") { + bean = new fmt::CustomBean(); + } else { + bean = new fmt::AbstractBean(-114514); + } + + auto ent = QSharedPointer(new ProxyEntity(bean, type)); + return ent; + } + + QSharedPointer ProfileManager::NewGroup() { + auto ent = QSharedPointer(new Group()); + return ent; + } + + // ProxyEntity + + ProxyEntity::ProxyEntity(fmt::AbstractBean *bean, QString _type) { + type = std::move(_type); + _add(new configItem("type", &type, itemType::string)); + _add(new configItem("id", &id, itemType::integer)); + _add(new configItem("gid", &gid, itemType::integer)); + + // 可以不关联 bean,只加载 ProxyEntity 的信息 + if (bean != nullptr) { + this->bean = QSharedPointer(bean); + // 有虚函数就要在这里 dynamic_cast + _add(new configItem("bean", dynamic_cast(bean), itemType::jsonStore)); + _add(new configItem("traffic", dynamic_cast(traffic_data.get()), itemType::jsonStore)); + } + }; + + QString ProxyEntity::DisplayLatency() const { + if (latency < 0) { + return QObject::tr("Unavailable"); + } else if (latency > 0) { + return QString("%1 ms").arg(latency); + } else { + return ""; + } + } + + // Profile + + int ProfileManager::NewProfileID() const { + if (profiles.empty()) { return 0; } else { return profiles.lastKey() + 1; } + } + + bool ProfileManager::AddProfile(const QSharedPointer &ent, int gid) { + if (ent->id >= 0) { + return false; + } + + ent->gid = gid < 0 ? dataStore->current_group : gid; + ent->id = NewProfileID(); + profiles[ent->id] = ent; + _profiles.push_back(ent->id); + Save(); + + ent->fn = QString("profiles/%1.json").arg(ent->id); + ent->Save(); + return true; + } + + void ProfileManager::DeleteProfile(int id) { + if (id < 0) return; + if (dataStore->started_id == id) return; + profiles.remove(id); + _profiles.removeAll(id); + Save(); + QFile(QString("profiles/%1.json").arg(id)).remove(); + } + + void ProfileManager::MoveProfile(const QSharedPointer &ent, int gid) { + if (gid == ent->gid || gid < 0) return; + auto oldGroup = GetGroup(ent->gid); + if (oldGroup != nullptr && !oldGroup->order.isEmpty()) { + oldGroup->order.removeAll(ent->id); + oldGroup->Save(); + } + auto newGroup = GetGroup(gid); + if (newGroup != nullptr && !newGroup->order.isEmpty()) { + newGroup->order.push_back(ent->id); + newGroup->Save(); + } + ent->gid = gid; + ent->Save(); + } + + QSharedPointer ProfileManager::GetProfile(int id) { + if (profiles.contains(id)) { + return profiles[id]; + } + return nullptr; + } + + //Group + + Group::Group() { + _add(new configItem("id", &id, itemType::integer)); + _add(new configItem("archive", &archive, itemType::boolean)); + _add(new configItem("name", &name, itemType::string)); + _add(new configItem("order", &order, itemType::integerList)); + _add(new configItem("url", &url, itemType::string)); + _add(new configItem("info", &info, itemType::string)); + } + + QSharedPointer ProfileManager::LoadGroup(const QString &jsonPath) { + QSharedPointer ent = QSharedPointer(new Group()); + ent->fn = jsonPath; + ent->Load(); + return ent; + } + + int ProfileManager::NewGroupID() const { + if (groups.empty()) { return 0; } else { return groups.lastKey() + 1; } + } + + bool ProfileManager::AddGroup(const QSharedPointer &ent) { + if (ent->id >= 0) { + return false; + } + + ent->id = NewGroupID(); + groups[ent->id] = ent; + _groups.push_back(ent->id); + Save(); + + ent->fn = QString("groups/%1.json").arg(ent->id); + ent->Save(); + return true; + } + + void ProfileManager::DeleteGroup(int gid) { + if (groups.count() == 1) return; + QList toDelete; + for (const auto &profile: profiles) { + if (profile->gid == gid) toDelete += profile->id; // map访问中,不能操作 + } + for (const auto &id: toDelete) { + DeleteProfile(id); + } + groups.remove(gid); + _groups.removeAll(gid); + Save(); + QFile(QString("groups/%1.json").arg(gid)).remove(); + } + + QSharedPointer ProfileManager::GetGroup(int id) { + if (groups.contains(id)) { + return groups[id]; + } + return nullptr; + } + + QSharedPointer ProfileManager::CurrentGroup() { + return GetGroup(NekoRay::dataStore->current_group); + } + + QList> Group::Profiles() const { + QList> ret; + for (const auto &ent: profileManager->profiles) { + if (id == ent->gid) ret += ent; + } + return ret; + } + + QList> Group::ProfilesWithOrder() const { + if (order.isEmpty()) { + return Profiles(); + } else { + QList> ret; + for (auto _id: order) { + auto ent = profileManager->GetProfile(_id); + if (ent != nullptr) ret += ent; + } + return ret; + } + } + +} \ No newline at end of file diff --git a/db/Database.hpp b/db/Database.hpp new file mode 100644 index 0000000..64d8336 --- /dev/null +++ b/db/Database.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "main/NekoRay.hpp" +#include "ProxyEntity.hpp" +#include "Group.hpp" + +namespace NekoRay { + class ProfileManager : public JsonStore { + public: + QMap> profiles; + QMap> groups; + + // JSON + QList _profiles; + QList _groups; // with order + + ProfileManager(); + + [[nodiscard]] static QSharedPointer NewProxyEntity(const QString &type); + + [[nodiscard]] static QSharedPointer NewGroup(); + + bool AddProfile(const QSharedPointer &ent, int gid = -1); + + void DeleteProfile(int id); + + void MoveProfile(const QSharedPointer &ent, int gid); + + QSharedPointer GetProfile(int id); + + bool AddGroup(const QSharedPointer &ent); + + void DeleteGroup(int gid); + + QSharedPointer GetGroup(int id); + + QSharedPointer CurrentGroup(); + + private: + void LoadManager(); + + void SaveManager(); + + [[nodiscard]] int NewProfileID() const; + + [[nodiscard]] int NewGroupID() const; + + static QSharedPointer LoadProxyEntity(const QString &jsonPath); + + static QSharedPointer LoadGroup(const QString &jsonPath); + }; + + extern ProfileManager *profileManager; +} diff --git a/db/Group.hpp b/db/Group.hpp new file mode 100644 index 0000000..7de92c3 --- /dev/null +++ b/db/Group.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "main/NekoRay.hpp" +#include "ProxyEntity.hpp" + +namespace NekoRay { + class Group : public JsonStore { + public: + int id = -1; + bool archive = false; + QString name = ""; + QList order; + QString url = ""; + QString info = ""; + + Group(); + + // 按 id 顺序 + [[nodiscard]] QList> Profiles() const; + + // 按 显示 顺序 + [[nodiscard]] QList> ProfilesWithOrder() const; + }; +} diff --git a/db/ProfileFilter.cpp b/db/ProfileFilter.cpp new file mode 100644 index 0000000..84383c9 --- /dev/null +++ b/db/ProfileFilter.cpp @@ -0,0 +1,77 @@ +#include "ProfileFilter.hpp" + +namespace NekoRay { + void ProfileFilter::Uniq(const QList> &in, + QList> &out, + bool by_address, bool keep_last) { + QMap> hashMap; + + for (const auto &ent: in) { + QString key = by_address ? (ent->bean->DisplayAddress() + ent->bean->DisplayType()) + : ent->bean->ToJsonBytes(); + if (hashMap.contains(key)) { + if (keep_last) { + out.removeAll(hashMap[key]); + hashMap[key] = ent; + out += ent; + } + } else { + hashMap[key] = ent; + out += ent; + } + } + } + + void + ProfileFilter::Common(const QList> &src, + const QList> &dst, + QList> &out, + bool by_address, bool keep_last) { + QMap> hashMap; + + for (const auto &ent: src) { + QString key = by_address ? (ent->bean->DisplayAddress() + ent->bean->DisplayType()) + : ent->bean->ToJsonBytes(); + hashMap[key] = ent; + } + for (const auto &ent: dst) { + QString key = by_address ? (ent->bean->DisplayAddress() + ent->bean->DisplayType()) + : ent->bean->ToJsonBytes(); + if (hashMap.contains(key)) { + if (keep_last) { + out += ent; + } else { + out += hashMap[key]; + } + } + } + } + + void ProfileFilter::OnlyInSrc(const QList> &src, + const QList> &dst, + QList> &out, + bool by_address) { + QMap hashMap; + + for (const auto &ent: dst) { + QString key = by_address ? (ent->bean->DisplayAddress() + ent->bean->DisplayType()) + : ent->bean->ToJsonBytes(); + hashMap[key] = true; + } + for (const auto &ent: src) { + QString key = by_address ? (ent->bean->DisplayAddress() + ent->bean->DisplayType()) + : ent->bean->ToJsonBytes(); + if (!hashMap.contains(key)) out += ent; + } + } + + void + ProfileFilter::OnlyInSrc_ByPointer(const QList> &src, + const QList> &dst, + QList> &out) { + for (const auto &ent: src) { + if (!dst.contains(ent)) out += ent; + } + } + +} \ No newline at end of file diff --git a/db/ProfileFilter.hpp b/db/ProfileFilter.hpp new file mode 100644 index 0000000..d860a27 --- /dev/null +++ b/db/ProfileFilter.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "ProxyEntity.hpp" + +namespace NekoRay { + class ProfileFilter { + public: + static void Uniq( + const QList> &in, + QList> &out, + bool by_address = false, //def by bean + bool keep_last = false //def keep first + ); + + static void Common( + const QList> &src, + const QList> &dst, + QList> &out, + bool by_address = false, //def by bean + bool keep_last = false //def keep first + ); + + static void OnlyInSrc( + const QList> &src, + const QList> &dst, + QList> &out, + bool by_address = false //def by bean + ); + + static void OnlyInSrc_ByPointer( + const QList> &src, + const QList> &dst, + QList> &out + ); + }; +} diff --git a/db/ProxyEntity.hpp b/db/ProxyEntity.hpp new file mode 100644 index 0000000..ecb4c63 --- /dev/null +++ b/db/ProxyEntity.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "main/NekoRay.hpp" +#include "TrafficData.hpp" +#include "fmt/AbstractBean.hpp" + +namespace NekoRay { + namespace fmt { + class SocksHttpBean; + + class ShadowSocksBean; + + class VMessBean; + + class TrojanVLESSBean; + + class NaiveBean; + + class CustomBean; + + class ChainBean; + }; + + class ProxyEntity : public JsonStore { + public: + QString type; + + int id = -1; + int gid = 0; + QSharedPointer bean; + QSharedPointer traffic_data = QSharedPointer( + new traffic::TrafficData("")); + + // Cache + int latency = 0; + QString full_test_report; + + ProxyEntity(fmt::AbstractBean *bean, QString _type); + + [[nodiscard]] QString DisplayLatency() const; + + [[nodiscard]] fmt::ChainBean *ChainBean() const { + return (fmt::ChainBean *) bean.get(); + }; + + [[nodiscard]] fmt::SocksHttpBean *SocksHTTPBean() const { + return (fmt::SocksHttpBean *) bean.get(); + }; + + [[nodiscard]] fmt::ShadowSocksBean *ShadowSocksBean() const { + return (fmt::ShadowSocksBean *) bean.get(); + }; + + [[nodiscard]] fmt::VMessBean *VMessBean() const { + return (fmt::VMessBean *) bean.get(); + }; + + [[nodiscard]] fmt::TrojanVLESSBean *TrojanVLESSBean() const { + return (fmt::TrojanVLESSBean *) bean.get(); + }; + + [[nodiscard]] fmt::NaiveBean *NaiveBean() const { + return (fmt::NaiveBean *) bean.get(); + }; + + [[nodiscard]] fmt::CustomBean *CustomBean() const { + return (fmt::CustomBean *) bean.get(); + }; + + }; +} diff --git a/db/TrafficData.hpp b/db/TrafficData.hpp new file mode 100644 index 0000000..2b3b9ed --- /dev/null +++ b/db/TrafficData.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "main/NekoRay.hpp" + +namespace NekoRay::traffic { + class TrafficData : public JsonStore { + public: + int id = -1; // ent id + std::string tag; + + long long downlink = 0; + long long uplink = 0; + long long downlink_rate = 0; + long long uplink_rate = 0; + + explicit TrafficData(std::string tag) { + this->tag = std::move(tag); + _add(new configItem("dl", &downlink, itemType::integer64)); + _add(new configItem("ul", &uplink, itemType::integer64)); + }; + + void Reset() { + downlink = 0; + uplink = 0; + downlink_rate = 0; + uplink_rate = 0; + } + + [[nodiscard]] QString DisplaySpeed() const { + return QString("%1↑ %2↓").arg(ReadableSize(uplink_rate), ReadableSize(downlink_rate)); + } + + [[nodiscard]] QString DisplayTraffic() const { + if (downlink + uplink == 0) return ""; + return QString("%1↑ %2↓").arg(ReadableSize(uplink), ReadableSize(downlink)); + } + }; +} diff --git a/db/TrafficLooper.cpp b/db/TrafficLooper.cpp new file mode 100644 index 0000000..c5baf05 --- /dev/null +++ b/db/TrafficLooper.cpp @@ -0,0 +1,120 @@ +#include "TrafficLooper.hpp" + +#include "rpc/gRPC.h" +#include "ui/mainwindow.h" + +#include + +namespace NekoRay::traffic { + + TrafficLooper *trafficLooper = new TrafficLooper; + + std::unique_ptr TrafficLooper::update_stats(TrafficData *item) { +#ifndef NKR_NO_GRPC + auto uplink = NekoRay::rpc::defaultClient->QueryStats(item->tag, "uplink"); + auto downlink = NekoRay::rpc::defaultClient->QueryStats(item->tag, "downlink"); + + item->downlink += downlink; + item->uplink += uplink; + + //? + item->downlink_rate = downlink * 1000 / dataStore->traffic_loop_interval; + item->uplink_rate = uplink * 1000 / dataStore->traffic_loop_interval; + + // return diff + auto ret = std::make_unique(item->tag); + ret->downlink = downlink; + ret->uplink = uplink; + ret->downlink_rate = item->downlink_rate; + ret->uplink_rate = item->uplink_rate; + return ret; +#endif + return nullptr; + } + + QJsonArray TrafficLooper::get_connection_list() { +#ifndef NKR_NO_GRPC + auto str = NekoRay::rpc::defaultClient->ListV2rayConnections(); + QJsonDocument jsonDocument = QJsonDocument::fromJson(str.c_str()); + return jsonDocument.array(); +#else + return QJsonArray{}; +#endif + } + + void TrafficLooper::update_all() { + std::map> updated; // tag to diff + for (const auto &item: items) { + auto data = item.get(); + auto diff = std::move(updated[data->tag]); + // 避免重复查询一个 outbound tag + if (diff == nullptr) { + diff = update_stats(data); + updated[data->tag] = std::move(diff); + } else { + data->uplink += diff->uplink; + data->downlink += diff->downlink; + data->uplink_rate = diff->uplink_rate; + data->downlink_rate = diff->downlink_rate; + } + } + update_stats(bypass); + } + + [[noreturn]] void TrafficLooper::loop() { + while (true) { + auto sleep_ms = dataStore->traffic_loop_interval; + auto user_disabled = sleep_ms == 0; + if (sleep_ms < 500 || sleep_ms > 2000) sleep_ms = 1000; + QThread::msleep(sleep_ms); + if (user_disabled) continue; + + if (!loop_enabled) { + // 停止 + if (looping) { + looping = false; + runOnUiThread([=] { + auto m = GetMainWindow(); + m->refresh_status("STOP"); + }); + } + continue; + } else { + //开始 + if (!looping) { + looping = true; + } + } + + // do update + loop_mutex.lock(); + + update_all(); + + // do conn list update + QJsonArray conn_list; + if (dataStore->connection_statistics) { + conn_list = get_connection_list(); + } + + loop_mutex.unlock(); + + // post to UI + runOnUiThread([=] { + auto m = GetMainWindow(); + if (proxy != nullptr) { + m->refresh_status( + QObject::tr("Proxy: %1\nDirect: %2").arg(proxy->DisplaySpeed(), bypass->DisplaySpeed())); + } + for (const auto &item: items) { + if (item->id < 0) continue; + m->refresh_proxy_list(item->id); + } + if (dataStore->connection_statistics) { + m->refresh_connection_list(conn_list); + } + }); + } + } + +} diff --git a/db/TrafficLooper.hpp b/db/TrafficLooper.hpp new file mode 100644 index 0000000..0002171 --- /dev/null +++ b/db/TrafficLooper.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +#include "TrafficData.hpp" + +namespace NekoRay::traffic { + class TrafficLooper { + public: + bool loop_enabled = false; + bool looping = false; + QMutex loop_mutex; + + QList> items; + TrafficData *bypass = new TrafficData("bypass"); + TrafficData *proxy = nullptr; + + static std::unique_ptr update_stats(TrafficData *item); + + static QJsonArray get_connection_list(); + + void update_all(); + + [[noreturn]] void loop(); + }; + + extern TrafficLooper *trafficLooper; +} + diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..02f9235 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1 @@ +tun2socks diff --git a/examples/build-alpine.md b/examples/build-alpine.md new file mode 100644 index 0000000..0856d66 --- /dev/null +++ b/examples/build-alpine.md @@ -0,0 +1,5 @@ +alpine 3.16 + +all use package + +apk add git cmake g++ ninja zxing-cpp-dev yaml-cpp-dev grpc-dev protobuf-dev qt5-qtbase-dev qt5-qtsvg-dev qt5-qttools-dev qt5-qtx11extras-dev c-ares-dev re2-dev diff --git a/examples/netns-root.sh b/examples/netns-root.sh new file mode 100755 index 0000000..2c78086 --- /dev/null +++ b/examples/netns-root.sh @@ -0,0 +1,30 @@ +#!/bin/sh +set -e +set -x + +if [ "$EUID" -ne 0 ]; then + echo "Please run as root" + exit +fi + +# add netns +ip netns add nekoray +# ip netns exec nekoray readlink /proc/self/ns/net + +# add lo: lo is not shared +ip -n nekoray addr add 127.0.0.1/8 dev lo +ip -n nekoray link set dev lo up + +# add tun +ip -n nekoray tuntap add tun0 user $USERID mode tun +ip -n nekoray addr add 26.0.0.1/30 dev tun0 +ip -n nekoray link set dev tun0 up +ip -n nekoray route add default dev tun0 + +# set veth to use the socks port +ip link add dev nekoray-ve1 type veth peer name nekoray-ve2 +ip addr add 26.1.0.1/30 dev nekoray-ve1 +ip link set nekoray-ve1 up +ip link set nekoray-ve2 netns nekoray +ip -n nekoray addr add 26.1.0.2/30 dev nekoray-ve2 +ip -n nekoray link set nekoray-ve2 up diff --git a/examples/netns.sh b/examples/netns.sh new file mode 100755 index 0000000..8ae51af --- /dev/null +++ b/examples/netns.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -e +set -x + +BASEDIR=$(dirname "$0") + +# netns +[ -f /var/run/netns/nekoray ] || pkexec env USERID=`id -u` sh -c "cd $PWD && $BASEDIR/netns-root.sh" || true + +# run xjasonlyu/tun2socks to provide vpn +firejail --noprofile --netns=nekoray ./tun2socks -device tun0 -proxy socks5://26.1.0.1:2080 -interface nekoray-ve2 -drop-multicast + +# use "firejail --noprofile --netns=nekoray ..." to run your program in VPN diff --git a/examples/readme.txt b/examples/readme.txt new file mode 100644 index 0000000..0af9227 --- /dev/null +++ b/examples/readme.txt @@ -0,0 +1,9 @@ +Linux Only + +此处为配置 VPN 的脚本,仅供参考,使用时要按实际情况替换某些参数(如 socks 端口) + +vpn.sh 配置全局 VPN +ctrl-c 退出后自动删除 VPN + +vpn-netns.sh 配置 netns +分应用代理,用法参考脚本内容 diff --git a/examples/set-cap.sh b/examples/set-cap.sh new file mode 100755 index 0000000..195f26e --- /dev/null +++ b/examples/set-cap.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -e +set -x + +if [ "$EUID" -ne 0 ] + then echo "Please run as root" + exit +fi + +killall nekoray_core || true +cp nekoray_core /opt/nekoray_core +cp geo* /opt/ +setcap cap_net_admin+ep /opt/nekoray_core +ln -sf /opt/nekoray_core nekoray_core_cap diff --git a/examples/sing-box-vpn.json b/examples/sing-box-vpn.json new file mode 100644 index 0000000..20ee12f --- /dev/null +++ b/examples/sing-box-vpn.json @@ -0,0 +1,43 @@ +{ + "dns": { + "servers": [], + "rules": [], + "strategy": "ipv4_only" + }, + "inbounds": [ + { + "type": "tun", + "interface_name": "nekoray-tun", + "inet4_address": "172.19.0.1/30", + "auto_route": true, + "sniff": false + } + ], + "outbounds": [ + { + "type": "socks", + "tag": "nekoray-socks", + "server": "127.0.0.1", + "server_port": %PORT% + }, + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + "network": "udp", + "port": [ + 135, + 137, + 138, + 139, + 5353 + ], + "outbound": "block" + } + ] + } +} \ No newline at end of file diff --git a/examples/vpn-run-root.sh b/examples/vpn-run-root.sh new file mode 100755 index 0000000..dcb9033 --- /dev/null +++ b/examples/vpn-run-root.sh @@ -0,0 +1,71 @@ +#!/bin/sh +set -e +set -x + +if [ "$EUID" -ne 0 ]; then + echo "Please run as root" + exit +fi + +[ -z $PORT ] && echo "Please set env PORT" && exit +[ -z $TABLE_FWMARK ] && echo "Please set env TABLE_FWMARK" && exit +[ -z $TUN_NAME ] && echo "Please set env TUN_NAME" && exit +[ -z $USER_ID ] && echo "Please set env USER_ID" && exit +command -v pkill >/dev/null 2>&1 || exit + +BASEDIR=$(dirname "$0") +cd $BASEDIR + +start() { + # add tun (TODO the ip must be the same as matsuri) + ip tuntap add $TUN_NAME mode tun user $USER_ID || return + ip addr add 172.19.0.1/30 dev $TUN_NAME || return + ip link set dev $TUN_NAME up || return + + # set ipv4 rule + ip rule add table $TABLE_FWMARK || return + ip route add table $TABLE_FWMARK default dev $TUN_NAME || return + + # set ipv6 unreachable + ip -6 rule add table $TABLE_FWMARK || return + ip -6 route add table $TABLE_FWMARK unreachable default || return + + # set bypass: fwmark + ip rule add fwmark $TABLE_FWMARK table main || return + ip -6 rule add fwmark $TABLE_FWMARK table main || return + + # set bypass: LAN + for local in $BYPASS_IPS; do + ip rule add to $local table main + done + + if [ ! -z $USE_NEKORAY ]; then + "./nekoray_core" tool protect --protect-listen-path "$PROTECT_LISTEN_PATH" --protect-fwmark $TABLE_FWMARK + else + if [ -z "$PROTECT_LISTEN_PATH" ]; then + "./tun2socks" -device $TUN_NAME -proxy socks5://127.0.0.1:$PORT -interface lo + else + "./tun2socks" -device $TUN_NAME -proxy socks5://127.0.0.1:$PORT -interface lo --protect-listen-path "$PROTECT_LISTEN_PATH" --protect-fwmark $TABLE_FWMARK + rm "$PROTECT_LISTEN_PATH" + fi + fi +} + +stop() { + for local in $BYPASS_IPS; do + ip rule del to $local table main + done + ip rule del table $TABLE_FWMARK + ip rule del fwmark $TABLE_FWMARK + ip route del table $TABLE_FWMARK default + ip -6 rule del table $TABLE_FWMARK + ip -6 rule del fwmark $TABLE_FWMARK + ip -6 route del table $TABLE_FWMARK default + ip link del $TUN_NAME +} + +if [ "$1" != "stop" ]; then + start || true +fi + +stop || true diff --git a/fmt/AbstractBean.cpp b/fmt/AbstractBean.cpp new file mode 100644 index 0000000..047ce90 --- /dev/null +++ b/fmt/AbstractBean.cpp @@ -0,0 +1,36 @@ +#include "AbstractBean.hpp" + +namespace NekoRay::fmt { + AbstractBean::AbstractBean(int version) { + this->version = version; + _add(new configItem("_v", &this->version, itemType::integer)); + _add(new configItem("name", &name, itemType::string)); + _add(new configItem("addr", &serverAddress, itemType::string)); + _add(new configItem("port", &serverPort, itemType::integer)); + } + + QString AbstractBean::ToNekorayShareLink(const QString &type) { + auto b = ToJson(); + QUrl url; + url.setScheme("nekoray"); + url.setHost(type); + url.setFragment(QJsonObject2QString(b, true) + .toUtf8().toBase64(QByteArray::Base64UrlEncoding)); + return url.toString(); + } + + QString AbstractBean::DisplayAddress() { + return ::DisplayAddress(serverAddress, serverPort); + } + + QString AbstractBean::DisplayName() { + if (name.isEmpty()) { + return DisplayAddress(); + } + return name; + } + + QString AbstractBean::DisplayTypeAndName() { + return QString(" [%1] %2").arg(DisplayType(), DisplayName()); + } +} \ No newline at end of file diff --git a/fmt/AbstractBean.hpp b/fmt/AbstractBean.hpp new file mode 100644 index 0000000..c9373b4 --- /dev/null +++ b/fmt/AbstractBean.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "main/NekoRay.hpp" + +namespace NekoRay::fmt { + + struct CoreObjOutboundBuildResult { + public: + QJsonObject outbound; + QString error; + }; + + struct ExternalBuildResult { + public: + QString program; + QStringList env; + QStringList arguments; + QString error; + }; + + class AbstractBean : public JsonStore { + public: + int version; + QString name = ""; + QString serverAddress = "127.0.0.1"; + int serverPort = 1080; + + explicit AbstractBean(int version); + + QString ToNekorayShareLink(const QString &type); + + [[nodiscard]] virtual QString DisplayAddress(); + + [[nodiscard]] virtual QString DisplayName(); + + virtual QString DisplayType() { return {}; }; + + virtual QString DisplayTypeAndName(); + + virtual bool NeedExternal() { return false; }; + + virtual CoreObjOutboundBuildResult BuildCoreObj() { return {}; }; + + virtual ExternalBuildResult BuildExternal(int mapping_port, int socks_port) { return {}; }; + + virtual QString ToShareLink() { return {}; }; + + virtual QString InsecureHint() { return {}; }; + + }; + + QString DisplayInsecureHint(const QSharedPointer &); + +} diff --git a/fmt/Bean2CoreObj.cpp b/fmt/Bean2CoreObj.cpp new file mode 100644 index 0000000..956c5a1 --- /dev/null +++ b/fmt/Bean2CoreObj.cpp @@ -0,0 +1,175 @@ +#include "db/ProxyEntity.hpp" +#include "fmt/includes.h" + +#define MAKE_SETTINGS_STREAM_SETTINGS \ +if (!stream->packet_encoding.isEmpty()) settings["packetEncoding"] = stream->packet_encoding; \ +outbound["settings"] = settings; \ +auto streamSettings = stream->BuildStreamSettings(); \ +outbound["streamSettings"] = streamSettings; + +namespace NekoRay::fmt { + + QJsonObject V2rayStreamSettings::BuildStreamSettings() { + QJsonObject streamSettings{ + {"network", network}, + {"security", security}, + }; + + if (network == "ws") { + QJsonObject ws; + if (!path.isEmpty()) ws["path"] = path; + if (!host.isEmpty()) ws["headers"] = QJsonObject{{"Host", host}}; + streamSettings["wsSettings"] = ws; + } else if (network == "h2") { + QJsonObject h2; + if (!path.isEmpty()) h2["path"] = path; + if (!host.isEmpty()) h2["host"] = QList2QJsonArray(host.split(",")); + streamSettings["httpSettings"] = h2; + } else if (network == "grpc") { + QJsonObject grpc; + if (!path.isEmpty()) grpc["serviceName"] = path; + streamSettings["grpcSettings"] = grpc; + } + + if (security == "tls") { + QJsonObject tls; + if (!sni.isEmpty()) tls["serverName"] = sni; + if (allow_insecure || dataStore->skip_cert) tls["allowInsecure"] = true; + if (!certificate.isEmpty()) + tls["certificates"] = QJsonArray{ + QJsonObject{ + {"certificate", certificate}, + }, + }; + streamSettings["tlsSettings"] = tls; + } + + return streamSettings; + } + + CoreObjOutboundBuildResult SocksHttpBean::BuildCoreObj() { + CoreObjOutboundBuildResult result; + + QJsonObject outbound; + outbound["protocol"] = socks_http_type == type_HTTP ? "http" : "socks"; + + QJsonObject settings; + QJsonArray servers; + QJsonObject server; + + server["address"] = serverAddress; + server["port"] = serverPort; + + QJsonArray users; + QJsonObject user; + user["user"] = username; + user["pass"] = password; + users.push_back(user); + if (!username.isEmpty() && !password.isEmpty()) server["users"] = users; + + servers.push_back(server); + settings["servers"] = servers; + + MAKE_SETTINGS_STREAM_SETTINGS + + result.outbound = outbound; + return result; + } + + CoreObjOutboundBuildResult ShadowSocksBean::BuildCoreObj() { + CoreObjOutboundBuildResult result; + + QJsonObject outbound; + outbound["protocol"] = "shadowsocks"; + + QJsonObject settings; + QJsonArray servers; + QJsonObject server; + + server["address"] = serverAddress; + server["port"] = serverPort; + server["method"] = method; + server["password"] = password; + + servers.push_back(server); + settings["servers"] = servers; + + if (!plugin.isEmpty()) { + settings["plugin"] = SubStrBefore(plugin, ";"); + settings["pluginOpts"] = SubStrAfter(plugin, ";"); + } + + MAKE_SETTINGS_STREAM_SETTINGS + + result.outbound = outbound; + return result; + } + + CoreObjOutboundBuildResult VMessBean::BuildCoreObj() { + CoreObjOutboundBuildResult result; + QJsonObject outbound{ + {"protocol", "vmess"}, + }; + + QJsonObject settings{ + {"vnext", QJsonArray{ + QJsonObject{ + {"address", serverAddress}, + {"port", serverPort}, + {"users", QJsonArray{ + QJsonObject{ + {"id", uuid}, + {"alterId", aid}, + {"security", security}, + } + }}, + } + }} + }; + + MAKE_SETTINGS_STREAM_SETTINGS + + result.outbound = outbound; + return result; + } + + CoreObjOutboundBuildResult TrojanVLESSBean::BuildCoreObj() { + CoreObjOutboundBuildResult result; + QJsonObject outbound{ + {"protocol", proxy_type == proxy_VLESS ? "vless" : "trojan"}, + }; + + QJsonObject settings; + if (proxy_type == proxy_VLESS) { + settings = QJsonObject{ + {"vnext", QJsonArray{ + QJsonObject{ + {"address", serverAddress}, + {"port", serverPort}, + {"users", QJsonArray{ + QJsonObject{ + {"id", password}, + {"encryption", "none"}, + } + }}, + } + }} + }; + } else { + settings = QJsonObject{ + {"servers", QJsonArray{ + QJsonObject{ + {"address", serverAddress}, + {"port", serverPort}, + {"password", password}, + } + }} + }; + } + + MAKE_SETTINGS_STREAM_SETTINGS + + result.outbound = outbound; + return result; + } +} \ No newline at end of file diff --git a/fmt/Bean2External.cpp b/fmt/Bean2External.cpp new file mode 100644 index 0000000..e7516e7 --- /dev/null +++ b/fmt/Bean2External.cpp @@ -0,0 +1,81 @@ +#include "db/ProxyEntity.hpp" +#include "fmt/includes.h" + +#include +#include +#include + +#define WriteTempFile(fn, data) \ +QDir dir; \ +if (!dir.exists("temp")) dir.mkdir("temp"); \ +QFile f(QString("temp/") + fn); \ +bool ok = f.open(QIODevice::WriteOnly | QIODevice::Truncate); \ +if (ok) { \ +f.write(data); \ +} else { \ +result.error = f.errorString(); \ +} \ +f.close(); \ +auto TempFile = QFileInfo(f).absoluteFilePath(); + +namespace NekoRay::fmt { + ExternalBuildResult NaiveBean::BuildExternal(int mapping_port, int socks_port) { + ExternalBuildResult result{dataStore->extraCore->Get("naive")}; + if (result.program.isEmpty()) { + result.error = QObject::tr("Core not found: %1").arg(DisplayType()); + return result; + } + + auto _serverAddress = sni.isEmpty() ? serverAddress : sni; + + result.arguments += "--log"; + result.arguments += "--listen=socks://127.0.0.1:" + Int2String(socks_port); + result.arguments += "--proxy=" + protocol + "://" + + username + ":" + password + "@" + + _serverAddress + ":" + Int2String(mapping_port); + result.arguments += "--host-resolver-rules=MAP " + _serverAddress + " 127.0.0.1"; + if (insecure_concurrency > 0) result.arguments += "--insecure-concurrency=" + Int2String(insecure_concurrency); + if (!extra_headers.isEmpty()) result.arguments += "--extra-headers=" + extra_headers; + if (!certificate.isEmpty()) { + WriteTempFile("naive_" + GetRandomString(10) + ".crt", certificate.toUtf8()); + result.env += "SSL_CERT_FILE=" + TempFile; + } + + return result; + } + + ExternalBuildResult CustomBean::BuildExternal(int mapping_port, int socks_port) { + ExternalBuildResult result{dataStore->extraCore->Get(core)}; + if (result.program.isEmpty()) { + result.error = QObject::tr("Core not found: %1").arg(DisplayType()); + return result; + } + + result.arguments = command; // TODO split? + + for (int i = 0; i < result.arguments.length(); i++) { + auto arg = result.arguments[i]; + if (arg.contains("%mapping_port%")) { + arg = arg.replace("%mapping_port%", Int2String(mapping_port)); + } else if (arg.contains("%socks_port%")) { + arg = arg.replace("%socks_port%", Int2String(socks_port)); + } else { + continue; + } + result.arguments[i] = arg; + } + + if (!config_simple.trimmed().isEmpty()) { + auto config = config_simple; + config = config.replace("%mapping_port%", Int2String(mapping_port)); + config = config.replace("%socks_port%", Int2String(socks_port)); + + WriteTempFile("custom_cfg_" + GetRandomString(10) + ".tmp", config.toUtf8()); + for (int i = 0; i < result.arguments.count(); i++) { + result.arguments[i] = result.arguments[i].replace("%config%", TempFile); + } + } + + return result; + } +} \ No newline at end of file diff --git a/fmt/Bean2Link.cpp b/fmt/Bean2Link.cpp new file mode 100644 index 0000000..d15a0b2 --- /dev/null +++ b/fmt/Bean2Link.cpp @@ -0,0 +1,94 @@ +#include "db/ProxyEntity.hpp" +#include "fmt/includes.h" + +#include + +namespace NekoRay::fmt { + QString SocksHttpBean::ToShareLink() { + QUrl url; + if (socks_http_type == type_HTTP) { // http + if (stream->security == "tls") { + url.setScheme("https"); + } else { + url.setScheme("http"); + } + } else { + url.setScheme(QString("socks%1").arg(socks_http_type)); + } + if (!name.isEmpty()) url.setFragment(UrlSafe_encode(name)); + if (!username.isEmpty()) url.setUserName(username); + if (!password.isEmpty()) url.setPassword(password); + url.setHost(serverAddress); + url.setPort(serverPort); + return url.toString(); + } + + QString TrojanVLESSBean::ToShareLink() { + QUrl url; + QUrlQuery query; + url.setScheme(proxy_type == proxy_VLESS ? "vless" : "trojan"); + url.setUserName(password); + url.setHost(serverAddress); + url.setPort(serverPort); + if (!name.isEmpty()) url.setFragment(UrlSafe_encode(name)); + if (!stream->sni.isEmpty()) query.addQueryItem("sni", stream->sni); + query.addQueryItem("security", "tls"); + query.addQueryItem("type", stream->network.replace("h2", "http")); + + if (stream->network == "ws" || stream->network == "h2") { + if (!stream->path.isEmpty()) query.addQueryItem("path", stream->path); + if (!stream->host.isEmpty()) query.addQueryItem("host", stream->host); + } else if (stream->network == "grpc") { + if (!stream->path.isEmpty()) query.addQueryItem("serviceName", stream->path); + } + + url.setQuery(query); + return url.toString(); + } + + QString ShadowSocksBean::ToShareLink() { + QUrl url; + url.setScheme("ss"); + auto username = method + ":" + password; + url.setUserName(username.toUtf8().toBase64(QByteArray::Base64Option::Base64UrlEncoding)); + url.setHost(serverAddress); + url.setPort(serverPort); + if (!name.isEmpty()) url.setFragment(UrlSafe_encode(name)); + QUrlQuery q; + if (!plugin.isEmpty()) q.addQueryItem("plugin", plugin); + if (!q.isEmpty()) url.setQuery(q); + return url.toString(); + } + + QString VMessBean::ToShareLink() { + QJsonObject N{ + {"v", 2}, + {"ps", name}, + {"add", serverAddress}, + {"port", serverPort}, + {"id", uuid}, + {"aid", aid}, + {"net", stream->network}, + {"host", stream->host}, + {"path", stream->path}, + {"type", stream->header_type}, + {"scy", security}, + // TODO header type + {"tls", stream->security == "tls" ? "tls" : ""}, + {"sni", stream->sni}, + }; + return "vmess://" + QJsonObject2QString(N, false).toUtf8().toBase64(); + } + + QString NaiveBean::ToShareLink() { + QUrl url; + url.setScheme("https+naive"); + url.setUserName(username); + url.setPassword(password); + url.setHost(serverAddress); + url.setPort(serverPort); + if (!name.isEmpty()) url.setFragment(UrlSafe_encode(name)); + return url.toString(); + } + +} \ No newline at end of file diff --git a/fmt/ChainBean.hpp b/fmt/ChainBean.hpp new file mode 100644 index 0000000..1eef702 --- /dev/null +++ b/fmt/ChainBean.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "main/NekoRay.hpp" + +namespace NekoRay::fmt { + class ChainBean : public AbstractBean { + public: + QList list; // in to out + + ChainBean() : AbstractBean(0) { + _add(new configItem("list", &list, itemType::integerList)); + }; + + QString DisplayType() override { return QObject::tr("Chain Proxy"); }; + + QString DisplayAddress() override { return ""; }; + }; +} diff --git a/fmt/CustomBean.hpp b/fmt/CustomBean.hpp new file mode 100644 index 0000000..e3239d7 --- /dev/null +++ b/fmt/CustomBean.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" + +namespace NekoRay::fmt { + class CustomBean : public AbstractBean { + public: + QString core; + QList command; +// QString config_map; // map: fn to text + QString config_simple; + + CustomBean() : AbstractBean(0) { + _add(new configItem("core", &core, itemType::string)); + _add(new configItem("cmd", &command, itemType::stringList)); +// _add(new configItem("cm", &config_map, itemType::string)); + _add(new configItem("cs", &config_simple, itemType::string)); + }; + + QString DisplayType() override { return core; }; + + bool NeedExternal() override { return true; }; + + ExternalBuildResult BuildExternal(int mapping_port, int socks_port) override; + }; +} \ No newline at end of file diff --git a/fmt/InsecureHint.cpp b/fmt/InsecureHint.cpp new file mode 100644 index 0000000..82703dc --- /dev/null +++ b/fmt/InsecureHint.cpp @@ -0,0 +1,67 @@ +#include "V2RayStreamSettings.hpp" +#include "ShadowSocksBean.hpp" +#include "VMessBean.hpp" +#include "TrojanVLESSBean.hpp" +#include "SocksHttpBean.hpp" + +namespace NekoRay::fmt { + QString DisplayInsecureHint(const QSharedPointer &bean) { + if (!dataStore->insecure_hint) return {}; + auto insecure_hint = bean->InsecureHint(); + auto stream = GetStreamSettings(bean); + if (stream != nullptr) insecure_hint += "\n" + stream->InsecureHint(); + return insecure_hint.trimmed(); + } + + QString V2rayStreamSettings::InsecureHint() const { + if (allow_insecure) { + return QObject::tr( + "The configuration (insecure) can be detected and identified, the transmission is fully visible to the censor and is not resistant to man-in-the-middle tampering with the content of the communication." + ); + } + return {}; + } + + QString ShadowSocksBean::InsecureHint() { + if (method.contains("-poly") || method.contains("-gcm")) { + return {}; + } + return QObject::tr( + "This configuration (Shadowsocks streaming cipher) can be accurately proactively detected and decrypted by censors without requiring a password, and cannot be mitigated by turning on IV replay filters on the server side.\n" + "\n" + "Learn more: https://github.com/net4people/bbs/issues/24" + ); + } + + QString VMessBean::InsecureHint() { + if (security == "none" || security == "zero") { + if (stream->security.isEmpty() || stream->security == "none") { + return QObject::tr( + "This profile is cleartext, don't use it if the server is not in your local network."); + } + } + if (aid > 0) { + return QObject::tr( + "This configuration (VMess MD5 authentication) has been deprecated by upstream because of its questionable resistance to tampering and concealment.\n" + "\n" + "As of January 1, 2022, compatibility with MD5 authentication information will be disabled on the server side by default. Any client using MD5 authentication information will not be able to connect to a server with VMess MD5 authentication information disabled." + ); + } + return {}; + } + + QString TrojanVLESSBean::InsecureHint() { + if (stream->security.isEmpty() || stream->security == "none") { + return QObject::tr("This profile is cleartext, don't use it if the server is not in your local network."); + } + return {}; + } + + QString SocksHttpBean::InsecureHint() { + if (stream->security.isEmpty() || stream->security == "none") { + return QObject::tr("This profile is cleartext, don't use it if the server is not in your local network."); + } + return {}; + } +} + diff --git a/fmt/Link2Bean.cpp b/fmt/Link2Bean.cpp new file mode 100644 index 0000000..47abea2 --- /dev/null +++ b/fmt/Link2Bean.cpp @@ -0,0 +1,147 @@ +#include "db/ProxyEntity.hpp" +#include "fmt/includes.h" + +#include + +namespace NekoRay::fmt { + +#define DECODE_V2RAY_N_1 auto linkN = DecodeB64IfValid(SubStrBefore(SubStrAfter(link, "://"), "#"), QByteArray::Base64Option::Base64UrlEncoding); \ + if (linkN.isEmpty()) return false; \ + auto hasRemarks = link.contains("#"); \ + if (hasRemarks) linkN += "#" + SubStrAfter(link, "#"); \ + auto url = QUrl("https://" + linkN); + + bool SocksHttpBean::TryParseLink(const QString &link) { + if (!SubStrAfter(link, "://").contains(":")) { + // v2rayN shit format + DECODE_V2RAY_N_1 + + if (hasRemarks) name = url.fragment(QUrl::FullyDecoded); + serverAddress = url.host(); + serverPort = url.port(); + username = url.userName(); + password = url.password(); + } else { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = GetQuery(url); + + if (link.startsWith("socks4")) socks_http_type = type_Socks4; + if (link.startsWith("http")) socks_http_type = type_HTTP; + serverAddress = url.host(); + serverPort = url.port(); + username = url.userName(); + password = url.password(); + if (serverPort == -1) serverPort = socks_http_type == type_HTTP ? 443 : 1080; + + stream->security = GetQueryValue(query, "security", "") == "true" ? "tls" : "none"; + stream->sni = GetQueryValue(query, "sni"); + } + return true; + } + + bool TrojanVLESSBean::TryParseLink(const QString &link) { + auto url = QUrl(link); + if (!url.isValid()) return false; + auto query = GetQuery(url); + + name = url.fragment(QUrl::FullyDecoded); + serverAddress = url.host(); + serverPort = url.port(); + password = url.userName(); + if (serverPort == -1) serverPort = 443; + + stream->network = GetQueryValue(query, "type", "tcp").replace("http", "h2"); + stream->security = GetQueryValue(query, "security", "tls"); + auto sni1 = GetQueryValue(query, "sni"); + auto sni2 = GetQueryValue(query, "peer"); + if (!sni1.isEmpty()) stream->sni = sni1; + if (!sni2.isEmpty()) stream->sni = sni2; + if (!query.queryItemValue("allowInsecure").isEmpty()) stream->allow_insecure = true; + + // TODO header kcp quic + if (stream->network == "ws") { + stream->path = GetQueryValue(query, "path", ""); + stream->host = GetQueryValue(query, "host", ""); + } else if (stream->network == "h2") { + stream->path = GetQueryValue(query, "path", ""); + stream->host = GetQueryValue(query, "host", "").replace("|", ","); + } else if (stream->network == "grpc") { + stream->path = GetQueryValue(query, "serviceName", ""); + } + + return !password.isEmpty(); + } + + bool ShadowSocksBean::TryParseLink(const QString &link) { + if (SubStrBefore(link, "#").contains("@")) { + // SS + auto url = QUrl(link); + if (!url.isValid()) return false; + + name = url.fragment(QUrl::FullyDecoded); + serverAddress = url.host(); + serverPort = url.port(); + auto method_password = DecodeB64IfValid(url.userName(), QByteArray::Base64Option::Base64UrlEncoding); + if (method_password.isEmpty()) return false; + method = SubStrBefore(method_password, ":"); + password = SubStrAfter(method_password, ":"); + auto query = GetQuery(url); + plugin = query.queryItemValue("plugin").replace("simple-obfs;", "obfs-local;"); + } else { + // v2rayN + DECODE_V2RAY_N_1 + + if (hasRemarks) name = url.fragment(QUrl::FullyDecoded); + serverAddress = url.host(); + serverPort = url.port(); + method = url.userName(); + password = url.password(); + } + return true; + } + + bool VMessBean::TryParseLink(const QString &link) { + // V2RayN Format + auto linkN = DecodeB64IfValid(SubStrAfter(link, "vmess://")); + if (!linkN.isEmpty()) { + auto objN = QString2QJsonObject(linkN); + if (objN.isEmpty()) return false; + // REQUIRED + uuid = objN["id"].toString(); + serverAddress = objN["add"].toString(); + serverPort = objN["port"].toVariant().toInt(); + // OPTIONAL + name = objN["ps"].toString(); + aid = objN["aid"].toInt(); + stream->host = objN["host"].toString(); + stream->path = objN["path"].toString(); + stream->sni = objN["sni"].toString(); + auto net = objN["net"].toString().replace("http", "h2"); + if (!net.isEmpty()) stream->network = net; + auto scy = objN["scy"].toString(); + if (!scy.isEmpty()) security = scy; + // TLS (XTLS?) + if (!objN["tls"].toString().isEmpty()) stream->security = "tls"; + // TODO quic & kcp + return true; + } + + // Std Format + return false; + } + + bool NaiveBean::TryParseLink(const QString &link) { + auto url = QUrl(link); + if (!url.isValid()) return false; + + name = url.fragment(QUrl::FullyDecoded); + serverAddress = url.host(); + serverPort = url.port(); + username = url.userName(); + password = url.password(); + + return !(username.isEmpty() || password.isEmpty()); + } + +} diff --git a/fmt/NaiveBean.hpp b/fmt/NaiveBean.hpp new file mode 100644 index 0000000..25a2642 --- /dev/null +++ b/fmt/NaiveBean.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" + +namespace NekoRay::fmt { + class NaiveBean : public AbstractBean { + public: + QString username = ""; + QString password = ""; + QString protocol = "https"; + QString extra_headers = ""; + QString sni = ""; + QString certificate = ""; + int insecure_concurrency = 0; + + NaiveBean() : AbstractBean(0) { + _add(new configItem("username", &username, itemType::string)); + _add(new configItem("password", &password, itemType::string)); + _add(new configItem("protocol", &protocol, itemType::string)); + _add(new configItem("extra_headers", &extra_headers, itemType::string)); + _add(new configItem("sni", &sni, itemType::string)); + _add(new configItem("certificate", &certificate, itemType::string)); + _add(new configItem("insecure_concurrency", &insecure_concurrency, itemType::integer)); + }; + + QString DisplayType() override { return "Naive"; }; + + bool NeedExternal() override { return true; }; + + ExternalBuildResult BuildExternal(int mapping_port, int socks_port) override; + + bool TryParseLink(const QString &link); + + QString ToShareLink() override; + }; +} \ No newline at end of file diff --git a/fmt/ShadowSocksBean.hpp b/fmt/ShadowSocksBean.hpp new file mode 100644 index 0000000..6fef724 --- /dev/null +++ b/fmt/ShadowSocksBean.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" +#include "fmt/V2RayStreamSettings.hpp" + +namespace NekoRay::fmt { + class ShadowSocksBean : public AbstractBean { + public: + QString method = "aes-128-gcm"; + QString password = ""; + QString plugin = ""; + + QSharedPointer stream = QSharedPointer(new V2rayStreamSettings()); + QString custom = ""; + + ShadowSocksBean() : AbstractBean(0) { + _add(new configItem("method", &method, itemType::string)); + _add(new configItem("pass", &password, itemType::string)); + _add(new configItem("plugin", &plugin, itemType::string)); + _add(new configItem("stream", dynamic_cast(stream.get()), itemType::jsonStore)); + _add(new configItem("custom", &custom, itemType::string)); + }; + + QString DisplayType() override { return "Shadowsocks"; }; + + CoreObjOutboundBuildResult BuildCoreObj() override; + + bool TryParseLink(const QString &link); + + QString ToShareLink() override; + + QString InsecureHint() override; + }; +} diff --git a/fmt/SocksHttpBean.hpp b/fmt/SocksHttpBean.hpp new file mode 100644 index 0000000..e35f430 --- /dev/null +++ b/fmt/SocksHttpBean.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" +#include "fmt/V2RayStreamSettings.hpp" + +namespace NekoRay::fmt { + class SocksHttpBean : public AbstractBean { + public: + static constexpr int type_HTTP = -80; + static constexpr int type_Socks4 = 4; + static constexpr int type_Socks5 = 5; + + int socks_http_type = type_Socks5; + QString username = ""; + QString password = ""; + + QSharedPointer stream = QSharedPointer(new V2rayStreamSettings()); + QString custom = ""; + + explicit SocksHttpBean(int _socks_http_type) : AbstractBean(0) { + this->socks_http_type = _socks_http_type; + _add(new configItem("v", &socks_http_type, itemType::integer)); + _add(new configItem("username", &username, itemType::string)); + _add(new configItem("password", &password, itemType::string)); + _add(new configItem("stream", dynamic_cast(stream.get()), itemType::jsonStore)); + _add(new configItem("custom", &custom, itemType::string)); + }; + + QString DisplayType() override { return socks_http_type == type_HTTP ? "HTTP" : "Socks"; }; + + CoreObjOutboundBuildResult BuildCoreObj() override; + + bool TryParseLink(const QString &link); + + QString ToShareLink() override; + + QString InsecureHint() override; + }; +} diff --git a/fmt/TrojanVLESSBean.hpp b/fmt/TrojanVLESSBean.hpp new file mode 100644 index 0000000..6d436dd --- /dev/null +++ b/fmt/TrojanVLESSBean.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" +#include "fmt/V2RayStreamSettings.hpp" + +namespace NekoRay::fmt { + class TrojanVLESSBean : public AbstractBean { + public: + static constexpr int proxy_Trojan = 0; + static constexpr int proxy_VLESS = 1; + int proxy_type = proxy_Trojan; + + QString password = ""; + + QSharedPointer stream = QSharedPointer(new V2rayStreamSettings()); + QString custom = ""; + + explicit TrojanVLESSBean(int _proxy_type) : AbstractBean(0) { + proxy_type = _proxy_type; + _add(new configItem("pass", &password, itemType::string)); + _add(new configItem("stream", dynamic_cast(stream.get()), itemType::jsonStore)); + _add(new configItem("custom", &custom, itemType::string)); + }; + + QString DisplayType() override { return proxy_type == proxy_VLESS ? "VLESS" : "Trojan"; }; + + CoreObjOutboundBuildResult BuildCoreObj() override; + + bool TryParseLink(const QString &link); + + QString ToShareLink() override; + + QString InsecureHint() override; + }; +} \ No newline at end of file diff --git a/fmt/V2RayStreamSettings.hpp b/fmt/V2RayStreamSettings.hpp new file mode 100644 index 0000000..023ad54 --- /dev/null +++ b/fmt/V2RayStreamSettings.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "AbstractBean.hpp" + +namespace NekoRay::fmt { + class V2rayStreamSettings : public JsonStore { + public: + QString network = "tcp"; + QString security = "none"; + QString packet_encoding = ""; + // ws/h2/grpc + QString path = ""; + QString host = ""; + // ws + int max_early_data = 0; + QString early_data_header_name = ""; + // QUIC & KCP + QString header_type = ""; + // tls + QString sni = ""; + QString certificate = ""; + bool allow_insecure = false; + + V2rayStreamSettings() : JsonStore() { + _add(new configItem("net", &network, itemType::string)); + _add(new configItem("sec", &security, itemType::string)); + _add(new configItem("pac_enc", &packet_encoding, itemType::string)); + _add(new configItem("path", &path, itemType::string)); + _add(new configItem("host", &host, itemType::string)); + _add(new configItem("sni", &sni, itemType::string)); + _add(new configItem("cert", &certificate, itemType::string)); + _add(new configItem("insecure", &allow_insecure, itemType::boolean)); + _add(new configItem("ws_med", &max_early_data, itemType::integer)); + _add(new configItem("ws_edhn", &early_data_header_name, itemType::string)); + _add(new configItem("h_type", &header_type, itemType::string)); + } + + QJsonObject BuildStreamSettings(); + + [[nodiscard]] QString InsecureHint() const; + }; + + inline V2rayStreamSettings *GetStreamSettings(const QSharedPointer &bean) { + if (bean == nullptr) return nullptr; + auto stream_item = bean->_get("stream"); + if (stream_item != nullptr) { + auto stream_store = (NekoRay::JsonStore *) stream_item->ptr; + auto stream = (NekoRay::fmt::V2rayStreamSettings *) stream_store; + return stream; + } + return nullptr; + } +} diff --git a/fmt/VMessBean.hpp b/fmt/VMessBean.hpp new file mode 100644 index 0000000..1091883 --- /dev/null +++ b/fmt/VMessBean.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "fmt/AbstractBean.hpp" +#include "fmt/V2RayStreamSettings.hpp" + +namespace NekoRay::fmt { + class VMessBean : public AbstractBean { + public: + QString uuid = ""; + int aid = 0; + QString security = "auto"; + + QSharedPointer stream = QSharedPointer(new V2rayStreamSettings()); + QString custom = ""; + + VMessBean() : AbstractBean(0) { + _add(new configItem("id", &uuid, itemType::string)); + _add(new configItem("aid", &aid, itemType::integer)); + _add(new configItem("sec", &security, itemType::string)); + _add(new configItem("stream", dynamic_cast(stream.get()), itemType::jsonStore)); + _add(new configItem("custom", &custom, itemType::string)); + }; + + QString DisplayType() override { return "VMess"; }; + + CoreObjOutboundBuildResult BuildCoreObj() override; + + bool TryParseLink(const QString &link); + + QString ToShareLink() override; + + QString InsecureHint() override; + }; +} + diff --git a/fmt/includes.h b/fmt/includes.h new file mode 100644 index 0000000..426fe4b --- /dev/null +++ b/fmt/includes.h @@ -0,0 +1,9 @@ +#pragma once + +#include "SocksHttpBean.hpp" +#include "ShadowSocksBean.hpp" +#include "ChainBean.hpp" +#include "VMessBean.hpp" +#include "TrojanVLESSBean.hpp" +#include "NaiveBean.hpp" +#include "CustomBean.hpp" diff --git a/go/.gitignore b/go/.gitignore new file mode 100644 index 0000000..164ab83 --- /dev/null +++ b/go/.gitignore @@ -0,0 +1,6 @@ +*.log +*.pem +*.json +*.exe +*.dat +/nekoray_core diff --git a/go/auth.go b/go/auth.go new file mode 100644 index 0000000..531b35c --- /dev/null +++ b/go/auth.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +// Authenticator exposes a function for authenticating requests. +type Authenticator struct { + Token string +} + +// Authenticate checks that a token exists and is valid. It stores the user +// metadata in the returned context and removes the token from the context. +func (a Authenticator) Authenticate(ctx context.Context) (newCtx context.Context, err error) { + auth, err := extractHeader(ctx, "nekoray_auth") + if err != nil { + return ctx, err + } + + if auth != a.Token { + return ctx, status.Error(codes.Unauthenticated, "invalid token") + } + + return purgeHeader(ctx, "nekoray_auth"), nil +} + +func extractHeader(ctx context.Context, header string) (string, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return "", status.Error(codes.Unauthenticated, "no headers in request") + } + + authHeaders, ok := md[header] + if !ok { + return "", status.Error(codes.Unauthenticated, "no header in request") + } + + if len(authHeaders) != 1 { + return "", status.Error(codes.Unauthenticated, "more than 1 header in request") + } + + return authHeaders[0], nil +} + +func purgeHeader(ctx context.Context, header string) context.Context { + md, _ := metadata.FromIncomingContext(ctx) + mdCopy := md.Copy() + mdCopy[header] = nil + return metadata.NewIncomingContext(ctx, mdCopy) +} diff --git a/go/core_rpc.go b/go/core_rpc.go new file mode 100644 index 0000000..1b95fb3 --- /dev/null +++ b/go/core_rpc.go @@ -0,0 +1,323 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "libcore" + "libcore/device" + "libcore/stun" + "nekoray_core/gen" + "net" + "os" + "strings" + "time" + + "github.com/sirupsen/logrus" +) + +var instance *libcore.V2RayInstance + +func setupCore() { + device.IsNekoray = true + libcore.SetConfig("", false, true) + libcore.InitCore("", "", "", nil, ".", "moe.nekoray.pc:bg", true, 50) +} + +func (s *server) Start(ctx context.Context, in *gen.LoadConfigReq) (out *gen.ErrorResp, _ error) { + var err error + + // only error use this + defer func() { + out = &gen.ErrorResp{} + if err != nil { + out.Error = err.Error() + instance = nil + } + }() + + if nekoray_debug { + logrus.Println("Start:", in) + } + + if instance != nil { + err = errors.New("instance already started") + return + } + + instance = libcore.NewV2rayInstance() + + libcore.SetConfig(in.TryDomains, false, true) + + err = instance.LoadConfig(in.CoreConfig) + if err != nil { + return + } + + err = instance.Start() + if err != nil { + return + } + + TunSetV2ray(instance) + + return +} + +func (s *server) SetTun(ctx context.Context, in *gen.SetTunReq) (out *gen.ErrorResp, _ error) { + var err error + + // only error use this + defer func() { + out = &gen.ErrorResp{} + if err != nil { + out.Error = err.Error() + } + }() + + if in.Implementation >= 0 { //Start + err = TunStart(in) + } else { //Stop + TunStop() + } + + return +} + +func (s *server) Stop(ctx context.Context, in *gen.EmptyReq) (out *gen.ErrorResp, _ error) { + var err error + + // only error use this + defer func() { + out = &gen.ErrorResp{} + if err != nil { + out.Error = err.Error() + } + }() + + if instance == nil { + return + } + + TunSetV2ray(nil) + + err = instance.Close() + instance = nil + + return +} + +func (s *server) Exit(ctx context.Context, in *gen.EmptyReq) (out *gen.EmptyResp, _ error) { + out = &gen.EmptyResp{} + + // Connection closed + os.Exit(0) + return +} + +func (s *server) Test(ctx context.Context, in *gen.TestReq) (out *gen.TestResp, _ error) { + var err error + out = &gen.TestResp{Ms: 0} + + defer func() { + if err != nil { + out.Error = err.Error() + } + }() + + if nekoray_debug { + logrus.Println("Test:", in) + } + + if in.Mode == gen.TestMode_UrlTest { + var i *libcore.V2RayInstance + + if in.Config != nil { + // Test instance + i = libcore.NewV2rayInstance() + defer i.Close() + + err = i.LoadConfig(in.Config.CoreConfig) + if err != nil { + return + } + + err = i.Start() + if err != nil { + return + } + } else { + // Test running instance + i = instance + if i == nil { + return + } + } + + // Latency + var t int32 + t, err = libcore.UrlTestV2ray(i, in.Inbound, in.Url, in.Timeout) + out.Ms = t // sn: ms==0 是错误 + } else if in.Mode == gen.TestMode_TcpPing { + startTime := time.Now() + _, err = net.DialTimeout("tcp", in.Address, time.Duration(in.Timeout)*time.Millisecond) + endTime := time.Now() + if err == nil { + out.Ms = int32(endTime.Sub(startTime).Milliseconds()) + } + } else if in.Mode == gen.TestMode_FullTest { + if in.Config == nil { + return + } + + // Test instance + i := libcore.NewV2rayInstance() + defer i.Close() + + err = i.LoadConfig(in.Config.CoreConfig) + if err != nil { + return + } + + err = i.Start() + if err != nil { + return + } + + // Latency + var latency string + if in.FullLatency { + t, _ := libcore.UrlTestV2ray(i, in.Inbound, in.Url, in.Timeout) + out.Ms = t + if t > 0 { + latency = fmt.Sprint(t, "ms") + } else { + latency = "Error" + } + } + + // 入口 IP + var in_ip string + if in.FullInOut { + _in_ip, err := net.ResolveIPAddr("ip", in.InAddress) + if err == nil { + in_ip = _in_ip.String() + } else { + in_ip = err.Error() + } + } + + client := getProxyHttpClient(i) + + // 出口 IP + var out_ip string + if in.FullInOut { + resp, err := client.Get("https://httpbin.org/get") + if err == nil { + v := make(map[string]interface{}) + json.NewDecoder(resp.Body).Decode(&v) + if a, ok := v["origin"]; ok { + if s, ok := a.(string); ok { + out_ip = s + } + } + resp.Body.Close() + } else { + out_ip = "Error" + } + } + + // 下载 + var speed string + if in.FullSpeed { + resp, err := client.Get("http://cachefly.cachefly.net/10mb.test") + if err == nil { + time_start := time.Now() + n, _ := io.Copy(io.Discard, resp.Body) + time_end := time.Now() + + speed = fmt.Sprintf("%.2fMiB/s", (float64(n)/time_end.Sub(time_start).Seconds())/1048576) + resp.Body.Close() + } else { + speed = "Error" + } + } + + // STUN + var stunText string + if in.FullNat { + timeout := time.NewTimer(time.Second * 5) + result := make(chan string, 0) + + go func() { + stunServer := "206.53.159.130:3478" + stunAddr, _ := net.ResolveUDPAddr("udp4", stunServer) + pc, err := i.DialUDP(stunAddr) + if err == nil { + stunClient := stun.NewClientWithConnection(pc) + stunClient.SetServerAddr(stunServer) + nat, host, err, fake := stunClient.Discover() + if err == nil { + if host != nil { + if fake { + result <- fmt.Sprint("No Endpoint", nat) + } else { + result <- fmt.Sprint(nat) + } + } + } else { + result <- "Discover Error" + } + } else { + result <- "DialUDP Error" + } + close(result) + }() + + select { + case <-timeout.C: + stunText = "Timeout" + case r := <-result: + stunText = r + } + } + + fr := make([]string, 0) + if latency != "" { + fr = append(fr, fmt.Sprintf("Latency: %s", latency)) + } + if speed != "" { + fr = append(fr, fmt.Sprintf("Speed: %s", speed)) + } + if in_ip != "" { + fr = append(fr, fmt.Sprintf("In: %s", in_ip)) + } + if out_ip != "" { + fr = append(fr, fmt.Sprintf("Out: %s", out_ip)) + } + if stunText != "" { + fr = append(fr, fmt.Sprintf("NAT: %s", stunText)) + } + + out.FullReport = strings.Join(fr, " / ") + } + + return +} + +func (s *server) QueryStats(ctx context.Context, in *gen.QueryStatsReq) (out *gen.QueryStatsResp, _ error) { + out = &gen.QueryStatsResp{} + if instance != nil { + out.Traffic = instance.QueryStats(in.Tag, in.Direct) + } + return +} + +func (s *server) ListV2RayConnections(ctx context.Context, in *gen.EmptyReq) (*gen.ListV2RayConnectionsResp, error) { + out := &gen.ListV2RayConnectionsResp{ + MatsuriConnectionsJson: libcore.ListV2rayConnections(), + } + return out, nil +} diff --git a/go/gen/libcore.pb.go b/go/gen/libcore.pb.go new file mode 100644 index 0000000..9dfbd26 --- /dev/null +++ b/go/gen/libcore.pb.go @@ -0,0 +1,1181 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.21.4 +// source: libcore.proto + +package gen + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TestMode int32 + +const ( + TestMode_TcpPing TestMode = 0 + TestMode_UrlTest TestMode = 1 + TestMode_FullTest TestMode = 2 +) + +// Enum value maps for TestMode. +var ( + TestMode_name = map[int32]string{ + 0: "TcpPing", + 1: "UrlTest", + 2: "FullTest", + } + TestMode_value = map[string]int32{ + "TcpPing": 0, + "UrlTest": 1, + "FullTest": 2, + } +) + +func (x TestMode) Enum() *TestMode { + p := new(TestMode) + *p = x + return p +} + +func (x TestMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TestMode) Descriptor() protoreflect.EnumDescriptor { + return file_libcore_proto_enumTypes[0].Descriptor() +} + +func (TestMode) Type() protoreflect.EnumType { + return &file_libcore_proto_enumTypes[0] +} + +func (x TestMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TestMode.Descriptor instead. +func (TestMode) EnumDescriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{0} +} + +type UpdateAction int32 + +const ( + UpdateAction_Check UpdateAction = 0 + UpdateAction_Download UpdateAction = 1 +) + +// Enum value maps for UpdateAction. +var ( + UpdateAction_name = map[int32]string{ + 0: "Check", + 1: "Download", + } + UpdateAction_value = map[string]int32{ + "Check": 0, + "Download": 1, + } +) + +func (x UpdateAction) Enum() *UpdateAction { + p := new(UpdateAction) + *p = x + return p +} + +func (x UpdateAction) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (UpdateAction) Descriptor() protoreflect.EnumDescriptor { + return file_libcore_proto_enumTypes[1].Descriptor() +} + +func (UpdateAction) Type() protoreflect.EnumType { + return &file_libcore_proto_enumTypes[1] +} + +func (x UpdateAction) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use UpdateAction.Descriptor instead. +func (UpdateAction) EnumDescriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{1} +} + +type EmptyReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *EmptyReq) Reset() { + *x = EmptyReq{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EmptyReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmptyReq) ProtoMessage() {} + +func (x *EmptyReq) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmptyReq.ProtoReflect.Descriptor instead. +func (*EmptyReq) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{0} +} + +type EmptyResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *EmptyResp) Reset() { + *x = EmptyResp{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EmptyResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmptyResp) ProtoMessage() {} + +func (x *EmptyResp) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmptyResp.ProtoReflect.Descriptor instead. +func (*EmptyResp) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{1} +} + +type ErrorResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *ErrorResp) Reset() { + *x = ErrorResp{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ErrorResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ErrorResp) ProtoMessage() {} + +func (x *ErrorResp) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ErrorResp.ProtoReflect.Descriptor instead. +func (*ErrorResp) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{2} +} + +func (x *ErrorResp) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type LoadConfigReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CoreConfig string `protobuf:"bytes,1,opt,name=coreConfig,proto3" json:"coreConfig,omitempty"` + TryDomains string `protobuf:"bytes,2,opt,name=tryDomains,proto3" json:"tryDomains,omitempty"` +} + +func (x *LoadConfigReq) Reset() { + *x = LoadConfigReq{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LoadConfigReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoadConfigReq) ProtoMessage() {} + +func (x *LoadConfigReq) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoadConfigReq.ProtoReflect.Descriptor instead. +func (*LoadConfigReq) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{3} +} + +func (x *LoadConfigReq) GetCoreConfig() string { + if x != nil { + return x.CoreConfig + } + return "" +} + +func (x *LoadConfigReq) GetTryDomains() string { + if x != nil { + return x.TryDomains + } + return "" +} + +type SetTunReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Mtu int32 `protobuf:"varint,2,opt,name=mtu,proto3" json:"mtu,omitempty"` + Implementation int32 `protobuf:"varint,3,opt,name=implementation,proto3" json:"implementation,omitempty"` + Fakedns bool `protobuf:"varint,4,opt,name=fakedns,proto3" json:"fakedns,omitempty"` +} + +func (x *SetTunReq) Reset() { + *x = SetTunReq{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetTunReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetTunReq) ProtoMessage() {} + +func (x *SetTunReq) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetTunReq.ProtoReflect.Descriptor instead. +func (*SetTunReq) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{4} +} + +func (x *SetTunReq) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SetTunReq) GetMtu() int32 { + if x != nil { + return x.Mtu + } + return 0 +} + +func (x *SetTunReq) GetImplementation() int32 { + if x != nil { + return x.Implementation + } + return 0 +} + +func (x *SetTunReq) GetFakedns() bool { + if x != nil { + return x.Fakedns + } + return false +} + +type TestReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Mode TestMode `protobuf:"varint,1,opt,name=mode,proto3,enum=libcore.TestMode" json:"mode,omitempty"` + Timeout int32 `protobuf:"varint,6,opt,name=timeout,proto3" json:"timeout,omitempty"` + // TcpPing + Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` + // UrlTest + Config *LoadConfigReq `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"` + Inbound string `protobuf:"bytes,4,opt,name=inbound,proto3" json:"inbound,omitempty"` + Url string `protobuf:"bytes,5,opt,name=url,proto3" json:"url,omitempty"` + // FullTest + InAddress string `protobuf:"bytes,7,opt,name=in_address,json=inAddress,proto3" json:"in_address,omitempty"` + FullLatency bool `protobuf:"varint,8,opt,name=full_latency,json=fullLatency,proto3" json:"full_latency,omitempty"` + FullSpeed bool `protobuf:"varint,9,opt,name=full_speed,json=fullSpeed,proto3" json:"full_speed,omitempty"` + FullInOut bool `protobuf:"varint,10,opt,name=full_in_out,json=fullInOut,proto3" json:"full_in_out,omitempty"` + FullNat bool `protobuf:"varint,11,opt,name=full_nat,json=fullNat,proto3" json:"full_nat,omitempty"` +} + +func (x *TestReq) Reset() { + *x = TestReq{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TestReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestReq) ProtoMessage() {} + +func (x *TestReq) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestReq.ProtoReflect.Descriptor instead. +func (*TestReq) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{5} +} + +func (x *TestReq) GetMode() TestMode { + if x != nil { + return x.Mode + } + return TestMode_TcpPing +} + +func (x *TestReq) GetTimeout() int32 { + if x != nil { + return x.Timeout + } + return 0 +} + +func (x *TestReq) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *TestReq) GetConfig() *LoadConfigReq { + if x != nil { + return x.Config + } + return nil +} + +func (x *TestReq) GetInbound() string { + if x != nil { + return x.Inbound + } + return "" +} + +func (x *TestReq) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *TestReq) GetInAddress() string { + if x != nil { + return x.InAddress + } + return "" +} + +func (x *TestReq) GetFullLatency() bool { + if x != nil { + return x.FullLatency + } + return false +} + +func (x *TestReq) GetFullSpeed() bool { + if x != nil { + return x.FullSpeed + } + return false +} + +func (x *TestReq) GetFullInOut() bool { + if x != nil { + return x.FullInOut + } + return false +} + +func (x *TestReq) GetFullNat() bool { + if x != nil { + return x.FullNat + } + return false +} + +type TestResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + Ms int32 `protobuf:"varint,2,opt,name=ms,proto3" json:"ms,omitempty"` + FullReport string `protobuf:"bytes,3,opt,name=full_report,json=fullReport,proto3" json:"full_report,omitempty"` +} + +func (x *TestResp) Reset() { + *x = TestResp{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TestResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestResp) ProtoMessage() {} + +func (x *TestResp) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestResp.ProtoReflect.Descriptor instead. +func (*TestResp) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{6} +} + +func (x *TestResp) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *TestResp) GetMs() int32 { + if x != nil { + return x.Ms + } + return 0 +} + +func (x *TestResp) GetFullReport() string { + if x != nil { + return x.FullReport + } + return "" +} + +type QueryStatsReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + Direct string `protobuf:"bytes,2,opt,name=direct,proto3" json:"direct,omitempty"` +} + +func (x *QueryStatsReq) Reset() { + *x = QueryStatsReq{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *QueryStatsReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryStatsReq) ProtoMessage() {} + +func (x *QueryStatsReq) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryStatsReq.ProtoReflect.Descriptor instead. +func (*QueryStatsReq) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{7} +} + +func (x *QueryStatsReq) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *QueryStatsReq) GetDirect() string { + if x != nil { + return x.Direct + } + return "" +} + +type QueryStatsResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Traffic int64 `protobuf:"varint,1,opt,name=traffic,proto3" json:"traffic,omitempty"` +} + +func (x *QueryStatsResp) Reset() { + *x = QueryStatsResp{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *QueryStatsResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryStatsResp) ProtoMessage() {} + +func (x *QueryStatsResp) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryStatsResp.ProtoReflect.Descriptor instead. +func (*QueryStatsResp) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{8} +} + +func (x *QueryStatsResp) GetTraffic() int64 { + if x != nil { + return x.Traffic + } + return 0 +} + +type UpdateReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Action UpdateAction `protobuf:"varint,1,opt,name=action,proto3,enum=libcore.UpdateAction" json:"action,omitempty"` +} + +func (x *UpdateReq) Reset() { + *x = UpdateReq{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateReq) ProtoMessage() {} + +func (x *UpdateReq) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateReq.ProtoReflect.Descriptor instead. +func (*UpdateReq) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{9} +} + +func (x *UpdateReq) GetAction() UpdateAction { + if x != nil { + return x.Action + } + return UpdateAction_Check +} + +type UpdateResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + AssetsName string `protobuf:"bytes,2,opt,name=assets_name,json=assetsName,proto3" json:"assets_name,omitempty"` + DownloadUrl string `protobuf:"bytes,3,opt,name=download_url,json=downloadUrl,proto3" json:"download_url,omitempty"` + ReleaseUrl string `protobuf:"bytes,4,opt,name=release_url,json=releaseUrl,proto3" json:"release_url,omitempty"` + ReleaseNote string `protobuf:"bytes,5,opt,name=release_note,json=releaseNote,proto3" json:"release_note,omitempty"` +} + +func (x *UpdateResp) Reset() { + *x = UpdateResp{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateResp) ProtoMessage() {} + +func (x *UpdateResp) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateResp.ProtoReflect.Descriptor instead. +func (*UpdateResp) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{10} +} + +func (x *UpdateResp) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *UpdateResp) GetAssetsName() string { + if x != nil { + return x.AssetsName + } + return "" +} + +func (x *UpdateResp) GetDownloadUrl() string { + if x != nil { + return x.DownloadUrl + } + return "" +} + +func (x *UpdateResp) GetReleaseUrl() string { + if x != nil { + return x.ReleaseUrl + } + return "" +} + +func (x *UpdateResp) GetReleaseNote() string { + if x != nil { + return x.ReleaseNote + } + return "" +} + +type ListV2RayConnectionsResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MatsuriConnectionsJson string `protobuf:"bytes,1,opt,name=matsuri_connections_json,json=matsuriConnectionsJson,proto3" json:"matsuri_connections_json,omitempty"` +} + +func (x *ListV2RayConnectionsResp) Reset() { + *x = ListV2RayConnectionsResp{} + if protoimpl.UnsafeEnabled { + mi := &file_libcore_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListV2RayConnectionsResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListV2RayConnectionsResp) ProtoMessage() {} + +func (x *ListV2RayConnectionsResp) ProtoReflect() protoreflect.Message { + mi := &file_libcore_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListV2RayConnectionsResp.ProtoReflect.Descriptor instead. +func (*ListV2RayConnectionsResp) Descriptor() ([]byte, []int) { + return file_libcore_proto_rawDescGZIP(), []int{11} +} + +func (x *ListV2RayConnectionsResp) GetMatsuriConnectionsJson() string { + if x != nil { + return x.MatsuriConnectionsJson + } + return "" +} + +var File_libcore_proto protoreflect.FileDescriptor + +var file_libcore_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x07, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x22, 0x0a, 0x0a, 0x08, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x52, 0x65, 0x71, 0x22, 0x0b, 0x0a, 0x09, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x22, 0x21, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x12, 0x14, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x22, 0x4f, 0x0a, 0x0d, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x72, 0x65, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x72, 0x65, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x72, 0x79, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x72, 0x79, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x22, 0x73, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x54, 0x75, 0x6e, 0x52, + 0x65, 0x71, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x74, 0x75, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x03, 0x6d, 0x74, 0x75, 0x12, 0x26, 0x0a, 0x0e, 0x69, 0x6d, 0x70, 0x6c, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0e, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x18, 0x0a, 0x07, 0x66, 0x61, 0x6b, 0x65, 0x64, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x66, 0x61, 0x6b, 0x65, 0x64, 0x6e, 0x73, 0x22, 0xdc, 0x02, 0x0a, 0x07, 0x54, + 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x12, 0x25, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x54, + 0x65, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, + 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4c, 0x6f, 0x61, 0x64, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, + 0x72, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1d, 0x0a, + 0x0a, 0x69, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x69, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x21, 0x0a, 0x0c, + 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0b, 0x66, 0x75, 0x6c, 0x6c, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, + 0x1d, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x73, 0x70, 0x65, 0x65, 0x64, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x70, 0x65, 0x65, 0x64, 0x12, 0x1e, + 0x0a, 0x0b, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x69, 0x6e, 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x49, 0x6e, 0x4f, 0x75, 0x74, 0x12, 0x19, + 0x0a, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x66, 0x75, 0x6c, 0x6c, 0x4e, 0x61, 0x74, 0x22, 0x51, 0x0a, 0x08, 0x54, 0x65, 0x73, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x6d, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x6d, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x66, + 0x75, 0x6c, 0x6c, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x39, 0x0a, 0x0d, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x12, 0x10, 0x0a, + 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, + 0x16, 0x0a, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x22, 0x2a, 0x0a, 0x0e, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x72, 0x61, + 0x66, 0x66, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x74, 0x72, 0x61, 0x66, + 0x66, 0x69, 0x63, 0x22, 0x3a, 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x12, 0x2d, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x15, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0xaa, 0x01, 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x14, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, + 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, + 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x6f, 0x77, + 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x72, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x6c, 0x65, + 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, + 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x6c, + 0x65, 0x61, 0x73, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x22, 0x54, 0x0a, 0x18, + 0x4c, 0x69, 0x73, 0x74, 0x56, 0x32, 0x72, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x12, 0x38, 0x0a, 0x18, 0x6d, 0x61, 0x74, 0x73, + 0x75, 0x72, 0x69, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, + 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x6d, 0x61, 0x74, 0x73, + 0x75, 0x72, 0x69, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x4a, 0x73, + 0x6f, 0x6e, 0x2a, 0x32, 0x0a, 0x08, 0x54, 0x65, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0b, + 0x0a, 0x07, 0x54, 0x63, 0x70, 0x50, 0x69, 0x6e, 0x67, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, + 0x72, 0x6c, 0x54, 0x65, 0x73, 0x74, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x46, 0x75, 0x6c, 0x6c, + 0x54, 0x65, 0x73, 0x74, 0x10, 0x02, 0x2a, 0x27, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x10, + 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x10, 0x01, 0x32, + 0x88, 0x04, 0x0a, 0x0e, 0x4c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x45, 0x78, 0x69, 0x74, 0x12, 0x11, 0x2e, 0x6c, 0x69, 0x62, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, + 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x09, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, + 0x12, 0x11, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x06, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x13, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, + 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x35, + 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x16, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, + 0x65, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x1a, + 0x12, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, + 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x32, 0x0a, 0x06, 0x53, 0x65, 0x74, 0x54, 0x75, 0x6e, 0x12, + 0x12, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x75, 0x6e, + 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x04, 0x53, 0x74, 0x6f, + 0x70, 0x12, 0x11, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x04, 0x54, 0x65, + 0x73, 0x74, 0x12, 0x10, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x54, 0x65, 0x73, + 0x74, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x54, + 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3f, 0x0a, 0x0a, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, + 0x65, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, + 0x17, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, + 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x4e, 0x0a, 0x14, 0x4c, 0x69, + 0x73, 0x74, 0x56, 0x32, 0x72, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x11, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x21, 0x2e, 0x6c, 0x69, 0x62, 0x63, 0x6f, 0x72, 0x65, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x56, 0x32, 0x72, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x42, 0x12, 0x5a, 0x10, 0x6e, 0x65, + 0x6b, 0x6f, 0x72, 0x61, 0x79, 0x5f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_libcore_proto_rawDescOnce sync.Once + file_libcore_proto_rawDescData = file_libcore_proto_rawDesc +) + +func file_libcore_proto_rawDescGZIP() []byte { + file_libcore_proto_rawDescOnce.Do(func() { + file_libcore_proto_rawDescData = protoimpl.X.CompressGZIP(file_libcore_proto_rawDescData) + }) + return file_libcore_proto_rawDescData +} + +var file_libcore_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_libcore_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_libcore_proto_goTypes = []interface{}{ + (TestMode)(0), // 0: libcore.TestMode + (UpdateAction)(0), // 1: libcore.UpdateAction + (*EmptyReq)(nil), // 2: libcore.EmptyReq + (*EmptyResp)(nil), // 3: libcore.EmptyResp + (*ErrorResp)(nil), // 4: libcore.ErrorResp + (*LoadConfigReq)(nil), // 5: libcore.LoadConfigReq + (*SetTunReq)(nil), // 6: libcore.SetTunReq + (*TestReq)(nil), // 7: libcore.TestReq + (*TestResp)(nil), // 8: libcore.TestResp + (*QueryStatsReq)(nil), // 9: libcore.QueryStatsReq + (*QueryStatsResp)(nil), // 10: libcore.QueryStatsResp + (*UpdateReq)(nil), // 11: libcore.UpdateReq + (*UpdateResp)(nil), // 12: libcore.UpdateResp + (*ListV2RayConnectionsResp)(nil), // 13: libcore.ListV2rayConnectionsResp +} +var file_libcore_proto_depIdxs = []int32{ + 0, // 0: libcore.TestReq.mode:type_name -> libcore.TestMode + 5, // 1: libcore.TestReq.config:type_name -> libcore.LoadConfigReq + 1, // 2: libcore.UpdateReq.action:type_name -> libcore.UpdateAction + 2, // 3: libcore.LibcoreService.Exit:input_type -> libcore.EmptyReq + 2, // 4: libcore.LibcoreService.KeepAlive:input_type -> libcore.EmptyReq + 11, // 5: libcore.LibcoreService.Update:input_type -> libcore.UpdateReq + 5, // 6: libcore.LibcoreService.Start:input_type -> libcore.LoadConfigReq + 6, // 7: libcore.LibcoreService.SetTun:input_type -> libcore.SetTunReq + 2, // 8: libcore.LibcoreService.Stop:input_type -> libcore.EmptyReq + 7, // 9: libcore.LibcoreService.Test:input_type -> libcore.TestReq + 9, // 10: libcore.LibcoreService.QueryStats:input_type -> libcore.QueryStatsReq + 2, // 11: libcore.LibcoreService.ListV2rayConnections:input_type -> libcore.EmptyReq + 3, // 12: libcore.LibcoreService.Exit:output_type -> libcore.EmptyResp + 3, // 13: libcore.LibcoreService.KeepAlive:output_type -> libcore.EmptyResp + 12, // 14: libcore.LibcoreService.Update:output_type -> libcore.UpdateResp + 4, // 15: libcore.LibcoreService.Start:output_type -> libcore.ErrorResp + 4, // 16: libcore.LibcoreService.SetTun:output_type -> libcore.ErrorResp + 4, // 17: libcore.LibcoreService.Stop:output_type -> libcore.ErrorResp + 8, // 18: libcore.LibcoreService.Test:output_type -> libcore.TestResp + 10, // 19: libcore.LibcoreService.QueryStats:output_type -> libcore.QueryStatsResp + 13, // 20: libcore.LibcoreService.ListV2rayConnections:output_type -> libcore.ListV2rayConnectionsResp + 12, // [12:21] is the sub-list for method output_type + 3, // [3:12] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_libcore_proto_init() } +func file_libcore_proto_init() { + if File_libcore_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_libcore_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EmptyReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EmptyResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ErrorResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LoadConfigReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetTunReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TestReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TestResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*QueryStatsReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*QueryStatsResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_libcore_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListV2RayConnectionsResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_libcore_proto_rawDesc, + NumEnums: 2, + NumMessages: 12, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_libcore_proto_goTypes, + DependencyIndexes: file_libcore_proto_depIdxs, + EnumInfos: file_libcore_proto_enumTypes, + MessageInfos: file_libcore_proto_msgTypes, + }.Build() + File_libcore_proto = out.File + file_libcore_proto_rawDesc = nil + file_libcore_proto_goTypes = nil + file_libcore_proto_depIdxs = nil +} diff --git a/go/gen/libcore.proto b/go/gen/libcore.proto new file mode 100644 index 0000000..e22f1be --- /dev/null +++ b/go/gen/libcore.proto @@ -0,0 +1,96 @@ +syntax = "proto3"; + +package libcore; +option go_package = "nekoray_core/gen"; + +service LibcoreService { + rpc Exit(EmptyReq) returns (EmptyResp) {} + rpc KeepAlive(EmptyReq) returns (EmptyResp) {} + rpc Update(UpdateReq) returns (UpdateResp) {} + // + rpc Start(LoadConfigReq) returns (ErrorResp) {} + rpc SetTun(SetTunReq) returns (ErrorResp) {} + rpc Stop(EmptyReq) returns (ErrorResp) {} + rpc Test(TestReq) returns (TestResp) {} + rpc QueryStats(QueryStatsReq) returns (QueryStatsResp) {} + rpc ListV2rayConnections(EmptyReq) returns (ListV2rayConnectionsResp) {} +} + +message EmptyReq {} + +message EmptyResp {} + +message ErrorResp { + string error = 1; +} + +message LoadConfigReq { + string coreConfig = 1; + string tryDomains = 2; +} + +message SetTunReq { + string name = 1; + int32 mtu = 2; + int32 implementation = 3; + bool fakedns = 4; +} + +enum TestMode { + TcpPing = 0; + UrlTest = 1; + FullTest = 2; +} + +message TestReq { + TestMode mode = 1; + int32 timeout = 6; + // TcpPing + string address = 2; + // UrlTest + LoadConfigReq config = 3; + string inbound = 4; + string url = 5; + // FullTest + string in_address = 7; + bool full_latency = 8; + bool full_speed = 9; + bool full_in_out = 10; + bool full_nat = 11; +} + +message TestResp { + string error = 1; + int32 ms = 2; + string full_report = 3; +} + +message QueryStatsReq{ + string tag = 1; + string direct = 2; +} + +message QueryStatsResp{ + int64 traffic = 1; +} + +enum UpdateAction { + Check = 0; + Download = 1; +} + +message UpdateReq { + UpdateAction action = 1; +} + +message UpdateResp { + string error = 1; + string assets_name = 2; + string download_url = 3; + string release_url = 4; + string release_note = 5; +} + +message ListV2rayConnectionsResp { + string matsuri_connections_json = 1; +} diff --git a/go/gen/libcore_grpc.pb.go b/go/gen/libcore_grpc.pb.go new file mode 100644 index 0000000..92c3843 --- /dev/null +++ b/go/gen/libcore_grpc.pb.go @@ -0,0 +1,395 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.21.4 +// source: libcore.proto + +package gen + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// LibcoreServiceClient is the client API for LibcoreService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type LibcoreServiceClient interface { + Exit(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*EmptyResp, error) + KeepAlive(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*EmptyResp, error) + Update(ctx context.Context, in *UpdateReq, opts ...grpc.CallOption) (*UpdateResp, error) + // + Start(ctx context.Context, in *LoadConfigReq, opts ...grpc.CallOption) (*ErrorResp, error) + SetTun(ctx context.Context, in *SetTunReq, opts ...grpc.CallOption) (*ErrorResp, error) + Stop(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*ErrorResp, error) + Test(ctx context.Context, in *TestReq, opts ...grpc.CallOption) (*TestResp, error) + QueryStats(ctx context.Context, in *QueryStatsReq, opts ...grpc.CallOption) (*QueryStatsResp, error) + ListV2RayConnections(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*ListV2RayConnectionsResp, error) +} + +type libcoreServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewLibcoreServiceClient(cc grpc.ClientConnInterface) LibcoreServiceClient { + return &libcoreServiceClient{cc} +} + +func (c *libcoreServiceClient) Exit(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*EmptyResp, error) { + out := new(EmptyResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/Exit", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) KeepAlive(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*EmptyResp, error) { + out := new(EmptyResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/KeepAlive", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) Update(ctx context.Context, in *UpdateReq, opts ...grpc.CallOption) (*UpdateResp, error) { + out := new(UpdateResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/Update", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) Start(ctx context.Context, in *LoadConfigReq, opts ...grpc.CallOption) (*ErrorResp, error) { + out := new(ErrorResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/Start", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) SetTun(ctx context.Context, in *SetTunReq, opts ...grpc.CallOption) (*ErrorResp, error) { + out := new(ErrorResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/SetTun", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) Stop(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*ErrorResp, error) { + out := new(ErrorResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/Stop", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) Test(ctx context.Context, in *TestReq, opts ...grpc.CallOption) (*TestResp, error) { + out := new(TestResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/Test", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) QueryStats(ctx context.Context, in *QueryStatsReq, opts ...grpc.CallOption) (*QueryStatsResp, error) { + out := new(QueryStatsResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/QueryStats", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *libcoreServiceClient) ListV2RayConnections(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*ListV2RayConnectionsResp, error) { + out := new(ListV2RayConnectionsResp) + err := c.cc.Invoke(ctx, "/libcore.LibcoreService/ListV2rayConnections", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// LibcoreServiceServer is the server API for LibcoreService service. +// All implementations must embed UnimplementedLibcoreServiceServer +// for forward compatibility +type LibcoreServiceServer interface { + Exit(context.Context, *EmptyReq) (*EmptyResp, error) + KeepAlive(context.Context, *EmptyReq) (*EmptyResp, error) + Update(context.Context, *UpdateReq) (*UpdateResp, error) + // + Start(context.Context, *LoadConfigReq) (*ErrorResp, error) + SetTun(context.Context, *SetTunReq) (*ErrorResp, error) + Stop(context.Context, *EmptyReq) (*ErrorResp, error) + Test(context.Context, *TestReq) (*TestResp, error) + QueryStats(context.Context, *QueryStatsReq) (*QueryStatsResp, error) + ListV2RayConnections(context.Context, *EmptyReq) (*ListV2RayConnectionsResp, error) + mustEmbedUnimplementedLibcoreServiceServer() +} + +// UnimplementedLibcoreServiceServer must be embedded to have forward compatible implementations. +type UnimplementedLibcoreServiceServer struct { +} + +func (UnimplementedLibcoreServiceServer) Exit(context.Context, *EmptyReq) (*EmptyResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method Exit not implemented") +} +func (UnimplementedLibcoreServiceServer) KeepAlive(context.Context, *EmptyReq) (*EmptyResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method KeepAlive not implemented") +} +func (UnimplementedLibcoreServiceServer) Update(context.Context, *UpdateReq) (*UpdateResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method Update not implemented") +} +func (UnimplementedLibcoreServiceServer) Start(context.Context, *LoadConfigReq) (*ErrorResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method Start not implemented") +} +func (UnimplementedLibcoreServiceServer) SetTun(context.Context, *SetTunReq) (*ErrorResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetTun not implemented") +} +func (UnimplementedLibcoreServiceServer) Stop(context.Context, *EmptyReq) (*ErrorResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method Stop not implemented") +} +func (UnimplementedLibcoreServiceServer) Test(context.Context, *TestReq) (*TestResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method Test not implemented") +} +func (UnimplementedLibcoreServiceServer) QueryStats(context.Context, *QueryStatsReq) (*QueryStatsResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method QueryStats not implemented") +} +func (UnimplementedLibcoreServiceServer) ListV2RayConnections(context.Context, *EmptyReq) (*ListV2RayConnectionsResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListV2RayConnections not implemented") +} +func (UnimplementedLibcoreServiceServer) mustEmbedUnimplementedLibcoreServiceServer() {} + +// UnsafeLibcoreServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to LibcoreServiceServer will +// result in compilation errors. +type UnsafeLibcoreServiceServer interface { + mustEmbedUnimplementedLibcoreServiceServer() +} + +func RegisterLibcoreServiceServer(s grpc.ServiceRegistrar, srv LibcoreServiceServer) { + s.RegisterService(&LibcoreService_ServiceDesc, srv) +} + +func _LibcoreService_Exit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EmptyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).Exit(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/Exit", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).Exit(ctx, req.(*EmptyReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_KeepAlive_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EmptyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).KeepAlive(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/KeepAlive", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).KeepAlive(ctx, req.(*EmptyReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).Update(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/Update", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).Update(ctx, req.(*UpdateReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_Start_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LoadConfigReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).Start(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/Start", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).Start(ctx, req.(*LoadConfigReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_SetTun_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetTunReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).SetTun(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/SetTun", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).SetTun(ctx, req.(*SetTunReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_Stop_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EmptyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).Stop(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/Stop", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).Stop(ctx, req.(*EmptyReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_Test_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TestReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).Test(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/Test", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).Test(ctx, req.(*TestReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_QueryStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryStatsReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).QueryStats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/QueryStats", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).QueryStats(ctx, req.(*QueryStatsReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _LibcoreService_ListV2RayConnections_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EmptyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LibcoreServiceServer).ListV2RayConnections(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/libcore.LibcoreService/ListV2rayConnections", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LibcoreServiceServer).ListV2RayConnections(ctx, req.(*EmptyReq)) + } + return interceptor(ctx, in, info, handler) +} + +// LibcoreService_ServiceDesc is the grpc.ServiceDesc for LibcoreService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var LibcoreService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "libcore.LibcoreService", + HandlerType: (*LibcoreServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Exit", + Handler: _LibcoreService_Exit_Handler, + }, + { + MethodName: "KeepAlive", + Handler: _LibcoreService_KeepAlive_Handler, + }, + { + MethodName: "Update", + Handler: _LibcoreService_Update_Handler, + }, + { + MethodName: "Start", + Handler: _LibcoreService_Start_Handler, + }, + { + MethodName: "SetTun", + Handler: _LibcoreService_SetTun_Handler, + }, + { + MethodName: "Stop", + Handler: _LibcoreService_Stop_Handler, + }, + { + MethodName: "Test", + Handler: _LibcoreService_Test_Handler, + }, + { + MethodName: "QueryStats", + Handler: _LibcoreService_QueryStats_Handler, + }, + { + MethodName: "ListV2rayConnections", + Handler: _LibcoreService_ListV2RayConnections_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "libcore.proto", +} diff --git a/go/gen/update_proto.sh b/go/gen/update_proto.sh new file mode 100644 index 0000000..709ed22 --- /dev/null +++ b/go/gen/update_proto.sh @@ -0,0 +1,3 @@ +protoc -I . --go_out=. --go_opt paths=source_relative --go-grpc_out=. --go-grpc_opt paths=source_relative libcore.proto + +# protoc -I . --cpp_out=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` libcore.proto diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..9d1ceec --- /dev/null +++ b/go/go.mod @@ -0,0 +1,93 @@ +module nekoray_core + +go 1.18 + +require ( + github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 + github.com/jsimonetti/rtnetlink v1.2.0 + github.com/sirupsen/logrus v1.8.1 + github.com/v2fly/v2ray-core/v5 v5.0.0 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a + google.golang.org/grpc v1.48.0 + google.golang.org/protobuf v1.28.1 + kernel.org/pub/linux/libs/security/libcap/cap v1.2.64 + libcore v1.0.0 +) + +require ( + github.com/Dreamacro/clash v1.9.0 // indirect + github.com/Dreamacro/go-shadowsocks2 v0.1.7 // indirect + github.com/adrg/xdg v0.4.0 // indirect + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 // indirect + github.com/boljen/go-bitmap v0.0.0-20151001105940-23cd2fb0ce7d // indirect + github.com/cheekybits/genny v1.0.0 // indirect + github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d // indirect + github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb // indirect + github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect + github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152 // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/geeksbaek/seed v0.0.0-20180909040025-2a7f5fb92e22 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/jhump/protoreflect v1.12.0 // indirect + github.com/josharian/native v1.0.0 // indirect + github.com/kierdavis/cfb8 v0.0.0-20180105024805-3a17c36ee2f8 // indirect + github.com/klauspost/cpuid v1.2.3 // indirect + github.com/klauspost/reedsolomon v1.9.3 // indirect + github.com/lucas-clemente/quic-go v0.28.1 // indirect + github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect + github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect + github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect + github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect + github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 // indirect + github.com/mdlayher/netlink v1.6.0 // indirect + github.com/mdlayher/socket v0.1.1 // indirect + github.com/miekg/dns v1.1.50 // indirect + github.com/mustafaturan/bus v1.0.2 // indirect + github.com/mustafaturan/monoton v1.0.0 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/onsi/ginkgo v1.16.5 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pion/dtls/v2 v2.0.0-rc.7 // indirect + github.com/pion/logging v0.2.2 // indirect + github.com/pion/sctp v1.7.6 // indirect + github.com/pires/go-proxyproto v0.6.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect + github.com/sagernet/gomobile v0.0.0-20210905032500-701a995ff844 // indirect + github.com/sagernet/libping v0.1.1 // indirect + github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4 // indirect + github.com/seiflotfy/cuckoofilter v0.0.0-20220312154859-af7fbb8e765b // indirect + github.com/ulikunitz/xz v0.5.10 // indirect + github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 // indirect + github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect + github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432 // indirect + github.com/xtaci/smux v1.5.16 // indirect + go.starlark.net v0.0.0-20220302181546-5411bad688d1 // indirect + go.uber.org/automaxprocs v1.4.0 // indirect + go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect + go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect + golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 // indirect + golang.org/x/mod v0.5.1 // indirect + golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect + golang.org/x/tools v0.1.9 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gvisor.dev/gvisor v0.0.0 // indirect + inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 // indirect + kernel.org/pub/linux/libs/security/libcap/psx v1.2.64 // indirect +) + +replace libcore v1.0.0 => ../../Matsuri/libcore + +replace github.com/v2fly/v2ray-core/v5 v5.0.0 => ../../v2ray-core + +replace gvisor.dev/gvisor => github.com/sagernet/gvisor v0.0.0-20220402114650-763d12dc953e diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..0232293 --- /dev/null +++ b/go/go.sum @@ -0,0 +1,737 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3 h1:AVXDdKsrtX33oR9fbCMu/+c1o8Ofjq6Ku/MInaLVg5Y= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Dreamacro/clash v1.9.0 h1:IfmPW86Klngu0iQ4LL6Bhxcvtr+QaI7Oppa9qRPX/Q8= +github.com/Dreamacro/clash v1.9.0/go.mod h1:vOzDB9KKD/PirNdSlsH4soMl1xF5lk8SwNQiVY5UacE= +github.com/Dreamacro/go-shadowsocks2 v0.1.7 h1:8CtbE1HoPPMfrQZGXmlluq6dO2lL31W6WRRE8fabc4Q= +github.com/Dreamacro/go-shadowsocks2 v0.1.7/go.mod h1:8p5G4cAj5ZlXwUR+Ww63gfSikr8kvw8uw3TDwLAJpUc= +github.com/FlowerWrong/water v0.0.0-20180301012659-01a4eaa1f6f2/go.mod h1:xrG5L7lq7T2DLnPr2frMnL906CNEoKRwLB+VYFhPq2w= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 h1:+JkXLHME8vLJafGhOH4aoV2Iu8bR55nU6iKMVfYVLjY= +github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1/go.mod h1:nuudZmJhzWtx2212z+pkuy7B6nkBqa+xwNXZHL1j8cg= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/boljen/go-bitmap v0.0.0-20151001105940-23cd2fb0ce7d h1:zsO4lp+bjv5XvPTF58Vq+qgmZEYZttJK+CWtSZhKenI= +github.com/boljen/go-bitmap v0.0.0-20151001105940-23cd2fb0ce7d/go.mod h1:f1iKL6ZhUWvbk7PdWVmOaak10o86cqMUYEmn1CZNGEI= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.8.1 h1:bLSSEbBLqGPXxls55pGr5qWZaTqcmfDJHhou7t254ao= +github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d h1:CPqTNIigGweVPT4CYb+OO2E6XyRKFOmvTHwWRLgCAlE= +github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d/go.mod h1:QX5ZVULjAfZJux/W62Y91HvCh9hyW6enAwcrrv/sLj0= +github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb h1:zXpN5126w/mhECTkqazBkrOJIMatbPP71aSIDR5UuW4= +github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb/go.mod h1:F7WkpqJj9t98ePxB/WJGQTIDeOVPuSJ3qdn6JUjg170= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0= +github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152 h1:ED31mPIxDJnrLt9W9dH5xgd/6KjzEACKHBVGQ33czc0= +github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152/go.mod h1:I9fhc/EvSg88cDxmfQ47v35Ssz9rlFunL/KY0A1JAYI= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521 h1:fBHFH+Y/GPGFGo7LIrErQc3p2MeAhoIQNgaxPWYsSxk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/geeksbaek/seed v0.0.0-20180909040025-2a7f5fb92e22 h1:CdVtqYWYGIEuYCbtyx6BVMKOcO0N6lKm99cR1DZubAs= +github.com/geeksbaek/seed v0.0.0-20180909040025-2a7f5fb92e22/go.mod h1:YS1s0XuwU13tHT0WeYeUXUwGk1m8WZvSbK9cx/kY1SE= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang-collections/go-datastructures v0.0.0-20150211160725-59788d5eb259/go.mod h1:9Qcha0gTWLw//0VNka1Cbnjvg3pNKGFdAm7E9sBabxE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20210420193930-a4630ec28c79/go.mod h1:Opf9rtYVq0eTyX+aRVmRO9hE8ERAozcdrBxWG9Q6mkQ= +github.com/gopherjs/websocket v0.0.0-20191103002815-9a42957e2b3a/go.mod h1:jd+zY81Fx2lC4bfw58+Rflg1srqmedQjbBUejKOjYNY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= +github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= +github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= +github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= +github.com/jhump/protoreflect v1.12.0 h1:1NQ4FpWMgn3by/n1X0fbeKEUxP1wBt7+Oitpv01HR10= +github.com/jhump/protoreflect v1.12.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk= +github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/jsimonetti/rtnetlink v1.2.0 h1:KlwYLoRXgirTFbh1aVI6MJ7i+R/zJr+JkyhlIW1X3z4= +github.com/jsimonetti/rtnetlink v1.2.0/go.mod h1:RA0RtDj3hv4g6l/Y4B7RubIQkdTDAwXfMW/8bMaZ0FY= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kierdavis/cfb8 v0.0.0-20180105024805-3a17c36ee2f8 h1:QxgFSDEqLP8ZsmVm/Qke0HP6JLV7EB93vtWK7noU1Sw= +github.com/kierdavis/cfb8 v0.0.0-20180105024805-3a17c36ee2f8/go.mod h1:uL2TcUivilrs0kPsqUwIf8XHAcmkSjsfrzSgAJwS0TI= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs= +github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY= +github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucas-clemente/quic-go v0.28.1 h1:Uo0lvVxWg5la9gflIF9lwa39ONq85Xq2D91YNEIslzU= +github.com/lucas-clemente/quic-go v0.28.1/go.mod h1:oGz5DKK41cJt5+773+BSO9BXDsREY4HLf7+0odGAPO0= +github.com/lunixbochs/struc v0.0.0-20190916212049-a5c72983bc42/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= +github.com/marten-seemann/qtls-go1-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtUCmny98/1uqQ= +github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= +github.com/marten-seemann/qtls-go1-17 v0.1.2 h1:JADBlm0LYiVbuSySCHeY863dNkcpMmDR7s0bLKJeYlQ= +github.com/marten-seemann/qtls-go1-17 v0.1.2/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s= +github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM= +github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= +github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 h1:7m/WlWcSROrcK5NxuXaxYD32BZqe/LEEnBrWcH/cOqQ= +github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0= +github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA= +github.com/mdlayher/socket v0.1.1 h1:q3uOGirUPfAV2MUoaC7BavjQ154J7+JOkTWyiV+intI= +github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mustafaturan/bus v1.0.2 h1:2x3ErwZ0uUPwwZ5ZZoknEQprdaxr68Yl3mY8jDye1Ws= +github.com/mustafaturan/bus v1.0.2/go.mod h1:h7gfehm8TThv4Dcaa+wDQG7r7j6p74v+7ftr0Rq9i1Q= +github.com/mustafaturan/monoton v0.3.1/go.mod h1:FOnE7NV3s3EWPXb8/7+/OSdiMBbdlkV0Lz8p1dc+vy8= +github.com/mustafaturan/monoton v1.0.0 h1:8SCej+JiNn0lyps7V+Jzc1CRAkDR4EZPWrTupQ61YCQ= +github.com/mustafaturan/monoton v1.0.0/go.mod h1:FOnE7NV3s3EWPXb8/7+/OSdiMBbdlkV0Lz8p1dc+vy8= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= +github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pion/dtls/v2 v2.0.0-rc.7 h1:LDAIQDt1pcuAIJs7Q2EZ3PSl8MseCFA2nCW0YYSYCx0= +github.com/pion/dtls/v2 v2.0.0-rc.7/go.mod h1:U199DvHpRBN0muE9+tVN4TMy1jvEhZIZ63lk4xkvVSk= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/sctp v1.7.6 h1:8qZTdJtbKfAns/Hv5L0PAj8FyXcsKhMH1pKUCGisQg4= +github.com/pion/sctp v1.7.6/go.mod h1:ichkYQ5tlgCQwEwvgfdcAolqx1nHbYCxo4D7zK/K0X8= +github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk= +github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= +github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8= +github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= +github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagernet/gomobile v0.0.0-20210905032500-701a995ff844 h1:o7izBZde2L5foPbQdYisY03y4+6T6UcUXOwR5MyQpUk= +github.com/sagernet/gomobile v0.0.0-20210905032500-701a995ff844/go.mod h1:2Xj8wyq0y6G6B1gCNTzRcKqo+cyVKatMTNWUmxNYfI4= +github.com/sagernet/gvisor v0.0.0-20220402114650-763d12dc953e h1:Y4avBAtZ59OWvLl6zP9sF62jtMEVRPIH78IQctq9aXQ= +github.com/sagernet/gvisor v0.0.0-20220402114650-763d12dc953e/go.mod h1:tWwEcFvJavs154OdjFCw78axNrsDlz4Zh8jvPqwcpGI= +github.com/sagernet/libping v0.1.1 h1:uNMN/02fQmRbsgJ0EuxuM/Upq8FrVP4Xj2+LlYviIOs= +github.com/sagernet/libping v0.1.1/go.mod h1:FhmyYM8L32JaKI08noqoS5cK+Gw/Q+4VDnI9WvP6Sp8= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4 h1:zOjq+1/uLzn/Xo40stbvjIY/yehG0+mfmlsiEmc0xmQ= +github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4/go.mod h1:aI+8yClBW+1uovkHw6HM01YXnYB8vohtB9C83wzx34E= +github.com/seiflotfy/cuckoofilter v0.0.0-20200416141329-862a88987de7/go.mod h1:ET5mVvNjwaGXRgZxO9UZr7X+8eAf87AfIYNwRSp9s4Y= +github.com/seiflotfy/cuckoofilter v0.0.0-20220312154859-af7fbb8e765b h1:wHoB6ZYEnIVizebcj419LbN4Tagk7RDFiudRFKyzzmo= +github.com/seiflotfy/cuckoofilter v0.0.0-20220312154859-af7fbb8e765b/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/txthinking/runnergroup v0.0.0-20200327135940-540a793bb997/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM= +github.com/txthinking/socks5 v0.0.0-20200327133705-caf148ab5e9d/go.mod h1:d3n8NJ6QMRb6I/WAlp4z5ZPAoaeqDmX5NgVZA0mhe+I= +github.com/txthinking/x v0.0.0-20200330144832-5ad2416896a9/go.mod h1:WgqbSEmUYSjEV3B1qmee/PpP2NYEz4bL9/+mF1ma+s4= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 h1:4Yh46CVE3k/lPq6hUbEdbB1u1anRBXLewm3k+L0iOMc= +github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08/go.mod h1:KAuQNm+LWQCOFqdBcUgihPzRpVXRKzGbTNhfEfRZ4wY= +github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= +github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432 h1:I/ATawgO2RerCq9ACwL0wBB8xNXZdE3J+93MCEHReRs= +github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432/go.mod h1:QN7Go2ftTVfx0aCTh9RXHV8pkpi0FtmbwQw40dy61wQ= +github.com/xtaci/smux v1.5.12/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +github.com/xtaci/smux v1.5.15/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +github.com/xtaci/smux v1.5.16 h1:FBPYOkW8ZTjLKUM4LI4xnnuuDC8CQ/dB04HD519WoEk= +github.com/xtaci/smux v1.5.16/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.starlark.net v0.0.0-20220302181546-5411bad688d1 h1:i0Sz4b+qJi5xwOaFZqZ+RNHkIpaKLDofei/Glt+PMNc= +go.starlark.net v0.0.0-20220302181546-5411bad688d1/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/automaxprocs v1.4.0 h1:CpDZl6aOlLhReez+8S3eEotD7Jx0Os++lemPlMULQP0= +go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 h1:Tx9kY6yUkLge/pFG7IEMwDZy6CS2ajFc9TvQdPCW0uA= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s= +golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 h1:B333XXssMuKQeBwiNODx4TupZy7bf4sxFZnN2ZOcvUE= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= +golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb h1:ZrsicilzPCS/Xr8qtBZZLpy4P9TYXAfl49ctG1/5tgw= +google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw= +inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8= +kernel.org/pub/linux/libs/security/libcap/cap v1.2.64 h1:E1U4GNGSXEdzQUT+mop0iYawCNXDUU46Y8nfodb+ZY0= +kernel.org/pub/linux/libs/security/libcap/cap v1.2.64/go.mod h1:gtBlgvjXflnxHng9/3bXyXG3XmBYKDt35zu+lNmB+IA= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.64 h1:zlw/KoDjEObyddpFcvLiuu8frEvyEwVNc62WZQBp68w= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.64/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/go/grpc.go b/go/grpc.go new file mode 100644 index 0000000..f7f691f --- /dev/null +++ b/go/grpc.go @@ -0,0 +1,223 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "flag" + "fmt" + "io" + "libcore" + "log" + "nekoray_core/gen" + "net" + "net/http" + "os" + "runtime" + "strconv" + "strings" + "time" + _ "unsafe" + + grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" + v2rayNet "github.com/v2fly/v2ray-core/v5/common/net" + "google.golang.org/grpc" +) + +type server struct { + gen.LibcoreServiceServer +} + +var last time.Time +var nekoray_debug bool + +func (s *server) KeepAlive(ctx context.Context, in *gen.EmptyReq) (*gen.EmptyResp, error) { + last = time.Now() + return &gen.EmptyResp{}, nil +} + +func NekorayCore() { + _token := flag.String("token", "", "") + _port := flag.Int("port", 19810, "") + _debug := flag.Bool("debug", false, "") + flag.CommandLine.Parse(os.Args[2:]) + + nekoray_debug = *_debug + + go func() { + t := time.NewTicker(time.Second * 10) + for { + <-t.C + if last.Add(time.Second * 10).Before(time.Now()) { + fmt.Println("Exit due to inactive") + os.Exit(0) + } + } + }() + + // Libcore + setupCore() + + // GRPC + lis, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(*_port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + + token := *_token + if token == "" { + os.Stderr.WriteString("Please set a token: ") + s := bufio.NewScanner(os.Stdin) + if s.Scan() { + token = strings.TrimSpace(s.Text()) + } + } + if token == "" { + fmt.Println("You must set a token") + os.Exit(0) + } + os.Stderr.WriteString("token is set\n") + + auther := Authenticator{ + Token: token, + } + + s := grpc.NewServer( + grpc.StreamInterceptor(grpc_auth.StreamServerInterceptor(auther.Authenticate)), + grpc.UnaryInterceptor(grpc_auth.UnaryServerInterceptor(auther.Authenticate)), + ) + gen.RegisterLibcoreServiceServer(s, &server{}) + + log.Printf("neokray grpc server listening at %v", lis.Addr()) + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} + +// PROXY + +func getProxyHttpClient(_instance *libcore.V2RayInstance) *http.Client { + dailContext := func(ctx context.Context, network, addr string) (net.Conn, error) { + dest, err := v2rayNet.ParseDestination(fmt.Sprintf("%s:%s", network, addr)) + if err != nil { + return nil, err + } + return _instance.DialContext(ctx, dest) + } + + transport := &http.Transport{ + TLSHandshakeTimeout: time.Second * 3, + ResponseHeaderTimeout: time.Second * 3, + } + if _instance != nil { + transport.DialContext = dailContext + } + + client := &http.Client{ + Transport: transport, + } + + return client +} + +// UPDATE + +var update_download_url, update_download_as string + +func (s *server) Update(ctx context.Context, in *gen.UpdateReq) (*gen.UpdateResp, error) { + ret := &gen.UpdateResp{} + + client := getProxyHttpClient(instance) + + if in.Action == gen.UpdateAction_Check { // Check update + ctx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/repos/MatsuriDayo/nekoray/releases", nil) + resp, err := client.Do(req) + if err != nil { + ret.Error = err.Error() + return ret, nil + } + defer resp.Body.Close() + + v := []struct { + HtmlUrl string `json:"html_url"` + Assets []struct { + Name string `json:"name"` + BrowserDownloadUrl string `json:"browser_download_url"` + } `json:"assets"` + Prerelease bool `json:"prerelease"` + Body string `json:"body"` + }{} + err = json.NewDecoder(resp.Body).Decode(&v) + if err != nil { + ret.Error = err.Error() + return ret, nil + } + + nowVer := strings.TrimLeft(version_standalone, "nekoray-") + + var search string + if runtime.GOOS == "windows" && runtime.GOARCH == "amd64" { + search = "windows64" + update_download_as = "nekoray.zip" + } else if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" { + search = "linux64" + update_download_as = "nekoray.tar.gz" + } else { + ret.Error = "Not official support platform" + return ret, nil + } + + for _, release := range v { + if len(release.Assets) > 0 { + for _, asset := range release.Assets { + if strings.Contains(asset.Name, nowVer) { + return ret, nil // No update + } + if strings.Contains(asset.Name, search) { + if release.Prerelease { + continue + } + update_download_url = asset.BrowserDownloadUrl + ret.AssetsName = asset.Name + ret.DownloadUrl = asset.BrowserDownloadUrl + ret.ReleaseUrl = release.HtmlUrl + ret.ReleaseNote = release.Body + return ret, nil // update + } + } + } + } + } else { // Download update + if update_download_url == "" || update_download_as == "" { + ret.Error = "?" + return ret, nil + } + + req, _ := http.NewRequestWithContext(ctx, "GET", update_download_url, nil) + resp, err := client.Do(req) + if err != nil { + ret.Error = err.Error() + return ret, nil + } + defer resp.Body.Close() + + f, err := os.OpenFile("../"+update_download_as, os.O_TRUNC|os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + ret.Error = err.Error() + return ret, nil + } + defer f.Close() + + _, err = io.Copy(f, resp.Body) + if err != nil { + ret.Error = err.Error() + return ret, nil + } + f.Sync() + } + + return ret, nil +} diff --git a/go/import_extra.go b/go/import_extra.go new file mode 100644 index 0000000..90aaf7e --- /dev/null +++ b/go/import_extra.go @@ -0,0 +1,8 @@ +package main + +import ( + _ "github.com/v2fly/v2ray-core/v5/proxy/vless/inbound" + _ "github.com/v2fly/v2ray-core/v5/proxy/vless/outbound" + _ "github.com/v2fly/v2ray-core/v5/proxy/vlite/inbound" + _ "github.com/v2fly/v2ray-core/v5/proxy/vlite/outbound" +) diff --git a/go/iphlpapi/callback_windows.go b/go/iphlpapi/callback_windows.go new file mode 100644 index 0000000..1709ec5 --- /dev/null +++ b/go/iphlpapi/callback_windows.go @@ -0,0 +1,23 @@ +package iphlpapi + +import ( + "syscall" + "unsafe" +) + +func notifyRouteChange2(family uint16, callback uintptr, callerContext uintptr, initialNotification bool, notificationHandle *syscall.Handle) (ret error) { + var _p0 uint32 + if initialNotification { + _p0 = 1 + } + r0, _, _ := syscall.Syscall6(proc_notifyRouteChange2.Addr(), 5, uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle)), 0) + if r0 != 0 { + ret = syscall.Errno(r0) + } + return +} + +func RegisterNotifyRouteChange2(callback func(callerContext uintptr, row uintptr, notificationType uint32) uintptr, initialNotification bool) (handle syscall.Handle) { + notifyRouteChange2(syscall.AF_UNSPEC, syscall.NewCallback(callback), 0, initialNotification, &handle) + return +} diff --git a/go/iphlpapi/dll_windows.go b/go/iphlpapi/dll_windows.go new file mode 100644 index 0000000..6b9b2d6 --- /dev/null +++ b/go/iphlpapi/dll_windows.go @@ -0,0 +1,14 @@ +package iphlpapi + +import "syscall" + +var ( + proc_getIpForwardTable *syscall.LazyProc + proc_notifyRouteChange2 *syscall.LazyProc +) + +func init() { + iphlpapi := syscall.NewLazyDLL("iphlpapi.dll") + proc_getIpForwardTable = iphlpapi.NewProc("GetIpForwardTable") + proc_notifyRouteChange2 = iphlpapi.NewProc("NotifyRouteChange2") +} diff --git a/go/iphlpapi/route_windows.go b/go/iphlpapi/route_windows.go new file mode 100644 index 0000000..e22b1a2 --- /dev/null +++ b/go/iphlpapi/route_windows.go @@ -0,0 +1,87 @@ +package iphlpapi + +import ( + "fmt" + "net" + "unsafe" +) + +/* +对于路由表,预期的方法是: +查询 0.0.0.0/0 获得原始默认路由 +然后为 vpn 服务器添加默认路由 +之后就根据需要下发vpn路由完事。 +对于0.0.0.0/0 vpn 路由,可以尝试更低的跃点数,也可以尝试分为2个。 +重新连接时可以删除vpn接口的所有非链路路由表。 +路由表格式: +目标网络 uint32 掩码位数 byte低6位 vpn/默认网关 byte 高1位 +*/ + +// 太低的值添加路由时会返回 106 错误 +const routeMetric = 93 + +type RouteRow struct { + ForwardDest [4]byte //目标网络 + ForwardMask [4]byte //掩码 + ForwardPolicy uint32 //ForwardPolicy:0x0 + ForwardNextHop [4]byte //网关 + ForwardIfIndex uint32 // 网卡索引 id + ForwardType uint32 //3 本地接口 4 远端接口 + ForwardProto uint32 //3静态路由 2本地接口 5EGP网关 + ForwardAge uint32 //存在时间 秒 + ForwardNextHopAS uint32 //下一跳自治域号码 0 + ForwardMetric1 uint32 //度量衡(跃点数),根据 ForwardProto 不同意义不同。 + ForwardMetric2 uint32 + ForwardMetric3 uint32 + ForwardMetric4 uint32 + ForwardMetric5 uint32 +} + +func (rr *RouteRow) GetForwardDest() net.IP { + return net.IP(rr.ForwardDest[:]) +} +func (rr *RouteRow) GetForwardMask() net.IP { + return net.IP(rr.ForwardMask[:]) +} +func (rr *RouteRow) GetForwardNextHop() net.IP { + return net.IP(rr.ForwardNextHop[:]) +} + +func GetRoutes() ([]RouteRow, error) { + buf := make([]byte, 4+unsafe.Sizeof(RouteRow{})) + buf_len := uint32(len(buf)) + + proc_getIpForwardTable.Call(uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&buf_len)), 0) + + var r1 uintptr + for i := 0; i < 5; i++ { + buf = make([]byte, buf_len) + r1, _, _ = proc_getIpForwardTable.Call(uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&buf_len)), 0) + if r1 == 122 { + continue + } + break + } + + if r1 != 0 { + return nil, fmt.Errorf("Failed to get the routing table, return value:%v", r1) + } + + num := *(*uint32)(unsafe.Pointer(&buf[0])) + routes := make([]RouteRow, num) + sr := uintptr(unsafe.Pointer(&buf[0])) + unsafe.Sizeof(num) + rowSize := unsafe.Sizeof(RouteRow{}) + + // 安全检查 + if len(buf) < int((unsafe.Sizeof(num) + rowSize*uintptr(num))) { + return nil, fmt.Errorf("System error: GetIpForwardTable returns the number is too long, beyond the buffer。") + } + + for i := uint32(0); i < num; i++ { + routes[i] = *((*RouteRow)(unsafe.Pointer(sr + (rowSize * uintptr(i))))) + } + + return routes, nil +} diff --git a/go/main.go b/go/main.go new file mode 100644 index 0000000..80f970d --- /dev/null +++ b/go/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "os" + _ "unsafe" + + "github.com/v2fly/v2ray-core/v5/main/commands" + "github.com/v2fly/v2ray-core/v5/main/commands/base" +) + +//go:linkname build github.com/v2fly/v2ray-core/v5.build +var build string + +var version_v2ray string = "N/A" +var version_standalone string = "N/A" + +func main() { + fmt.Println("V2Ray:", version_v2ray, "Version:", version_standalone) + fmt.Println() + + // nekoray + if len(os.Args) > 1 && os.Args[1] == "nekoray" { + NekorayCore() + return + } + + // toolbox + if len(os.Args) > 1 && os.Args[1] == "tool" { + ToolBox() + return + } + + build = "Matsuridayo/Nekoray" + main_v2ray_v5() +} + +func main_v2ray_v5() { + base.RootCommand.Long = "A unified platform for anti-censorship." + base.RegisterCommand(commands.CmdRun) + base.RegisterCommand(commands.CmdVersion) + base.RegisterCommand(commands.CmdTest) + base.SortLessFunc = runIsTheFirst + base.SortCommands() + base.Execute() +} + +func runIsTheFirst(i, j *base.Command) bool { + left := i.Name() + right := j.Name() + if left == "run" { + return true + } + if right == "run" { + return false + } + return left < right +} diff --git a/go/protect_bindinterface_windows.go b/go/protect_bindinterface_windows.go new file mode 100644 index 0000000..772206a --- /dev/null +++ b/go/protect_bindinterface_windows.go @@ -0,0 +1,139 @@ +package main + +import ( + "encoding/binary" + "log" + "nekoray_core/iphlpapi" + "net" + "strings" + "sync" + "syscall" + "unsafe" + + "github.com/v2fly/v2ray-core/v5/transport/internet" +) + +// https://docs.microsoft.com/en-us/windows/win32/api/ipmib/ns-ipmib-mib_ipforwardrow +var routes []iphlpapi.RouteRow +var interfaces []net.Interface +var lock sync.Mutex + +func init() { + internet.RegisterListenerController(func(network, address string, fd uintptr) error { + bindInterfaceIndex := getBindInterfaceIndex() + if bindInterfaceIndex != 0 { + if err := bindInterface(fd, bindInterfaceIndex, true, true); err != nil { + log.Println("bind inbound interface", err) + return err + } + } + return nil + }) + internet.RegisterDialerController(func(network, address string, fd uintptr) error { + bindInterfaceIndex := getBindInterfaceIndex() + if bindInterfaceIndex != 0 { + var v4, v6 bool + if strings.HasSuffix(network, "6") { + v4 = false + v6 = true + } else { + v4 = true + v6 = false + } + if err := bindInterface(fd, bindInterfaceIndex, v4, v6); err != nil { + log.Println("bind outbound interface", err) + return err + } + } + return nil + }) + iphlpapi.RegisterNotifyRouteChange2(func(callerContext uintptr, row uintptr, notificationType uint32) uintptr { + updateRoutes() + return 0 + }, true) +} + +func updateRoutes() { + lock.Lock() + defer lock.Unlock() + + var err error + routes, err = iphlpapi.GetRoutes() + if err != nil { + log.Println("warning: GetRoutes failed", err) + } + interfaces, err = net.Interfaces() + if err != nil { + log.Println("warning: Interfaces failed", err) + } +} + +func getBindInterfaceIndex() uint32 { + lock.Lock() + defer lock.Unlock() + + if routes == nil { + log.Println("warning: no routes info") + return 0 + } + if interfaces == nil { + log.Println("warning: no interfaces info") + return 0 + } + + var nextInterface int + for i, intf := range interfaces { + if intf.Name == "nekoray-tun" || intf.Name == "wintun" || intf.Name == "TunMax" { + if len(interfaces) > i+1 { + nextInterface = interfaces[i+1].Index + } + break + } + } + + // Not in VPN mode + if nextInterface == 0 { + return 0 + } + + for _, route := range routes { + // MIB_IPROUTE_TYPE_INDIRECT + if route.ForwardType == 4 { + // MIB_IPPROTO_NETMGMT + if route.ForwardProto == 3 { + if route.GetForwardMask().IsUnspecified() { + return route.ForwardIfIndex + } + } + } + } + + // Default route not found + return uint32(nextInterface) +} + +const ( + IP_UNICAST_IF = 31 // nolint: golint,stylecheck + IPV6_UNICAST_IF = 31 // nolint: golint,stylecheck +) + +func bindInterface(fd uintptr, interfaceIndex uint32, v4, v6 bool) error { + if v4 { + /* MSDN says for IPv4 this needs to be in net byte order, so that it's like an IP address with leading zeros. */ + bytes := make([]byte, 4) + binary.BigEndian.PutUint32(bytes, interfaceIndex) + interfaceIndex_v4 := *(*uint32)(unsafe.Pointer(&bytes[0])) + + if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, IP_UNICAST_IF, int(interfaceIndex_v4)); err != nil { + return err + } + } + + if v6 { + if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IPV6, IPV6_UNICAST_IF, int(interfaceIndex)); err != nil { + return err + } + } + + return nil +} diff --git a/go/protect_fwmark_linux.go b/go/protect_fwmark_linux.go new file mode 100644 index 0000000..737033d --- /dev/null +++ b/go/protect_fwmark_linux.go @@ -0,0 +1,105 @@ +package main + +import ( + "fmt" + "libcore/protect" + "log" + "strings" + "syscall" + + "github.com/jsimonetti/rtnetlink" + linuxcap "kernel.org/pub/linux/libs/security/libcap/cap" +) + +type fwmarkProtector struct{} + +var rtnetlink_conn *rtnetlink.Conn +var cap_net_admin = 0 + +func (f *fwmarkProtector) Protect(fd int32) bool { + if cap_net_admin == 0 { + str := strings.ToLower(linuxcap.GetProc().String()) + if strings.Contains(str, "cap_net_admin") || str == "=ep" { + cap_net_admin = 1 + } else { + cap_net_admin = -1 + } + } + + // check is in VPN mode + if is_fwmark_exist(514) { + if cap_net_admin == 1 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, 514); err != nil { + log.Println("syscall.SetsockoptInt:", err) + return false + } + } else { + if err := cmsgProtect(int(fd), "./protect"); err != nil { + log.Println("cmsgProtect:", err) + return false + } + } + } + return true +} + +func cmsgProtect(fd int, unixPath string) error { + socket, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) + if err != nil { + return err + } + defer syscall.Close(socket) + + syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Sec: 3}) + syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Sec: 3}) + + err = syscall.Connect(socket, &syscall.SockaddrUnix{Name: unixPath}) + if err != nil { + return err + } + + err = syscall.Sendmsg(socket, nil, syscall.UnixRights(fd), nil, 0) + if err != nil { + return err + } + + dummy := []byte{1} + n, err := syscall.Read(socket, dummy) + if err != nil { + return err + } + if n != 1 { + return fmt.Errorf("cmsgProtect protect failed") + } + return nil +} + +func is_fwmark_exist(number int) bool { + var err error + + if rtnetlink_conn == nil { + rtnetlink_conn, err = rtnetlink.Dial(nil) + if err != nil { + log.Println(err) + } + return false + } + + rules, err := rtnetlink_conn.Rule.List() + if err != nil { + rtnetlink_conn = nil + return false + } + + for _, rule := range rules { + if rule.Attributes != nil && rule.Attributes.FwMark != nil && uint32(number) == *rule.Attributes.FwMark { + return true + } + } + + return false +} + +func init() { + protect.FdProtector = &fwmarkProtector{} +} diff --git a/go/protect_server/protect_server_linux.go b/go/protect_server/protect_server_linux.go new file mode 100644 index 0000000..737a7ed --- /dev/null +++ b/go/protect_server/protect_server_linux.go @@ -0,0 +1,92 @@ +package protect_server + +import ( + "fmt" + "log" + "net" + "os" + "os/signal" + "reflect" + "syscall" +) + +func getOneFd(socket int) (int, error) { + // recvmsg + buf := make([]byte, syscall.CmsgSpace(4)) + _, _, _, _, err := syscall.Recvmsg(socket, nil, buf, 0) + if err != nil { + return 0, err + } + + // parse control msgs + var msgs []syscall.SocketControlMessage + msgs, err = syscall.ParseSocketControlMessage(buf) + + if len(msgs) != 1 { + return 0, fmt.Errorf("invaild msgs count: %d", len(msgs)) + } + + var fds []int + fds, err = syscall.ParseUnixRights(&msgs[0]) + if len(fds) != 1 { + return 0, fmt.Errorf("invaild fds count: %d", len(fds)) + } + return fds[0], nil +} + +// GetFdFromConn get net.Conn's file descriptor. +func GetFdFromConn(l net.Conn) int { + v := reflect.ValueOf(l) + netFD := reflect.Indirect(reflect.Indirect(v).FieldByName("fd")) + pfd := reflect.Indirect(netFD.FieldByName("pfd")) + fd := int(pfd.FieldByName("Sysfd").Int()) + return fd +} + +func ServeProtect(path string, fwmark int) { + os.Remove(path) + defer os.Remove(path) + + l, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"}) + if err != nil { + log.Fatal(err) + } + defer l.Close() + + os.Chmod(path, 0777) + + go func() { + for { + c, err := l.Accept() + if err != nil { + log.Println("Accept:", err) + return + } + + go func() { + socket := GetFdFromConn(c) + defer c.Close() + + fd, err := getOneFd(socket) + if err != nil { + log.Println("getOneFd:", err) + return + } + + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, fwmark); err != nil { + log.Println("syscall.SetsockoptInt:", err) + } + + if err == nil { + c.Write([]byte{1}) + } else { + c.Write([]byte{0}) + } + }() + } + }() + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + <-sigCh +} diff --git a/go/protect_server/protect_server_other.go b/go/protect_server/protect_server_other.go new file mode 100644 index 0000000..f6556c8 --- /dev/null +++ b/go/protect_server/protect_server_other.go @@ -0,0 +1,9 @@ +//go:build !linux + +package protect_server + +import "log" + +func ServeProtect(path string, fwmark int) { + log.Println("ServeProtect is not for this platform") +} diff --git a/go/toolbox_linux.go b/go/toolbox_linux.go new file mode 100644 index 0000000..8ce44b9 --- /dev/null +++ b/go/toolbox_linux.go @@ -0,0 +1,67 @@ +package main + +import ( + "flag" + "log" + "nekoray_core/protect_server" + "os" + + "github.com/jsimonetti/rtnetlink" + linuxcap "kernel.org/pub/linux/libs/security/libcap/cap" +) + +func ToolBox() { + // + var protectListenPath string + var protectFwMark int + // + flag.StringVar(&protectListenPath, "protect-listen-path", "", "Set unix protect server listen path (Linux ROOT only)") + flag.IntVar(&protectFwMark, "protect-fwmark", 0, "Set unix protect fwmark (Linux ROOT only)") + flag.CommandLine.Parse(os.Args[3:]) + // + switch os.Args[2] { + case "rule": + { + // Dial a connection to the rtnetlink socket + conn, err := rtnetlink.Dial(nil) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + // Request a list of rules + rules, err := conn.Rule.List() + if err != nil { + log.Fatal(err) + } + + for _, rule := range rules { + log.Printf("%+v", rule) + log.Printf("%+v", rule.Attributes) + } + + for _, rule := range rules { + if rule.Attributes.FwMark != nil { + log.Printf("%+v", rule.Attributes) + log.Println(*rule.Attributes.FwMark, *rule.Attributes.Table) + } + } + } + case "cap": + { + set := linuxcap.GetProc() + if set != nil { + log.Println(set) + } + } + case "protect": + { + if protectListenPath == "" { + log.Println("missing protect-listen-path") + return + } + log.Println(protectListenPath, protectFwMark) + protect_server.ServeProtect(protectListenPath, protectFwMark) + } + } +} diff --git a/go/toolbox_other.go b/go/toolbox_other.go new file mode 100644 index 0000000..b155827 --- /dev/null +++ b/go/toolbox_other.go @@ -0,0 +1,6 @@ +//go:build !windows && !linux + +package main + +func ToolBox() { +} diff --git a/go/toolbox_windows.go b/go/toolbox_windows.go new file mode 100644 index 0000000..8716ee3 --- /dev/null +++ b/go/toolbox_windows.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + "net" + "os" +) + +func ToolBox() { + switch os.Args[2] { + case "if": + { + intfs, err := net.Interfaces() + if err != nil { + log.Fatalln(err) + } + for _, intf := range intfs { + log.Println(intf) + } + for _, route := range routes { + log.Println(route) + } + log.Println("Upstream:", getBindInterfaceIndex()) + } + } +} diff --git a/go/tun_linux.go b/go/tun_linux.go new file mode 100644 index 0000000..c380957 --- /dev/null +++ b/go/tun_linux.go @@ -0,0 +1,62 @@ +package main + +import ( + "errors" + "libcore" + "nekoray_core/gen" + "sync" + "syscall" + + gvisorTun "gvisor.dev/gvisor/pkg/tcpip/link/tun" +) + +var tun2ray *libcore.Tun2ray +var tun_fd int +var tun_lock sync.Mutex + +func TunStart(config *gen.SetTunReq) (err error) { + tun_lock.Lock() + defer tun_lock.Unlock() + + if tun2ray != nil { + return errors.New("tun aleary started") + } + + tun_fd, err = gvisorTun.Open(config.Name) + if err != nil { + return + } + + tun2ray, err = libcore.NewTun2ray(&libcore.TunConfig{ + FileDescriptor: int32(tun_fd), + MTU: config.Mtu, + V2Ray: instance, // use current if started + Implementation: config.Implementation, + Sniffing: true, + FakeDNS: config.Fakedns, + }) + return +} + +func TunStop() { + tun_lock.Lock() + defer tun_lock.Unlock() + + if tun2ray != nil { + tun2ray.Close() + tun2ray = nil + if tun_fd > 0 { + syscall.Close(tun_fd) + } + tun_fd = 0 + } +} + +func TunSetV2ray(i *libcore.V2RayInstance) { + tun_lock.Lock() + defer tun_lock.Unlock() + + if tun2ray != nil { + tun2ray.SetV2ray(i) + } +} diff --git a/go/tun_stub.go b/go/tun_stub.go new file mode 100644 index 0000000..464cf45 --- /dev/null +++ b/go/tun_stub.go @@ -0,0 +1,19 @@ +//go:build !linux + +package main + +import ( + "errors" + "libcore" + "nekoray_core/gen" +) + +func TunStart(config *gen.SetTunReq) error { + return errors.New("not for this platform") +} + +func TunStop() { +} + +func TunSetV2ray(i *libcore.V2RayInstance) { +} diff --git a/libs/.gitignore b/libs/.gitignore new file mode 100644 index 0000000..f428a32 --- /dev/null +++ b/libs/.gitignore @@ -0,0 +1,2 @@ +deps +downloaded \ No newline at end of file diff --git a/libs/README b/libs/README new file mode 100644 index 0000000..ec17a4a --- /dev/null +++ b/libs/README @@ -0,0 +1,6 @@ +依赖 +libs/deps/* +libs/deps/windows-x64 (vcpkg) +libs/deps/built (prefix) + +全部在项目根目录运行 diff --git a/libs/build_deps_all.sh b/libs/build_deps_all.sh new file mode 100755 index 0000000..d2711b8 --- /dev/null +++ b/libs/build_deps_all.sh @@ -0,0 +1,54 @@ +#!/bin/bash +set -e + +cd libs + +# libs/deps/... +mkdir -p deps; cd deps +INSTLL_PREFIX=$PWD/built +mkdir -p $INSTLL_PREFIX + +#### yaml-cpp #### +curl -L -o dl.zip https://github.com/jbeder/yaml-cpp/archive/refs/tags/yaml-cpp-0.7.0.zip +unzip dl.zip + +cd yaml-* +mkdir -p build; cd build + +cmake .. -GNinja -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$INSTLL_PREFIX +ninja && ninja install + +cd ../.. + +#### ZXing #### +curl -L -o dl.zip https://github.com/nu-book/zxing-cpp/archive/refs/tags/v1.3.0.zip +unzip dl.zip + +cd zxing-* +mkdir -p build; cd build + +cmake .. -GNinja -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DBUILD_EXAMPLES=OFF -DBUILD_BLACKBOX_TESTS=OFF -DCMAKE_INSTALL_PREFIX=$INSTLL_PREFIX +ninja && ninja install + +cd ../.. + +#### protobuf #### +git clone --recurse-submodules -b v21.4 --depth 1 --shallow-submodules https://github.com/protocolbuffers/protobuf + +#备注:交叉编译要在 host 也安装 protobuf 并且版本一致,编译安装,同参数,安装到 /usr/local + +mkdir -p protobuf/build +cd protobuf/build + +cmake .. -GNinja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -Dprotobuf_MSVC_STATIC_RUNTIME=OFF \ + -Dprotobuf_BUILD_TESTS=OFF \ + -DCMAKE_INSTALL_PREFIX=$INSTLL_PREFIX +ninja && ninja install + +cd ../.. + +#### clean #### +rm -rf dl.zip yaml-* zxing-* protobuf diff --git a/libs/deploy_common.sh b/libs/deploy_common.sh new file mode 100644 index 0000000..34d8825 --- /dev/null +++ b/libs/deploy_common.sh @@ -0,0 +1,46 @@ +SRC_ROOT="$PWD" +DEST="$PWD/deployment/nekoray" +BUILD="$SRC_ROOT/build" + +mkdir -p $DEST +mkdir -p $BUILD + +export CGO_ENABLED=0 + +#### Go: updater #### +pushd updater +go build -o $DEST -trimpath -ldflags "-w -s" +popd + +#### libcore #### +COMMIT_M=$(cat matsuri_commit.txt) +COMMIT_V=$(cat core_commit.txt) +version_standalone="nekoray-"$(cat nekoray_version.txt) + +pushd .. + +git clone --no-checkout https://github.com/MatsuriDayo/Matsuri.git +git clone --no-checkout https://github.com/MatsuriDayo/v2ray-core.git + +pushd Matsuri +git checkout $COMMIT_M +popd + +pushd v2ray-core +git checkout $COMMIT_V +version_v2ray=$(git log --pretty=format:'%h' -n 1) +popd + +popd + +#### Go: nekoray_core #### +pushd go +go build -o $DEST -trimpath -ldflags "-w -s -X main.version_v2ray=$version_v2ray -X main.version_standalone=$version_standalone" +popd + +#### Download: geoip #### +curl -Lso $DEST/geoip.dat "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/download/202206042210/geoip.dat" +curl -Lso $DEST/geosite.dat "https://github.com/v2fly/domain-list-community/releases/download/20220604062951/dlc.dat" + +#### copy assets #### +cp assets/* $DEST diff --git a/libs/deploy_linux64.sh b/libs/deploy_linux64.sh new file mode 100755 index 0000000..1859645 --- /dev/null +++ b/libs/deploy_linux64.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -e + +source libs/deploy_common.sh + +#### updater to launcher #### +mv $DEST/updater $DEST/launcher + +#### copy binary #### +cp $BUILD/nekoray $DEST + +#### Download: prebuilt runtime #### +curl -Lso usr.zip https://github.com/MatsuriDayo/nekoray_qt_runtime/releases/download/20220503/20220705-5.15.2-linux64.zip +unzip usr.zip +mv usr $DEST + +#### copy runtime #### +LIB=$SRC_ROOT/libs/deps/built/lib +#cp $LIB/libZXing.so.1 $DEST/usr/lib + +#### pack tar #### +chmod +x $DEST/nekoray $DEST/nekoray_core $DEST/launcher +tar cvzf $SRC_ROOT/deployment/$version_standalone-linux64.tar.gz -C $SRC_ROOT/deployment nekoray +rm -rf $DEST $BUILD diff --git a/libs/deploy_windows64.sh b/libs/deploy_windows64.sh new file mode 100755 index 0000000..898223a --- /dev/null +++ b/libs/deploy_windows64.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + +source libs/deploy_common.sh + +#### Go: sing-box #### +pushd $BUILD +curl -Lso sing-box.zip https://github.com/SagerNet/sing-box/archive/64dbac813837bbadfaeec1a6e0d064875a123e5e.zip +unzip sing-box.zip +pushd sing-box-*/cmd/sing-box +go build -o $DEST -trimpath -ldflags "-w -s" +popd +popd + +#### copy exe #### +cp $BUILD/nekoray.exe $DEST + +#### deploy qt & DLL runtime #### +pushd $DEST +windeployqt nekoray.exe --no-compiler-runtime --no-system-d3d-compiler --no-opengl-sw --verbose 2 +curl -LSsO https://github.com/MatsuriDayo/nekoray_qt_runtime/releases/download/20220503/libcrypto-1_1-x64.dll +curl -LSsO https://github.com/MatsuriDayo/nekoray_qt_runtime/releases/download/20220503/libssl-1_1-x64.dll +rm -rf translations +popd + +#### pack zip #### +7z a $SRC_ROOT/deployment/$version_standalone-windows64.zip $DEST +cp $BUILD/*.pdb $SRC_ROOT/deployment/ +rm -rf $DEST $BUILD diff --git a/libs/dl.sh b/libs/dl.sh new file mode 100755 index 0000000..a5c9ab1 --- /dev/null +++ b/libs/dl.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +cd libs + +mkdir -p deps; cd ./deps +mkdir -p downloaded; cd ./downloaded; + +NAME=$1 +echo "Downloading: $NAME" +curl -sL $2 -o $NAME; + +cd .. + +for f in $(ls ./downloaded) +do + 7z x -y ./downloaded/$f +done + +# libs/deps/windows-x64/installed/ + +rm -rf downloaded diff --git a/main/Const.hpp b/main/Const.hpp new file mode 100644 index 0000000..d1863c5 --- /dev/null +++ b/main/Const.hpp @@ -0,0 +1,28 @@ +#pragma once + +namespace NekoRay { + namespace DomainMatcher { + enum DomainMatcher { + DEFAULT, + MPH, + }; + } + + namespace SniffingMode { + enum SniffingMode { + DISABLE, + FOR_ROUTING, + FOR_DESTINATION, + }; + } + + namespace SystemProxyMode { + enum SystemProxyMode { + DISABLE, + SYSTEM_PROXY, + VPN, + }; + } + +} + diff --git a/main/GuiUtils.hpp b/main/GuiUtils.hpp new file mode 100644 index 0000000..d4bc784 --- /dev/null +++ b/main/GuiUtils.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include + +// Dialogs + +inline QWidget *mainwindow; + +#define Dialog_DialogBasicSettings "DialogBasicSettings" +#define Dialog_DialogEditProfile "DialogEditProfile" +#define Dialog_DialogManageGroups "DialogManageGroups" +#define Dialog_DialogManageRoutes "DialogManageRoutes" + +// Utils + +inline QList +CreateActions(QWidget *parent, const QList &texts, const std::function &slot) { + QList acts; + + for (const auto &text: texts) { + acts.push_back(new QAction(text, parent)); //按顺序来 + } + + for (int i = 0; i < acts.size(); i++) { + if (acts[i]->text() == "[Separator]") { + acts[i]->setSeparator(true); + acts[i]->setText(""); + acts[i]->setDisabled(true); + acts[i]->setData(-1); + } else { + acts[i]->setData(i); + QObject::connect(acts[i], &QAction::triggered, parent, [=] { + slot(acts[i]); + }); + } + } + + return acts; +} + +inline QMenu *CreateMenu(QWidget *parent, const QList &texts, const std::function &slot) { + auto menu = new QMenu(parent); + menu->addActions(CreateActions(parent, texts, slot)); + return menu; +} + +#define QRegExpValidator_Number new QRegularExpressionValidator(QRegularExpression("^[0-9]+$") + +// NekoRay Save&Load + +#define P_LOAD_STRING(a) ui->a->setText(bean->a); +#define P_SAVE_STRING(a) bean->a = ui->a->text(); +#define P_SAVE_STRING_QTEXTEDIT(a) bean->a = ui->a->toPlainText(); +#define D_LOAD_STRING(a) ui->a->setText(NekoRay::dataStore->a); +#define D_SAVE_STRING(a) NekoRay::dataStore->a = ui->a->text(); +#define P_C_LOAD_STRING(a) CACHE.a = bean->a; +#define P_C_SAVE_STRING(a) bean->a = CACHE.a; +#define D_C_LOAD_STRING(a) CACHE.a = NekoRay::dataStore->a; +#define D_C_SAVE_STRING(a) NekoRay::dataStore->a = CACHE.a; +#define P_LOAD_INT(a) ui->a->setText(Int2String(bean->a)); ui->a->setValidator(QRegExpValidator_Number, this)); +#define P_SAVE_INT(a) bean->a = ui->a->text().toInt(); +#define D_LOAD_INT(a) ui->a->setText(Int2String(NekoRay::dataStore->a)); ui->a->setValidator(QRegExpValidator_Number, this)); +#define D_SAVE_INT(a) NekoRay::dataStore->a = ui->a->text().toInt(); +#define P_LOAD_COMBO(a) ui->a->setCurrentText(bean->a); +#define P_SAVE_COMBO(a) bean->a = ui->a->currentText(); + +#define D_LOAD_INT_ENABLE(i, e) \ +if (NekoRay::dataStore->i > 0) { \ +ui->e->setChecked(true); \ +ui->i->setText(Int2String(NekoRay::dataStore->i)); \ +} else { \ +ui->e->setChecked(false); \ +ui->i->setText(Int2String(-NekoRay::dataStore->i)); \ +} \ +ui->i->setValidator(QRegExpValidator_Number, this)); +#define D_SAVE_INT_ENABLE(i, e) \ +if (ui->e->isChecked()) { \ +NekoRay::dataStore->i = ui->i->text().toInt(); \ +} else { \ +NekoRay::dataStore->i = -ui->i->text().toInt(); \ +} + +#define C_EDIT_JSON_ALLOW_EMPTY(a) auto editor = new JsonEditor(QString2QJsonObject(CACHE.a), this); \ +auto result = editor->OpenEditor(); \ +CACHE.a = QJsonObject2QString(result, true); \ +if (result.isEmpty()) CACHE.a = ""; \ +editor->deleteLater(); diff --git a/main/NekoRay.cpp b/main/NekoRay.cpp new file mode 100644 index 0000000..d5ac8d0 --- /dev/null +++ b/main/NekoRay.cpp @@ -0,0 +1,320 @@ +#include "NekoRay.hpp" + +#include +#include + +namespace NekoRay { + + DataStore *dataStore = new DataStore(); + + // datastore + + DataStore::DataStore() : JsonStore("groups/nekoray.json") { + _add(new configItem("extraCore", dynamic_cast(extraCore), itemType::jsonStore)); + + _add(new configItem("core_path", &core_path, itemType::string)); + _add(new configItem("user_agent", &user_agent, itemType::string)); + _add(new configItem("test_url", &test_url, itemType::string)); + _add(new configItem("current_group", ¤t_group, itemType::integer)); + _add(new configItem("inbound_address", &inbound_address, itemType::string)); + _add(new configItem("inbound_socks_port", &inbound_socks_port, itemType::integer)); + _add(new configItem("inbound_http_port", &inbound_http_port, itemType::integer)); + _add(new configItem("log_level", &log_level, itemType::string)); + _add(new configItem("remote_dns", &remote_dns, itemType::string)); + _add(new configItem("direct_dns", &direct_dns, itemType::string)); + _add(new configItem("domain_matcher", &domain_matcher, itemType::integer)); + _add(new configItem("domain_strategy", &domain_strategy, itemType::string)); + _add(new configItem("outbound_domain_strategy", &outbound_domain_strategy, itemType::string)); + _add(new configItem("sniffing_mode", &sniffing_mode, itemType::integer)); + _add(new configItem("mux_cool", &mux_cool, itemType::integer)); + _add(new configItem("traffic_loop_interval", &traffic_loop_interval, itemType::integer)); + _add(new configItem("dns_routing", &dns_routing, itemType::boolean)); + _add(new configItem("test_concurrent", &test_concurrent, itemType::integer)); + _add(new configItem("theme", &theme, itemType::string)); + _add(new configItem("custom_inbound", &custom_inbound, itemType::string)); + _add(new configItem("custom_route", &custom_route_global, itemType::string)); + _add(new configItem("v2ray_asset_dir", &v2ray_asset_dir, itemType::string)); + _add(new configItem("sub_use_proxy", &sub_use_proxy, itemType::boolean)); + _add(new configItem("enhance_domain", &enhance_resolve_server_domain, itemType::boolean)); + _add(new configItem("remember_id", &remember_id, itemType::integer)); + _add(new configItem("remember_enable", &remember_enable, itemType::boolean)); + _add(new configItem("start_minimal", &start_minimal, itemType::boolean)); + _add(new configItem("language", &language, itemType::integer)); + _add(new configItem("spmode", &system_proxy_mode, itemType::integer)); + _add(new configItem("insecure_hint", &insecure_hint, itemType::boolean)); + _add(new configItem("skip_cert", &skip_cert, itemType::boolean)); + _add(new configItem("hk_mw", &hotkey_mainwindow, itemType::string)); + _add(new configItem("hk_group", &hotkey_group, itemType::string)); + _add(new configItem("hk_route", &hotkey_route, itemType::string)); + _add(new configItem("fakedns", &fake_dns, itemType::boolean)); + _add(new configItem("active_routing", &active_routing, itemType::string)); + _add(new configItem("mw_size", &mw_size, itemType::string)); + _add(new configItem("conn_stat", &connection_statistics, itemType::boolean)); + } + + void DataStore::UpdateStartedId(int id) { + started_id = id; + if (remember_enable) { + remember_id = id; + Save(); + } else if (remember_id >= 0) { + remember_id = -1919; + Save(); + } + } + + // preset routing + Routing::Routing(int preset) : JsonStore() { + if (preset == 1) { + direct_ip = "geoip:cn\n" + "geoip:private"; + direct_domain = "geosite:cn"; + proxy_ip = ""; + proxy_domain = ""; + block_ip = ""; + block_domain = "geosite:category-ads-all\n" + "domain:appcenter.ms\n" + "domain:app-measurement.com\n" + "domain:firebase.io\n" + "domain:crashlytics.com\n" + "domain:google-analytics.com"; + } + _add(new configItem("direct_ip", &this->direct_ip, itemType::string)); + _add(new configItem("direct_domain", &this->direct_domain, itemType::string)); + _add(new configItem("proxy_ip", &this->proxy_ip, itemType::string)); + _add(new configItem("proxy_domain", &this->proxy_domain, itemType::string)); + _add(new configItem("block_ip", &this->block_ip, itemType::string)); + _add(new configItem("block_domain", &this->block_domain, itemType::string)); + _add(new configItem("custom", &this->custom, itemType::string)); + } + + QString Routing::toString() const { + return QString("[Proxy] %1\n[Proxy] %2\n[Direct] %3\n[Direct] %4\n[Block] %5\n[Block] %6") + .arg(SplitLines(proxy_domain).join(",")) + .arg(SplitLines(proxy_ip).join(",")) + .arg(SplitLines(direct_domain).join(",")) + .arg(SplitLines(direct_ip).join(",")) + .arg(SplitLines(block_domain).join(",")) + .arg(SplitLines(block_ip).join(",")); + } + + QStringList Routing::List() { + QStringList l; + QDir d; + if (d.exists("routes")) { + QDir dr("routes"); + return dr.entryList(QDir::Files); + } + return l; + } + + void Routing::SetToActive(const QString &name) { + dataStore->routing->fn = "routes/" + name; + dataStore->routing->Load(); + dataStore->active_routing = name; + dataStore->Save(); + } + + // NO default extra core + + ExtraCore::ExtraCore() : JsonStore() { + _add(new configItem("core_map", &this->core_map, itemType::string)); + } + + QString ExtraCore::Get(const QString &id) const { + auto obj = QString2QJsonObject(core_map); + for (const auto &c: obj.keys()) { + if (c == id) return obj[id].toString(); + } + return ""; + } + + void ExtraCore::Set(const QString &id, const QString &path) { + auto obj = QString2QJsonObject(core_map); + obj[id] = path; + core_map = QJsonObject2QString(obj, true); + } + + void ExtraCore::Delete(const QString &id) { + auto obj = QString2QJsonObject(core_map); + obj.remove(id); + core_map = QJsonObject2QString(obj, true); + } + + // 添加关联 + void JsonStore::_add(configItem *item) { + _map.insert(item->name, QSharedPointer(item)); + } + + QSharedPointer JsonStore::_get(const QString &name) { + // 直接 [] 会设置一个 nullptr ,所以先判断是否存在 + if (_map.contains(name)) { + return _map[name]; + } + return nullptr; + } + + QJsonObject JsonStore::ToJson() { + QJsonObject object; + for (const auto &_item: _map) { + auto item = _item.get(); + switch (item->type) { + case itemType::string: + object.insert(item->name, *(QString *) item->ptr); + break; + case itemType::integer: + object.insert(item->name, *(int *) item->ptr); + break; + case itemType::integer64: + object.insert(item->name, *(long long *) item->ptr); + break; + case itemType::boolean: + object.insert(item->name, *(bool *) item->ptr); + break; + case itemType::stringList: + object.insert(item->name, QList2QJsonArray(*(QList *) item->ptr)); + break; + case itemType::integerList: + object.insert(item->name, QList2QJsonArray(*(QList *) item->ptr)); + break; + case itemType::jsonStore: + // _add 时应关联对应 JsonStore 的指针 + object.insert(item->name, ((JsonStore *) item->ptr)->ToJson()); + break; + } + } + return object; + } + + QByteArray JsonStore::ToJsonBytes() { + QJsonDocument document; + document.setObject(ToJson()); + return document.toJson(save_control_compact ? QJsonDocument::Compact : QJsonDocument::Indented); + } + + void JsonStore::FromJson(QJsonObject object) { + for (const auto &key: object.keys()) { + if (_map.count(key) == 0) { + if (debug_verbose) { + qDebug() << QString("unknown key\n%1\n%2").arg(key, QJsonObject2QString(object, false)); + } + continue; + } + + auto value = object[key]; + auto item = _map[key].get(); + + if (item == nullptr) + continue; // 故意忽略 + + // 根据类型修改ptr的内容 + switch (item->type) { + case itemType::string: + if (value.type() != QJsonValue::String) { + MessageBoxWarning("错误", "Not a string\n" + key); + continue; + } + *(QString *) item->ptr = value.toString(); + break; + case itemType::integer: + if (value.type() != QJsonValue::Double) { + MessageBoxWarning("错误", "Not a int\n" + key); + continue; + } + *(int *) item->ptr = value.toInt(); + break; + case itemType::integer64: + if (value.type() != QJsonValue::Double) { + MessageBoxWarning("错误", "Not a int64\n" + key); + continue; + } + *(long long *) item->ptr = value.toDouble(); + break; + case itemType::boolean: + if (value.type() != QJsonValue::Bool) { + MessageBoxWarning("错误", "Not a bool\n" + key); + continue; + } + *(bool *) item->ptr = value.toBool(); + break; + case itemType::stringList: + if (value.type() != QJsonValue::Array) { + MessageBoxWarning("错误", "Not a Array\n" + key); + continue; + } + *(QList *) item->ptr = QJsonArray2QListString(value.toArray()); + break; + case itemType::integerList: + if (value.type() != QJsonValue::Array) { + MessageBoxWarning("错误", "Not a Array\n" + key); + continue; + } + *(QList *) item->ptr = QJsonArray2QListInt(value.toArray()); + break; + case itemType::jsonStore: + if (value.type() != QJsonValue::Object) { + MessageBoxWarning("错误", "Not a json object\n" + key); + continue; + } + if (load_control_no_jsonStore) + continue; + ((JsonStore *) item->ptr)->FromJson(value.toObject()); + break; + } + } + + for (const auto &hook: _hooks_after_load) { + hook(); + } + } + + void JsonStore::FromJsonBytes(const QByteArray &data) { + QJsonParseError error{}; + auto document = QJsonDocument::fromJson(data, &error); + + if (error.error != error.NoError) { + if (debug_verbose) qDebug() << "QJsonParseError" << error.errorString(); + return; + } + + FromJson(document.object()); + } + + bool JsonStore::Save() { + for (const auto &hook: _hooks_before_save) { + hook(); + } + + auto save_content = ToJsonBytes(); + auto changed = last_save_content != save_content; + last_save_content = save_content; + + QFile file; + file.setFileName(fn); + file.open(QIODevice::ReadWrite | QIODevice::Truncate); + file.write(save_content); + file.close(); + + return changed; + } + + bool JsonStore::Load() { + QFile file; + file.setFileName(fn); + + if (!file.exists() && !load_control_force) + return false; + + bool ok = file.open(QIODevice::ReadOnly); + if (!ok) { + MessageBoxWarning("error", "can not open config " + fn + "\n" + file.errorString()); + } else { + last_save_content = file.readAll(); + FromJsonBytes(last_save_content); + } + + file.close(); + return ok; + } + +} diff --git a/main/NekoRay.hpp b/main/NekoRay.hpp new file mode 100644 index 0000000..c2bf117 --- /dev/null +++ b/main/NekoRay.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include "Const.hpp" +#include "NekoRay_Utils.hpp" +#include "NekoRay_ConfigItem.hpp" +#include "NekoRay_DataStore.hpp" diff --git a/main/NekoRay_ConfigItem.hpp b/main/NekoRay_ConfigItem.hpp new file mode 100644 index 0000000..377ffe4 --- /dev/null +++ b/main/NekoRay_ConfigItem.hpp @@ -0,0 +1,63 @@ +// DO NOT INCLUDE THIS + +namespace NekoRay { + // config 工具 + enum itemType { + string, + integer, + integer64, + boolean, + stringList, + integerList, + jsonStore, + }; + + class configItem { + public: + QString name; + void *ptr; + itemType type; + + configItem(QString n, void *p, itemType t) { + name = std::move(n); + ptr = p; + type = t; + } + }; + + // 可格式化对象 + class JsonStore { + public: + QMap> _map; + QList> _hooks_after_load; + QList> _hooks_before_save; + QString fn; + bool debug_verbose = false; + bool load_control_force = false; + bool load_control_no_jsonStore = false; //不加载 json object + bool save_control_compact = false; + QByteArray last_save_content; + + JsonStore() = default; + + explicit JsonStore(QString fileName) { + fn = std::move(fileName); + } + + void _add(configItem *item); + + QSharedPointer _get(const QString &name); + + QJsonObject ToJson(); + + QByteArray ToJsonBytes(); + + void FromJson(QJsonObject object); + + void FromJsonBytes(const QByteArray &data); + + bool Save(); + + bool Load(); + }; +} diff --git a/main/NekoRay_DataStore.hpp b/main/NekoRay_DataStore.hpp new file mode 100644 index 0000000..d81addf --- /dev/null +++ b/main/NekoRay_DataStore.hpp @@ -0,0 +1,122 @@ +// DO NOT INCLUDE THIS + +namespace NekoRay { + + class Routing : public JsonStore { + public: + QString direct_ip; + QString direct_domain; + QString proxy_ip; + QString proxy_domain; + QString block_ip; + QString block_domain; + QString custom = "{\"rules\": []}"; + + explicit Routing(int preset = 0); + + QString toString() const; + + static QStringList List(); + + static void SetToActive(const QString &name); + }; + + class ExtraCore : public JsonStore { + public: + QString core_map; + + explicit ExtraCore(); + + [[nodiscard]] QString Get(const QString &id) const; + + void Set(const QString &id, const QString &path); + + void Delete(const QString &id); + }; + + class DataStore : public JsonStore { + public: + // Running + + QString core_token; + int core_port = 19810; + int started_id = -1919; + + // Saved + + // Misc + QString core_path = "../nekoray_core"; + QString log_level = "warning"; + QString user_agent = "ClashForAndroid/2.5.9.premium"; + bool sub_use_proxy = false; + QString test_url = "http://cp.cloudflare.com/"; + int test_concurrent = 5; + int traffic_loop_interval = 500; + bool connection_statistics = false; + int current_group = 0; //group id + int mux_cool = -8; + QString theme = "0"; + QString v2ray_asset_dir = ""; + int language = 0; + QString mw_size = ""; + + // Security + bool insecure_hint = true; + bool skip_cert = false; + + // Remember + int system_proxy_mode = NekoRay::SystemProxyMode::DISABLE; + int remember_id = -1919; + bool remember_enable = false; + bool start_minimal = false; + + // Socks & HTTP Inbound + QString inbound_address = "127.0.0.1"; + int inbound_socks_port = 2080; + int inbound_http_port = -2081; + QString custom_inbound = "{\"inbounds\": []}"; + + // DNS + QString remote_dns = "https://8.8.8.8/dns-query"; + QString direct_dns = "https+local://223.5.5.5/dns-query"; + bool dns_routing = true; + bool enhance_resolve_server_domain = false; + + // Routing + bool fake_dns = false; + QString domain_strategy = "AsIs"; + QString outbound_domain_strategy = "AsIs"; + int sniffing_mode = SniffingMode::FOR_ROUTING; + int domain_matcher = DomainMatcher::MPH; + QString custom_route_global = "{\"rules\": []}"; + QString active_routing = "Default"; + + // Hotkey + QString hotkey_mainwindow = ""; + QString hotkey_group = ""; + QString hotkey_route = ""; + + // Other Core + ExtraCore *extraCore = new ExtraCore; + + // Running Cache + + Routing *routing = new Routing; + int imported_count = 0; + bool refreshing_group_list = false; + + // Running Flags + + bool flag_use_appdata = false; + bool flag_many = false; + + // + + DataStore(); + + void UpdateStartedId(int id); + }; + + extern DataStore *dataStore; + +} diff --git a/main/NekoRay_Utils.cpp b/main/NekoRay_Utils.cpp new file mode 100644 index 0000000..841ea08 --- /dev/null +++ b/main/NekoRay_Utils.cpp @@ -0,0 +1,103 @@ +#include "NekoRay_Utils.hpp" + +#include "3rdparty/QThreadCreateThread.hpp" +#include "main/GuiUtils.hpp" + +#include + +#include +#include +#include +#include +#include + +QString GetQueryValue(const QUrlQuery &q, const QString &key, const QString &def) { + auto a = q.queryItemValue(key); + if (a.isEmpty()) { + return def; + } + return a; +} + +QString GetRandomString(int randomStringLength) { + std::random_device rd; + std::mt19937 mt(rd()); + + const QString possibleCharacters("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); + + std::uniform_int_distribution dist(0, possibleCharacters.count() - 1); + + QString randomString; + for (int i = 0; i < randomStringLength; ++i) { + QChar nextChar = possibleCharacters.at(dist(mt)); + randomString.append(nextChar); + } + return randomString; +} + +QByteArray ReadFile(const QString &path) { + QFile file(path); + file.open(QFile::ReadOnly); + return file.readAll(); +} + +QString ReadFileText(const QString &path) { + QFile file(path); + file.open(QFile::ReadOnly | QFile::Text); + QTextStream stream(&file); + return stream.readAll(); +} + +int MkPort() { + QTcpServer s; + s.listen(); + auto port = s.serverPort(); + s.close(); + return port; +} + +bool IsIpAddress(const QString &str) { + auto address = QHostAddress(str); + if (address.protocol() == QAbstractSocket::IPv4Protocol || address.protocol() == QAbstractSocket::IPv6Protocol) + return true; + return false; +} + +bool IsIpAddressV4(const QString &str) { + auto address = QHostAddress(str); + if (address.protocol() == QAbstractSocket::IPv4Protocol) + return true; + return false; +} + +bool IsIpAddressV6(const QString &str) { + auto address = QHostAddress(str); + if (address.protocol() == QAbstractSocket::IPv6Protocol) + return true; + return false; +} + +int MessageBoxWarning(const QString &title, const QString &text) { + return QMessageBox::warning(nullptr, title, text); +} + +int MessageBoxInfo(const QString &title, const QString &text) { + return QMessageBox::information(nullptr, title, text); +} + +void runOnUiThread(const std::function &callback, QObject *parent) { + // any thread + auto *timer = new QTimer(); + timer->moveToThread(parent == nullptr ? mainwindow->thread() : parent->thread()); + timer->setSingleShot(true); + QObject::connect(timer, &QTimer::timeout, [=]() { + // main thread + callback(); + timer->deleteLater(); + }); + QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection, Q_ARG(int, 0)); +} + +void runOnNewThread(const std::function &callback) { + createQThread(callback)->start(); +} diff --git a/main/NekoRay_Utils.hpp b/main/NekoRay_Utils.hpp new file mode 100644 index 0000000..72c139c --- /dev/null +++ b/main/NekoRay_Utils.hpp @@ -0,0 +1,180 @@ +// DO NOT INCLUDE THIS + +#include +#include +#include +#include + +// Dialogs + +inline std::function showLog; +inline std::function showLog_ext; +inline std::function showLog_ext_vt100; +inline std::function dialog_message; + +// Utils + +#define QJSONARRAY_ADD(arr, add) for(const auto &a: (add)) { (arr) += a; } + +inline QString SubStrBefore(QString str, const QString &sub) { + if (!str.contains(sub)) return str; + return str.left(str.indexOf(sub)); +} + +inline QString SubStrAfter(QString str, const QString &sub) { + if (!str.contains(sub)) return str; + return str.right(str.length() - str.indexOf(sub) - sub.length()); +} + +inline QString +DecodeB64IfValid(const QString &input, QByteArray::Base64Option options = QByteArray::Base64Option::Base64Encoding) { + auto result = QByteArray::fromBase64Encoding(input.toUtf8(), + options | QByteArray::Base64Option::AbortOnBase64DecodingErrors); + if (result) { + return result.decoded; + } + return ""; +} + +#define GetQuery(url) QUrlQuery((url).query(QUrl::ComponentFormattingOption::FullyDecoded)); + +QString GetQueryValue(const QUrlQuery &q, const QString &key, const QString &def = ""); + +inline QString Int2String(int i) { + return QVariant(i).toString(); +} + +inline QString Int2String(qint64 i) { + return QVariant(i).toString(); +} + +QString GetRandomString(int randomStringLength); + +// QString >> QJson +inline QJsonObject QString2QJsonObject(const QString &jsonString) { + QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonString.toUtf8()); + QJsonObject jsonObject = jsonDocument.object(); + return jsonObject; +} + +// QJson >> QString +inline QString QJsonObject2QString(const QJsonObject &jsonObject, bool compact) { + return QString(QJsonDocument(jsonObject).toJson(compact ? QJsonDocument::Compact : QJsonDocument::Indented)); +} + +template +inline QJsonArray QList2QJsonArray(const QList &list) { + QVariantList list2; + for (auto &item: list) + list2.append(item); + return QJsonArray::fromVariantList(list2); +} + +inline QList QJsonArray2QListInt(const QJsonArray &arr) { + QList list2; + for (auto item: arr) + list2.append(item.toInt()); + return list2; +} + +inline QList QJsonArray2QListString(const QJsonArray &arr) { + QList list2; + for (auto item: arr) + list2.append(item.toString()); + return list2; +} + +inline QString UrlSafe_encode(const QString &s) { + return s.toUtf8().toPercentEncoding().replace(" ", "%20"); +} + +inline bool InRange(unsigned x, unsigned low, unsigned high) { + return (low <= x && x <= high); +} + +inline QStringList SplitLines(const QString &_string) { + return _string.split(QRegularExpression("[\r\n]"), Qt::SplitBehaviorFlags::SkipEmptyParts); +} + +QByteArray ReadFile(const QString &path); + +QString ReadFileText(const QString &path); + +// Net + +int MkPort(); + +// Validators + +bool IsIpAddress(const QString &str); + +bool IsIpAddressV4(const QString &str); + +bool IsIpAddressV6(const QString &str); + +// [2001:4860:4860::8888] -> 2001:4860:4860::8888 +inline QString UnwrapIPV6Host(QString &str) { + return str.replace("[", "").replace("]", ""); +} + +// [2001:4860:4860::8888] or 2001:4860:4860::8888 -> [2001:4860:4860::8888] +inline QString WrapIPV6Host(QString &str) { + if (!IsIpAddressV6(str)) return str; + return "[" + UnwrapIPV6Host(str) + "]"; +} + +inline QString DisplayAddress(QString serverAddress, int serverPort) { + return WrapIPV6Host(serverAddress) + ":" + Int2String(serverPort); +}; + +// Format + +inline QString DisplayTime(long long time, QLocale::FormatType formatType = QLocale::LongFormat) { + QDateTime t; + t.setSecsSinceEpoch(time); + return QLocale().toString(t, formatType); +} + +inline QString ReadableSize(const qint64 &size) { + double sizeAsDouble = size; + static QStringList measures; + if (measures.isEmpty()) + measures << "B" + << "KiB" + << "MiB" + << "GiB" + << "TiB" + << "PiB" + << "EiB" + << "ZiB" + << "YiB"; + QStringListIterator it(measures); + QString measure(it.next()); + while (sizeAsDouble >= 1024.0 && it.hasNext()) { + measure = it.next(); + sizeAsDouble /= 1024.0; + } + return QString::fromLatin1("%1 %2").arg(sizeAsDouble, 0, 'f', 2).arg(measure); +} + +// UI + +int MessageBoxWarning(const QString &title, const QString &text); + +int MessageBoxInfo(const QString &title, const QString &text); + +void runOnUiThread(const std::function &callback, QObject *parent = nullptr); + +void runOnNewThread(const std::function &callback); + +template +inline void connectOnce(EMITTER *emitter, SIGNAL signal, RECEIVER *receiver, ReceiverFunc f, + Qt::ConnectionType connectionType = Qt::AutoConnection) { + auto connection = std::make_shared(); + auto onTriggered = [connection, f](auto... arguments) { + std::invoke(f, arguments...); + QObject::disconnect(*connection); + }; + + *connection = QObject::connect(emitter, signal, receiver, onTriggered, connectionType); +} diff --git a/main/main.cpp b/main/main.cpp new file mode 100644 index 0000000..deec573 --- /dev/null +++ b/main/main.cpp @@ -0,0 +1,125 @@ +#include "ui/mainwindow.h" + +#include +#include +#include +#include +#include + +#include "3rdparty/RunGuard.hpp" +#include "main/NekoRay.hpp" + +#ifdef Q_OS_WIN +#include "sys/windows/MiniDump.h" +#endif + +int main(int argc, char *argv[]) { + // Core dump +#ifdef Q_OS_WIN + Windows_SetCrashHandler(); +#endif + + QApplication a(argc, argv); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QApplication::setAttribute(Qt::AA_DisableWindowContextHelpButton); +#endif + QApplication::setAttribute(Qt::AA_DontUseNativeDialogs); + + // Clean + QDir::setCurrent(QApplication::applicationDirPath()); + QFile::remove("updater.old"); +#ifndef Q_OS_WIN + if (!QFile::exists("updater")) { + QFile::link("launcher", "updater"); + } +#endif + + // Flags + auto args = QApplication::arguments(); + if (args.contains("-many")) NekoRay::dataStore->flag_many = true; + if (args.contains("-appdata")) NekoRay::dataStore->flag_use_appdata = true; + + // dirs & clean + auto wd = QDir(QApplication::applicationDirPath()); + if (NekoRay::dataStore->flag_use_appdata) { + QApplication::setApplicationName("nekoray"); + wd.setPath(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); + } + if (!wd.exists()) wd.mkdir(wd.absolutePath()); + if (!wd.exists("config")) wd.mkdir("config"); + QDir::setCurrent(wd.absoluteFilePath("config")); + QDir("temp").removeRecursively(); + + // RunGuard + RunGuard guard("nekoray" + wd.absolutePath()); + if (!NekoRay::dataStore->flag_many) { + if (!guard.tryToRun()) { + QMessageBox::warning(nullptr, "NekoRay", QObject::tr("Another program is running.")); + return 0; + } + } + + // icons + QIcon::setFallbackSearchPaths(QStringList{ + ":/nekoray", + ":/icon", + }); + + // icon for no theme + if (QIcon::themeName().isEmpty()) { + QIcon::setThemeName("breeze"); + } + + // Dir + QDir dir; + bool dir_success = true; + if (!dir.exists("profiles")) { + dir_success = dir_success && dir.mkdir("profiles"); + } + if (!dir.exists("groups")) { + dir_success = dir_success && dir.mkdir("groups"); + } + if (!dir.exists("routes")) { + dir_success = dir_success && dir.mkdir("routes"); + } + if (!dir_success) { + QMessageBox::warning(nullptr, "Error", "No permission to write " + dir.absolutePath()); + return 1; + } + + // Load dataStore + auto isLoaded = NekoRay::dataStore->Load(); + if (!isLoaded) { + NekoRay::dataStore->Save(); + } + + // load routing + NekoRay::dataStore->routing->fn = "routes/" + NekoRay::dataStore->active_routing; + isLoaded = NekoRay::dataStore->routing->Load(); + if (!isLoaded) { + NekoRay::dataStore->routing->Save(); + } + + // Translate + QString locale; + switch (NekoRay::dataStore->language) { + case 1: // English + break; + case 2: + locale = "zh_CN"; + break; + default: + locale = QLocale().name(); + } + QTranslator trans; + if (trans.load(":/translations/" + locale + ".qm")) { + QCoreApplication::installTranslator(&trans); + } + QTranslator trans_qt; + if (trans_qt.load(QApplication::applicationDirPath() + "/qtbase_" + locale + ".qm")) { + QCoreApplication::installTranslator(&trans_qt); + } + + MainWindow w; + return QApplication::exec(); +} diff --git a/matsuri_commit.txt b/matsuri_commit.txt new file mode 100644 index 0000000..5fa36e5 --- /dev/null +++ b/matsuri_commit.txt @@ -0,0 +1 @@ +3dd90ce8b7dcb7001f3de42f6bcc8edf0093353a diff --git a/nekoray_version.txt b/nekoray_version.txt new file mode 100644 index 0000000..37dccef --- /dev/null +++ b/nekoray_version.txt @@ -0,0 +1 @@ +1.0-2022-08-04 diff --git a/qv2ray/components/proxy/QvProxyConfigurator.cpp b/qv2ray/components/proxy/QvProxyConfigurator.cpp new file mode 100644 index 0000000..b41aa55 --- /dev/null +++ b/qv2ray/components/proxy/QvProxyConfigurator.cpp @@ -0,0 +1,484 @@ +#ifndef __MINGW32__ + +#include "QvProxyConfigurator.hpp" + +#ifdef Q_OS_WIN +// +#include +// +#include +#include +#include +#include +#endif + +#include +#include + +#include "qv2ray/wrapper.hpp" + +#define QV_MODULE_NAME "SystemProxy" + +#define QSTRN(num) QString::number(num) + +namespace Qv2ray::components::proxy { + + using ProcessArgument = QPair; +#ifdef Q_OS_MACOS + QStringList macOSgetNetworkServices() + { + QProcess p; + p.setProgram("/usr/sbin/networksetup"); + p.setArguments(QStringList{ "-listallnetworkservices" }); + p.start(); + p.waitForStarted(); + p.waitForFinished(); + LOG(p.errorString()); + auto str = p.readAllStandardOutput(); + auto lines = SplitLines(str); + QStringList result; + + // Start from 1 since first line is unneeded. + for (auto i = 1; i < lines.count(); i++) + { + // * means disabled. + if (!lines[i].contains("*")) + { + result << lines[i]; + } + } + + LOG("Found " + QSTRN(result.size()) + " network services: " + result.join(";")); + return result; + } +#endif +#ifdef Q_OS_WIN +#define NO_CONST(expr) const_cast(expr) + // static auto DEFAULT_CONNECTION_NAME = + // NO_CONST(L"DefaultConnectionSettings"); + /// + /// INTERNAL FUNCTION + bool __QueryProxyOptions() + { + INTERNET_PER_CONN_OPTION_LIST List; + INTERNET_PER_CONN_OPTION Option[5]; + // + unsigned long nSize = sizeof(INTERNET_PER_CONN_OPTION_LIST); + Option[0].dwOption = INTERNET_PER_CONN_AUTOCONFIG_URL; + Option[1].dwOption = INTERNET_PER_CONN_AUTODISCOVERY_FLAGS; + Option[2].dwOption = INTERNET_PER_CONN_FLAGS; + Option[3].dwOption = INTERNET_PER_CONN_PROXY_BYPASS; + Option[4].dwOption = INTERNET_PER_CONN_PROXY_SERVER; + // + List.dwSize = sizeof(INTERNET_PER_CONN_OPTION_LIST); + List.pszConnection = nullptr; // NO_CONST(DEFAULT_CONNECTION_NAME); + List.dwOptionCount = 5; + List.dwOptionError = 0; + List.pOptions = Option; + + if (!InternetQueryOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &List, &nSize)) + { + LOG("InternetQueryOption failed, GLE=" + QSTRN(GetLastError())); + } + + LOG("System default proxy info:"); + + if (Option[0].Value.pszValue != nullptr) + { + LOG(QString::fromWCharArray(Option[0].Value.pszValue)); + } + + if ((Option[2].Value.dwValue & PROXY_TYPE_AUTO_PROXY_URL) == PROXY_TYPE_AUTO_PROXY_URL) + { + LOG("PROXY_TYPE_AUTO_PROXY_URL"); + } + + if ((Option[2].Value.dwValue & PROXY_TYPE_AUTO_DETECT) == PROXY_TYPE_AUTO_DETECT) + { + LOG("PROXY_TYPE_AUTO_DETECT"); + } + + if ((Option[2].Value.dwValue & PROXY_TYPE_DIRECT) == PROXY_TYPE_DIRECT) + { + LOG("PROXY_TYPE_DIRECT"); + } + + if ((Option[2].Value.dwValue & PROXY_TYPE_PROXY) == PROXY_TYPE_PROXY) + { + LOG("PROXY_TYPE_PROXY"); + } + + if (!InternetQueryOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &List, &nSize)) + { + LOG("InternetQueryOption failed,GLE=" + QSTRN(GetLastError())); + } + + if (Option[4].Value.pszValue != nullptr) + { + LOG(QString::fromStdWString(Option[4].Value.pszValue)); + } + + INTERNET_VERSION_INFO Version; + nSize = sizeof(INTERNET_VERSION_INFO); + InternetQueryOption(nullptr, INTERNET_OPTION_VERSION, &Version, &nSize); + + if (Option[0].Value.pszValue != nullptr) + { + GlobalFree(Option[0].Value.pszValue); + } + + if (Option[3].Value.pszValue != nullptr) + { + GlobalFree(Option[3].Value.pszValue); + } + + if (Option[4].Value.pszValue != nullptr) + { + GlobalFree(Option[4].Value.pszValue); + } + + return false; + } + bool __SetProxyOptions(LPWSTR proxy_full_addr, bool isPAC) + { + INTERNET_PER_CONN_OPTION_LIST list; + DWORD dwBufSize = sizeof(list); + // Fill the list structure. + list.dwSize = sizeof(list); + // NULL == LAN, otherwise connectoid name. + list.pszConnection = nullptr; + + if (nullptr == proxy_full_addr) + { + LOG("Clearing system proxy"); + // + list.dwOptionCount = 1; + list.pOptions = new INTERNET_PER_CONN_OPTION[1]; + + // Ensure that the memory was allocated. + if (nullptr == list.pOptions) + { + // Return if the memory wasn't allocated. + return false; + } + + // Set flags. + list.pOptions[0].dwOption = INTERNET_PER_CONN_FLAGS; + list.pOptions[0].Value.dwValue = PROXY_TYPE_DIRECT; + } + else if (isPAC) + { + LOG("Setting system proxy for PAC"); + // + list.dwOptionCount = 2; + list.pOptions = new INTERNET_PER_CONN_OPTION[2]; + + if (nullptr == list.pOptions) + { + return false; + } + + // Set flags. + list.pOptions[0].dwOption = INTERNET_PER_CONN_FLAGS; + list.pOptions[0].Value.dwValue = PROXY_TYPE_DIRECT | PROXY_TYPE_AUTO_PROXY_URL; + // Set proxy name. + list.pOptions[1].dwOption = INTERNET_PER_CONN_AUTOCONFIG_URL; + list.pOptions[1].Value.pszValue = proxy_full_addr; + } + else + { + LOG("Setting system proxy for Global Proxy"); + // + list.dwOptionCount = 2; + list.pOptions = new INTERNET_PER_CONN_OPTION[2]; + + if (nullptr == list.pOptions) + { + return false; + } + + // Set flags. + list.pOptions[0].dwOption = INTERNET_PER_CONN_FLAGS; + list.pOptions[0].Value.dwValue = PROXY_TYPE_DIRECT | PROXY_TYPE_PROXY; + // Set proxy name. + list.pOptions[1].dwOption = INTERNET_PER_CONN_PROXY_SERVER; + list.pOptions[1].Value.pszValue = proxy_full_addr; + // Set proxy override. + // list.pOptions[2].dwOption = INTERNET_PER_CONN_PROXY_BYPASS; + // auto localhost = L"localhost"; + // list.pOptions[2].Value.pszValue = NO_CONST(localhost); + } + + // Set proxy for LAN. + if (!InternetSetOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, dwBufSize)) + { + LOG("InternetSetOption failed for LAN, GLE=" + QSTRN(GetLastError())); + } + + RASENTRYNAME entry; + entry.dwSize = sizeof(entry); + std::vector entries; + DWORD size = sizeof(entry), count; + LPRASENTRYNAME entryAddr = &entry; + auto ret = RasEnumEntries(nullptr, nullptr, entryAddr, &size, &count); + if (ERROR_BUFFER_TOO_SMALL == ret) + { + entries.resize(count); + entries[0].dwSize = sizeof(RASENTRYNAME); + entryAddr = entries.data(); + ret = RasEnumEntries(nullptr, nullptr, entryAddr, &size, &count); + } + if (ERROR_SUCCESS != ret) + { + LOG("Failed to list entry names"); + return false; + } + + // Set proxy for each connectoid. + for (DWORD i = 0; i < count; ++i) + { + list.pszConnection = entryAddr[i].szEntryName; + if (!InternetSetOption(nullptr, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, dwBufSize)) + { + LOG("InternetSetOption failed for connectoid " + QString::fromWCharArray(list.pszConnection) + ", GLE=" + QSTRN(GetLastError())); + } + } + + delete[] list.pOptions; + InternetSetOption(nullptr, INTERNET_OPTION_SETTINGS_CHANGED, nullptr, 0); + InternetSetOption(nullptr, INTERNET_OPTION_REFRESH, nullptr, 0); + return true; + } +#endif + + void SetSystemProxy(const QString &address, int httpPort, int socksPort) { + LOG("Setting up System Proxy"); + bool hasHTTP = (httpPort > 0 && httpPort < 65536); + bool hasSOCKS = (socksPort > 0 && socksPort < 65536); + +#ifdef Q_OS_WIN + if (!hasHTTP) + { + LOG("Nothing?"); + return; + } + else + { + LOG("Qv2ray will set system proxy to use HTTP"); + } +#else + if (!hasHTTP && !hasSOCKS) { + LOG("Nothing?"); + return; + } + + if (hasHTTP) { + LOG("Qv2ray will set system proxy to use HTTP"); + } + + if (hasSOCKS) { + LOG("Qv2ray will set system proxy to use SOCKS"); + } +#endif + +#ifdef Q_OS_WIN + QString __a; + const QHostAddress ha(address); + const auto type = ha.protocol(); + if (type == QAbstractSocket::IPv6Protocol) + { + // many software do not recognize IPv6 proxy server string though + const auto str = ha.toString(); // RFC5952 + __a = "[" + str + "]:" + QSTRN(httpPort); + } + else + { + __a = address + ":" + QSTRN(httpPort); + } + + LOG("Windows proxy string: " + __a); + auto proxyStrW = new WCHAR[__a.length() + 1]; + wcscpy(proxyStrW, __a.toStdWString().c_str()); + // + __QueryProxyOptions(); + + if (!__SetProxyOptions(proxyStrW, false)) + { + LOG("Failed to set proxy."); + } + + __QueryProxyOptions(); +#elif defined(Q_OS_LINUX) + QList actions; + actions << ProcessArgument{"gsettings", {"set", "org.gnome.system.proxy", "mode", "manual"}}; + // + bool isKDE = qEnvironmentVariable("XDG_SESSION_DESKTOP") == "KDE" || + qEnvironmentVariable("XDG_SESSION_DESKTOP") == "plasma"; + const auto configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); + + // + // Configure HTTP Proxies for HTTP, FTP and HTTPS + if (hasHTTP) { + // iterate over protocols... + for (const auto &protocol: QStringList{"http", "ftp", "https"}) { + // for GNOME: + { + actions << ProcessArgument{"gsettings", + {"set", "org.gnome.system.proxy." + protocol, "host", address}}; + actions << ProcessArgument{"gsettings", + {"set", "org.gnome.system.proxy." + protocol, "port", QSTRN(httpPort)}}; + } + + // for KDE: + if (isKDE) { + actions << ProcessArgument{"kwriteconfig5", + {"--file", configPath + "/kioslaverc", // + "--group", "Proxy Settings", // + "--key", protocol + "Proxy", // + "http://" + address + " " + QSTRN(httpPort)}}; + } + } + } + + // Configure SOCKS5 Proxies + if (hasSOCKS) { + // for GNOME: + { + actions << ProcessArgument{"gsettings", {"set", "org.gnome.system.proxy.socks", "host", address}}; + actions << ProcessArgument{"gsettings", + {"set", "org.gnome.system.proxy.socks", "port", QSTRN(socksPort)}}; + + // for KDE: + if (isKDE) { + actions << ProcessArgument{"kwriteconfig5", + {"--file", configPath + "/kioslaverc", // + "--group", "Proxy Settings", // + "--key", "socksProxy", // + "socks://" + address + " " + QSTRN(socksPort)}}; + } + } + } + // Setting Proxy Mode to Manual + { + // for GNOME: + { + actions << ProcessArgument{"gsettings", {"set", "org.gnome.system.proxy", "mode", "manual"}}; + } + + // for KDE: + if (isKDE) { + actions << ProcessArgument{"kwriteconfig5", + {"--file", configPath + "/kioslaverc", // + "--group", "Proxy Settings", // + "--key", "ProxyType", "1"}}; + } + } + + // Notify kioslaves to reload system proxy configuration. + if (isKDE) { + actions << ProcessArgument{"dbus-send", + {"--type=signal", "/KIO/Scheduler", // + "org.kde.KIO.Scheduler.reparseSlaveConfiguration", // + "string:''"}}; + } + // Execute them all! + // + // note: do not use std::all_of / any_of / none_of, + // because those are short-circuit and cannot guarantee atomicity. + QList results; + for (const auto &action: actions) { + // execute and get the code + const auto returnCode = QProcess::execute(action.first, action.second); + // print out the commands and result codes + DEBUG(QString("[%1] Program: %2, Args: %3").arg(returnCode).arg(action.first).arg(action.second.join(";"))); + // give the code back + results << (returnCode == QProcess::NormalExit); + } + + if (results.count(true) != actions.size()) { + LOG("Something wrong when setting proxies."); + } +#else + + for (const auto &service : macOSgetNetworkServices()) + { + LOG("Setting proxy for interface: " + service); + if (hasHTTP) + { + QProcess::execute("/usr/sbin/networksetup", { "-setwebproxystate", service, "on" }); + QProcess::execute("/usr/sbin/networksetup", { "-setsecurewebproxystate", service, "on" }); + QProcess::execute("/usr/sbin/networksetup", { "-setwebproxy", service, address, QSTRN(httpPort) }); + QProcess::execute("/usr/sbin/networksetup", { "-setsecurewebproxy", service, address, QSTRN(httpPort) }); + } + + if (hasSOCKS) + { + QProcess::execute("/usr/sbin/networksetup", { "-setsocksfirewallproxystate", service, "on" }); + QProcess::execute("/usr/sbin/networksetup", { "-setsocksfirewallproxy", service, address, QSTRN(socksPort) }); + } + } + +#endif + } + + void ClearSystemProxy() { + LOG("Clearing System Proxy"); + +#ifdef Q_OS_WIN + if (!__SetProxyOptions(nullptr, false)) + { + LOG("Failed to clear proxy."); + } +#elif defined(Q_OS_LINUX) + QList actions; + const bool isKDE = qEnvironmentVariable("XDG_SESSION_DESKTOP") == "KDE" || + qEnvironmentVariable("XDG_SESSION_DESKTOP") == "plasma"; + const auto configRoot = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); + + // Setting System Proxy Mode to: None + { + // for GNOME: + { + actions << ProcessArgument{"gsettings", {"set", "org.gnome.system.proxy", "mode", "none"}}; + } + + // for KDE: + if (isKDE) { + actions << ProcessArgument{"kwriteconfig5", + {"--file", configRoot + "/kioslaverc", // + "--group", "Proxy Settings", // + "--key", "ProxyType", "0"}}; + } + } + + // Notify kioslaves to reload system proxy configuration. + if (isKDE) { + actions << ProcessArgument{"dbus-send", + {"--type=signal", "/KIO/Scheduler", // + "org.kde.KIO.Scheduler.reparseSlaveConfiguration", // + "string:''"}}; + } + + // Execute the Actions + for (const auto &action: actions) { + // execute and get the code + const auto returnCode = QProcess::execute(action.first, action.second); + // print out the commands and result codes + DEBUG(QString("[%1] Program: %2, Args: %3").arg(returnCode).arg(action.first).arg(action.second.join(";"))); + } + +#else + for (const auto &service : macOSgetNetworkServices()) + { + LOG("Clearing proxy for interface: " + service); + QProcess::execute("/usr/sbin/networksetup", { "-setautoproxystate", service, "off" }); + QProcess::execute("/usr/sbin/networksetup", { "-setwebproxystate", service, "off" }); + QProcess::execute("/usr/sbin/networksetup", { "-setsecurewebproxystate", service, "off" }); + QProcess::execute("/usr/sbin/networksetup", { "-setsocksfirewallproxystate", service, "off" }); + } + +#endif + } +} // namespace Qv2ray::components::proxy + +#endif diff --git a/qv2ray/components/proxy/QvProxyConfigurator.hpp b/qv2ray/components/proxy/QvProxyConfigurator.hpp new file mode 100644 index 0000000..a6b0300 --- /dev/null +++ b/qv2ray/components/proxy/QvProxyConfigurator.hpp @@ -0,0 +1,13 @@ +#pragma once +#include +#include +#include +// +namespace Qv2ray::components::proxy +{ + void ClearSystemProxy(); + void SetSystemProxy(const QString &address, int http_port, int socks_port); +} // namespace Qv2ray::components::proxy + +using namespace Qv2ray::components; +using namespace Qv2ray::components::proxy; diff --git a/qv2ray/ui/LogHighlighter.cpp b/qv2ray/ui/LogHighlighter.cpp new file mode 100644 index 0000000..4644878 --- /dev/null +++ b/qv2ray/ui/LogHighlighter.cpp @@ -0,0 +1,148 @@ +#include "LogHighlighter.hpp" + +#define TO_EOL "(([\\s\\S]*)|([\\d\\D]*)|([\\w\\W]*))$" +#define REGEX_IPV6_ADDR \ + R"(\[\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*\])" +#define REGEX_IPV4_ADDR \ + R"((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5]))" +#define REGEX_PORT_NUMBER R"(([0-9]|[1-9]\d{1,3}|[1-5]\d{4}|6[0-5]{2}[0-3][0-5])*)" + +namespace Qv2ray::ui { + SyntaxHighlighter::SyntaxHighlighter(bool darkMode, QTextDocument *parent) : QSyntaxHighlighter(parent) { + HighlightingRule rule; + + if (darkMode) { + tcpudpFormat.setForeground(QColor(0, 200, 230)); + ipHostFormat.setForeground(Qt::yellow); + warningFormat.setForeground(QColor(255, 160, 15)); + warningFormat2.setForeground(Qt::cyan); + } else { + ipHostFormat.setForeground(Qt::black); + ipHostFormat.setFontWeight(QFont::Bold); + tcpudpFormat.setForeground(QColor(0, 52, 130)); + warningFormat.setBackground(QColor(255, 160, 15)); + warningFormat.setForeground(Qt::white); + warningFormat2.setForeground(Qt::darkCyan); + } + + dateFormat.setForeground(darkMode ? Qt::cyan : Qt::darkCyan); + rule.pattern = QRegularExpression("\\d\\d\\d\\d/\\d\\d/\\d\\d"); + rule.format = dateFormat; + highlightingRules.append(rule); + // + timeFormat.setForeground(darkMode ? Qt::cyan : Qt::darkCyan); + rule.pattern = QRegularExpression("\\d\\d:\\d\\d:\\d\\d"); + rule.format = timeFormat; + highlightingRules.append(rule); + // + debugFormat.setForeground(Qt::darkGray); + rule.pattern = QRegularExpression("\\[[Dd]ebug\\]" TO_EOL); + rule.format = debugFormat; + highlightingRules.append(rule); + // + infoFormat.setForeground(darkMode ? Qt::lightGray : Qt::darkCyan); + rule.pattern = QRegularExpression("\\[[Ii]nfo\\]" TO_EOL); + rule.format = infoFormat; + highlightingRules.append(rule); + // + + const static QColor darkGreenColor(10, 180, 0); + // + // + acceptedFormat.setForeground(darkGreenColor); + acceptedFormat.setFontItalic(true); + acceptedFormat.setFontWeight(QFont::Bold); + rule.pattern = QRegularExpression("\\saccepted\\s"); + rule.format = acceptedFormat; + highlightingRules.append(rule); + // + rejectedFormat.setFontWeight(QFont::Bold); + rejectedFormat.setBackground(Qt::red); + rejectedFormat.setForeground(Qt::white); + rejectedFormat.setFontItalic(true); + rejectedFormat.setFontWeight(QFont::Bold); + rule.pattern = QRegularExpression("\\srejected\\s" TO_EOL); + rule.format = rejectedFormat; + highlightingRules.append(rule); + // + v2rayComponentFormat.setForeground(darkMode ? darkGreenColor : Qt::darkYellow); + rule.pattern = QRegularExpression(R"( (\w+\/)+\w+: )"); + rule.format = v2rayComponentFormat; + highlightingRules.append(rule); + // + warningFormat.setFontWeight(QFont::Bold); + rule.pattern = QRegularExpression("\\[[Ww]arning\\]" TO_EOL); + rule.format = warningFormat; + highlightingRules.append(rule); + // + warningFormat2.setFontWeight(QFont::Bold); + rule.pattern = QRegularExpression("\\[[Ww]arning\\]" TO_EOL); + rule.format = warningFormat2; + highlightingRules.append(rule); + // + failedFormat.setFontWeight(QFont::Bold); + failedFormat.setBackground(Qt::red); + failedFormat.setForeground(Qt::white); + rule.pattern = QRegularExpression("failed"); + rule.format = failedFormat; + highlightingRules.append(rule); + // + qvAppLogFormat.setForeground(darkMode ? Qt::cyan : Qt::darkCyan); + rule.pattern = QRegularExpression("\\[[A-Z]*\\]:"); + rule.format = qvAppLogFormat; + highlightingRules.append(rule); + // + qvAppDebugLogFormat.setForeground(darkMode ? Qt::yellow : Qt::darkYellow); + rule.pattern = QRegularExpression(R"( \[\w+\] )"); + rule.format = qvAppDebugLogFormat; + highlightingRules.append(rule); + // + rule.pattern = QRegularExpression("default route"); + rule.format = qvAppDebugLogFormat; + highlightingRules.append(rule); + // + rule.pattern = QRegularExpression(">>>>+"); + rule.format = warningFormat; + highlightingRules.append(rule); + + { + // IP IPv6 Host; + rule.pattern = QRegularExpression(REGEX_IPV4_ADDR ":" REGEX_PORT_NUMBER); + rule.pattern.setPatternOptions(QRegularExpression::ExtendedPatternSyntaxOption); + rule.format = ipHostFormat; + highlightingRules.append(rule); + // + rule.pattern = QRegularExpression(REGEX_IPV6_ADDR ":" REGEX_PORT_NUMBER); + rule.pattern.setPatternOptions(QRegularExpression::ExtendedPatternSyntaxOption); + rule.format = ipHostFormat; + highlightingRules.append(rule); + // + rule.pattern = QRegularExpression( + "([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}(/|):" REGEX_PORT_NUMBER); + rule.pattern.setPatternOptions(QRegularExpression::PatternOption::ExtendedPatternSyntaxOption); + rule.format = ipHostFormat; + highlightingRules.append(rule); + } + + for (const auto &pattern: {"tcp", "udp"}) { + tcpudpFormat.setFontWeight(QFont::Bold); + rule.pattern = QRegularExpression(pattern); + rule.format = tcpudpFormat; + highlightingRules.append(rule); + } + + } + + void SyntaxHighlighter::highlightBlock(const QString &text) { + for (const HighlightingRule &rule: qAsConst(highlightingRules)) { + QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text); + + while (matchIterator.hasNext()) { + QRegularExpressionMatch match = matchIterator.next(); + setFormat(match.capturedStart(), match.capturedLength(), rule.format); + } + } + + setCurrentBlockState(0); + } +} // namespace Qv2ray::ui diff --git a/qv2ray/ui/LogHighlighter.hpp b/qv2ray/ui/LogHighlighter.hpp new file mode 100644 index 0000000..a5bbc5d --- /dev/null +++ b/qv2ray/ui/LogHighlighter.hpp @@ -0,0 +1,95 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#pragma once +#include +#include +#include +#include + +namespace Qv2ray::ui +{ + class SyntaxHighlighter : public QSyntaxHighlighter + { + Q_OBJECT + + public: + explicit SyntaxHighlighter(bool darkMode, QTextDocument *parent = nullptr); + + protected: + void highlightBlock(const QString &text) override; + + private: + struct HighlightingRule + { + QRegularExpression pattern; + QTextCharFormat format; + }; + QVector highlightingRules; + + QTextCharFormat tcpudpFormat; + QTextCharFormat dateFormat; + QTextCharFormat acceptedFormat; + QTextCharFormat rejectedFormat; + QTextCharFormat failedFormat; + QTextCharFormat warningFormat; + QTextCharFormat warningFormat2; + QTextCharFormat infoFormat; + QTextCharFormat debugFormat; + QTextCharFormat timeFormat; + QTextCharFormat ipHostFormat; + QTextCharFormat v2rayComponentFormat; + // + QTextCharFormat qvAppLogFormat; + QTextCharFormat qvAppDebugLogFormat; + }; +} // namespace Qv2ray::ui + +using namespace Qv2ray::ui; diff --git a/qv2ray/ui/QvAutoCompleteTextEdit.cpp b/qv2ray/ui/QvAutoCompleteTextEdit.cpp new file mode 100644 index 0000000..910d5d5 --- /dev/null +++ b/qv2ray/ui/QvAutoCompleteTextEdit.cpp @@ -0,0 +1,167 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "QvAutoCompleteTextEdit.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Qv2ray::ui::widgets +{ + AutoCompleteTextEdit::AutoCompleteTextEdit(const QString &prefix, const QStringList &sourceStrings, QWidget *parent) : QPlainTextEdit(parent) + { + this->prefix = prefix; + this->setLineWrapMode(QPlainTextEdit::NoWrap); + c = new QCompleter(this); + c->setModel(new QStringListModel(sourceStrings, c)); + c->setWidget(this); + c->setCompletionMode(QCompleter::PopupCompletion); + c->setCaseSensitivity(Qt::CaseInsensitive); + QObject::connect(c, QOverload::of(&QCompleter::activated), this, &AutoCompleteTextEdit::insertCompletion); + } + + AutoCompleteTextEdit::~AutoCompleteTextEdit() + { + } + + void AutoCompleteTextEdit::insertCompletion(const QString &completion) + { + QTextCursor tc = textCursor(); + int extra = completion.length() - c->completionPrefix().length(); + tc.movePosition(QTextCursor::Left); + tc.movePosition(QTextCursor::EndOfWord); + tc.insertText(completion.right(extra)); + setTextCursor(tc); + } + + QString AutoCompleteTextEdit::lineUnderCursor() const + { + QTextCursor tc = textCursor(); + tc.select(QTextCursor::LineUnderCursor); + return tc.selectedText(); + } + + QString AutoCompleteTextEdit::wordUnderCursor() const + { + QTextCursor tc = textCursor(); + tc.select(QTextCursor::WordUnderCursor); + return tc.selectedText(); + } + + void AutoCompleteTextEdit::focusInEvent(QFocusEvent *e) + { + if (c) + c->setWidget(this); + + QPlainTextEdit::focusInEvent(e); + } + + void AutoCompleteTextEdit::keyPressEvent(QKeyEvent *e) + { + const bool hasCtrlOrShiftModifier = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::ShiftModifier); + const bool hasOtherModifiers = (e->modifiers() != Qt::NoModifier) && !hasCtrlOrShiftModifier; // has other modifiers + // + const bool isSpace = (e->modifiers().testFlag(Qt::ShiftModifier) || e->modifiers().testFlag(Qt::NoModifier)) // + && e->key() == Qt::Key_Space; + const bool isTab = (e->modifiers().testFlag(Qt::NoModifier) && e->key() == Qt::Key_Tab); + const bool isOtherSpace = e->text() == " "; + // + if (isSpace || isTab || isOtherSpace) + { + QToolTip::showText(this->mapToGlobal(QPoint(0, 0)), tr("You can not input space characters here."), this, QRect{}, 2000); + return; + } + // + if (c && c->popup()->isVisible()) + { + // The following keys are forwarded by the completer to the widget + switch (e->key()) + { + case Qt::Key_Enter: + case Qt::Key_Return: + case Qt::Key_Escape: + case Qt::Key_Tab: + case Qt::Key_Backtab: e->ignore(); return; // let the completer do default behavior + + default: break; + } + } + + QPlainTextEdit::keyPressEvent(e); + + if (!c || (hasCtrlOrShiftModifier && e->text().isEmpty())) + return; + + // if we have other modifiers, or the text is empty, or the line does not start with our prefix. + if (hasOtherModifiers || e->text().isEmpty() || !lineUnderCursor().startsWith(prefix)) + { + c->popup()->hide(); + return; + } + + if (auto word = wordUnderCursor(); word != c->completionPrefix()) + { + c->setCompletionPrefix(word); + c->popup()->setCurrentIndex(c->completionModel()->index(0, 0)); + } + + QRect cr = cursorRect(); + cr.setWidth(c->popup()->sizeHintForColumn(0) + c->popup()->verticalScrollBar()->sizeHint().width()); + c->complete(cr); // popup it up! + } +} // namespace Qv2ray::ui::widgets diff --git a/qv2ray/ui/QvAutoCompleteTextEdit.hpp b/qv2ray/ui/QvAutoCompleteTextEdit.hpp new file mode 100644 index 0000000..e2e930c --- /dev/null +++ b/qv2ray/ui/QvAutoCompleteTextEdit.hpp @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#pragma once +#include +#include +QT_BEGIN_NAMESPACE +class QCompleter; +QT_END_NAMESPACE + +namespace Qv2ray::ui::widgets +{ + class AutoCompleteTextEdit : public QPlainTextEdit + { + Q_OBJECT + + public: + AutoCompleteTextEdit(const QString &prefix, const QStringList &sourceStrings, QWidget *parent = nullptr); + ~AutoCompleteTextEdit(); + + protected: + void keyPressEvent(QKeyEvent *e) override; + void focusInEvent(QFocusEvent *e) override; + + private slots: + void insertCompletion(const QString &completion); + + private: + QString lineUnderCursor() const; + QString wordUnderCursor() const; + + QString prefix; + QCompleter *c = nullptr; + }; +} // namespace Qv2ray::ui::widgets +using namespace Qv2ray::ui::widgets; diff --git a/qv2ray/ui/widgets/common/QJsonModel.cpp b/qv2ray/ui/widgets/common/QJsonModel.cpp new file mode 100644 index 0000000..c633b8c --- /dev/null +++ b/qv2ray/ui/widgets/common/QJsonModel.cpp @@ -0,0 +1,402 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2011 SCHUTZ Sacha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "QJsonModel.hpp" + +#include +#include + +QJsonTreeItem::QJsonTreeItem(QJsonTreeItem *parent) +{ + mParent = parent; +} + +QJsonTreeItem::~QJsonTreeItem() +{ + qDeleteAll(mChilds); +} + +void QJsonTreeItem::appendChild(QJsonTreeItem *item) +{ + mChilds.append(item); +} + +QJsonTreeItem *QJsonTreeItem::child(int row) +{ + return mChilds.value(row); +} + +QJsonTreeItem *QJsonTreeItem::parent() +{ + return mParent; +} + +int QJsonTreeItem::childCount() const +{ + return mChilds.count(); +} + +int QJsonTreeItem::row() const +{ + if (mParent) + return mParent->mChilds.indexOf(const_cast(this)); + + return 0; +} + +void QJsonTreeItem::setKey(const QString &key) +{ + mKey = key; +} + +void QJsonTreeItem::setValue(const QString &value) +{ + mValue = value; +} + +void QJsonTreeItem::setType(const QJsonValue::Type &type) +{ + mType = type; +} + +QString QJsonTreeItem::key() const +{ + return mKey; +} + +QString QJsonTreeItem::value() const +{ + return mValue; +} + +QJsonValue::Type QJsonTreeItem::type() const +{ + return mType; +} + +QJsonTreeItem *QJsonTreeItem::load(const QJsonValue &value, QJsonTreeItem *parent) +{ + QJsonTreeItem *rootItem = new QJsonTreeItem(parent); + rootItem->setKey("root"); + + if (value.isObject()) + { + // Get all QJsonValue childs + for (QString key : value.toObject().keys()) + { + QJsonValue v = value.toObject().value(key); + QJsonTreeItem *child = load(v, rootItem); + child->setKey(key); + child->setType(v.type()); + rootItem->appendChild(child); + } + } + else if (value.isArray()) + { + // Get all QJsonValue childs + int index = 0; + + for (QJsonValue v : value.toArray()) + { + QJsonTreeItem *child = load(v, rootItem); + child->setKey(QString::number(index)); + child->setType(v.type()); + rootItem->appendChild(child); + ++index; + } + } + else + { + rootItem->setValue(value.toVariant().toString()); + rootItem->setType(value.type()); + } + + return rootItem; +} + +//========================================================================= + +QJsonModel::QJsonModel(QObject *parent) : QAbstractItemModel(parent), mRootItem{ new QJsonTreeItem } +{ + mHeaders.append("key"); + mHeaders.append("value"); +} + +QJsonModel::QJsonModel(const QString &fileName, QObject *parent) : QAbstractItemModel(parent), mRootItem{ new QJsonTreeItem } +{ + mHeaders.append("key"); + mHeaders.append("value"); + load(fileName); +} + +QJsonModel::QJsonModel(QIODevice *device, QObject *parent) : QAbstractItemModel(parent), mRootItem{ new QJsonTreeItem } +{ + mHeaders.append("key"); + mHeaders.append("value"); + load(device); +} + +QJsonModel::QJsonModel(const QByteArray &json, QObject *parent) : QAbstractItemModel(parent), mRootItem{ new QJsonTreeItem } +{ + mHeaders.append("key"); + mHeaders.append("value"); + loadJson(json); +} + +QJsonModel::~QJsonModel() +{ + delete mRootItem; +} + +bool QJsonModel::load(const QString &fileName) +{ + QFile file(fileName); + bool success = false; + + if (file.open(QIODevice::ReadOnly)) + { + success = load(&file); + file.close(); + } + else + success = false; + + return success; +} + +bool QJsonModel::load(QIODevice *device) +{ + return loadJson(device->readAll()); +} + +bool QJsonModel::loadJson(const QByteArray &json) +{ + auto const &jdoc = QJsonDocument::fromJson(json); + + if (!jdoc.isNull()) + { + beginResetModel(); + delete mRootItem; + + if (jdoc.isArray()) + { + mRootItem = QJsonTreeItem::load(QJsonValue(jdoc.array())); + mRootItem->setType(QJsonValue::Array); + } + else + { + mRootItem = QJsonTreeItem::load(QJsonValue(jdoc.object())); + mRootItem->setType(QJsonValue::Object); + } + + endResetModel(); + return true; + } + + qDebug() << Q_FUNC_INFO << "cannot load json"; + return false; +} + +QVariant QJsonModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + QJsonTreeItem *item = static_cast(index.internalPointer()); + + if (role == Qt::DisplayRole) + { + if (index.column() == 0) + return QString("%1").arg(item->key()); + + if (index.column() == 1) + return QString("%1").arg(item->value()); + } + else if (Qt::EditRole == role) + { + if (index.column() == 1) + { + return QString("%1").arg(item->value()); + } + } + + return QVariant(); +} + +bool QJsonModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + int col = index.column(); + + if (Qt::EditRole == role) + { + if (col == 1) + { + QJsonTreeItem *item = static_cast(index.internalPointer()); + item->setValue(value.toString()); + emit dataChanged(index, index, { Qt::EditRole }); + return true; + } + } + + return false; +} + +QVariant QJsonModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + if (orientation == Qt::Horizontal) + { + return mHeaders.value(section); + } + else + return QVariant(); +} + +QModelIndex QJsonModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent)) + return QModelIndex(); + + QJsonTreeItem *parentItem; + + if (!parent.isValid()) + parentItem = mRootItem; + else + parentItem = static_cast(parent.internalPointer()); + + QJsonTreeItem *childItem = parentItem->child(row); + + if (childItem) + return createIndex(row, column, childItem); + else + return QModelIndex(); +} + +QModelIndex QJsonModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + return QModelIndex(); + + QJsonTreeItem *childItem = static_cast(index.internalPointer()); + QJsonTreeItem *parentItem = childItem->parent(); + + if (parentItem == mRootItem) + return QModelIndex(); + + return createIndex(parentItem->row(), 0, parentItem); +} + +int QJsonModel::rowCount(const QModelIndex &parent) const +{ + QJsonTreeItem *parentItem; + + if (parent.column() > 0) + return 0; + + if (!parent.isValid()) + parentItem = mRootItem; + else + parentItem = static_cast(parent.internalPointer()); + + return parentItem->childCount(); +} + +int QJsonModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 2; +} + +Qt::ItemFlags QJsonModel::flags(const QModelIndex &index) const +{ + int col = index.column(); + auto item = static_cast(index.internalPointer()); + auto isArray = QJsonValue::Array == item->type(); + auto isObject = QJsonValue::Object == item->type(); + + if ((col == 1) && !(isArray || isObject)) + { + return Qt::ItemIsEditable | QAbstractItemModel::flags(index); + } + else + { + return QAbstractItemModel::flags(index); + } +} + +QJsonDocument QJsonModel::json() const +{ + auto v = genJson(mRootItem); + QJsonDocument doc; + + if (v.isObject()) + { + doc = QJsonDocument(v.toObject()); + } + else + { + doc = QJsonDocument(v.toArray()); + } + + return doc; +} + +QJsonValue QJsonModel::genJson(QJsonTreeItem *item) const +{ + auto type = item->type(); + int nchild = item->childCount(); + + if (QJsonValue::Object == type) + { + QJsonObject jo; + + for (int i = 0; i < nchild; ++i) + { + auto ch = item->child(i); + auto key = ch->key(); + jo.insert(key, genJson(ch)); + } + + return jo; + } + else if (QJsonValue::Array == type) + { + QJsonArray arr; + + for (int i = 0; i < nchild; ++i) + { + auto ch = item->child(i); + arr.append(genJson(ch)); + } + + return arr; + } + else + { + QJsonValue va(item->value()); + return va; + } +} diff --git a/qv2ray/ui/widgets/common/QJsonModel.hpp b/qv2ray/ui/widgets/common/QJsonModel.hpp new file mode 100644 index 0000000..3b07e7c --- /dev/null +++ b/qv2ray/ui/widgets/common/QJsonModel.hpp @@ -0,0 +1,94 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2011 SCHUTZ Sacha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +class QJsonModel; +class QJsonItem; + +class QJsonTreeItem +{ + public: + QJsonTreeItem(QJsonTreeItem *parent = nullptr); + ~QJsonTreeItem(); + void appendChild(QJsonTreeItem *item); + QJsonTreeItem *child(int row); + QJsonTreeItem *parent(); + int childCount() const; + int row() const; + void setKey(const QString &key); + void setValue(const QString &value); + void setType(const QJsonValue::Type &type); + QString key() const; + QString value() const; + QJsonValue::Type type() const; + + static QJsonTreeItem *load(const QJsonValue &value, QJsonTreeItem *parent = 0); + + protected: + private: + QString mKey; + QString mValue; + QJsonValue::Type mType; + QList mChilds; + QJsonTreeItem *mParent; +}; + +//--------------------------------------------------- + +class QJsonModel : public QAbstractItemModel +{ + Q_OBJECT + public: + explicit QJsonModel(QObject *parent = nullptr); + QJsonModel(const QString &fileName, QObject *parent = nullptr); + QJsonModel(QIODevice *device, QObject *parent = nullptr); + QJsonModel(const QByteArray &json, QObject *parent = nullptr); + ~QJsonModel(); + bool load(const QString &fileName); + bool load(QIODevice *device); + bool loadJson(const QByteArray &json); + QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) Q_DECL_OVERRIDE; + QVariant headerData(int section, Qt::Orientation orientation, int role) const Q_DECL_OVERRIDE; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + QJsonDocument json() const; + + private: + QJsonValue genJson(QJsonTreeItem *) const; + + QJsonTreeItem *mRootItem; + QStringList mHeaders; +}; diff --git a/qv2ray/ui/widgets/editors/w_JsonEditor.cpp b/qv2ray/ui/widgets/editors/w_JsonEditor.cpp new file mode 100644 index 0000000..06e7edd --- /dev/null +++ b/qv2ray/ui/widgets/editors/w_JsonEditor.cpp @@ -0,0 +1,92 @@ +#include "w_JsonEditor.hpp" + +#include "main/NekoRay.hpp" + +JsonEditor::JsonEditor(const QJsonObject& rootObject, QWidget *parent) : QDialog(parent) { + setupUi(this); +// QvMessageBusConnect(JsonEditor); + // + original = rootObject; + final = rootObject; + QString jsonString = JsonToString(rootObject); + + if (VerifyJsonString(jsonString).isEmpty()) { + jsonTree->setModel(&model); + model.loadJson(QJsonDocument(rootObject).toJson()); + } else { + QvMessageBoxWarn(this, tr("Json Contains Syntax Errors"), + tr("Original Json may contain syntax errors. Json tree is disabled.")); + } + + jsonEditor->setText(JsonToString(rootObject)); + jsonTree->expandAll(); + jsonTree->resizeColumnToContents(0); +} + +//QvMessageBusSlotImpl(JsonEditor) +// { +// switch (msg) +// { +// MBShowDefaultImpl; +// MBHideDefaultImpl; +// MBRetranslateDefaultImpl; +// case UPDATE_COLORSCHEME: +// break; +// } +// } + +QJsonObject JsonEditor::OpenEditor() { + int resultCode = this->exec(); + auto string = jsonEditor->toPlainText(); + + while (resultCode == QDialog::Accepted && !VerifyJsonString(string).isEmpty()) { + QvMessageBoxWarn(this, tr("Json Contains Syntax Errors"), + tr("You must correct these errors before continuing.")); + resultCode = this->exec(); + string = jsonEditor->toPlainText(); + } + + return resultCode == QDialog::Accepted ? final : original; +} + +JsonEditor::~JsonEditor() { +} + +void JsonEditor::on_jsonEditor_textChanged() { + auto string = jsonEditor->toPlainText(); + auto VerifyResult = VerifyJsonString(string); + jsonValidateStatus->setText(VerifyResult); + + if (VerifyResult.isEmpty()) { + BLACK(jsonEditor); + final = JsonFromString(string); + model.loadJson(QJsonDocument(final).toJson()); + jsonTree->expandAll(); + jsonTree->resizeColumnToContents(0); + } else { + RED(jsonEditor); + } +} + +void JsonEditor::on_formatJsonBtn_clicked() { + auto string = jsonEditor->toPlainText(); + auto VerifyResult = VerifyJsonString(string); + jsonValidateStatus->setText(VerifyResult); + + if (VerifyResult.isEmpty()) { + BLACK(jsonEditor); + jsonEditor->setPlainText(JsonToString(JsonFromString(string))); + model.loadJson(QJsonDocument(JsonFromString(string)).toJson()); + jsonTree->setModel(&model); + jsonTree->expandAll(); + jsonTree->resizeColumnToContents(0); + } else { + RED(jsonEditor); + QvMessageBoxWarn(this, tr("Syntax Errors"), + tr("Please fix the JSON errors or remove the comments before continue")); + } +} + +void JsonEditor::on_removeCommentsBtn_clicked() { + jsonEditor->setPlainText(JsonToString(JsonFromString(jsonEditor->toPlainText()))); +} diff --git a/qv2ray/ui/widgets/editors/w_JsonEditor.hpp b/qv2ray/ui/widgets/editors/w_JsonEditor.hpp new file mode 100644 index 0000000..2f216dd --- /dev/null +++ b/qv2ray/ui/widgets/editors/w_JsonEditor.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "qv2ray/wrapper.hpp" +#include "qv2ray/ui/widgets/common/QJsonModel.hpp" +#include "ui_w_JsonEditor.h" + + +#include + +class JsonEditor + : public QDialog + , private Ui::JsonEditor +{ + Q_OBJECT + + public: + explicit JsonEditor(const QJsonObject& rootObject, QWidget *parent = nullptr); + ~JsonEditor(); + QJsonObject OpenEditor(); + + private slots: + void on_jsonEditor_textChanged(); + + void on_formatJsonBtn_clicked(); + + void on_removeCommentsBtn_clicked(); + + private: + QJsonModel model; + QJsonObject original; + QJsonObject final; +}; diff --git a/qv2ray/ui/widgets/editors/w_JsonEditor.ui b/qv2ray/ui/widgets/editors/w_JsonEditor.ui new file mode 100644 index 0000000..51dcd76 --- /dev/null +++ b/qv2ray/ui/widgets/editors/w_JsonEditor.ui @@ -0,0 +1,172 @@ + + + JsonEditor + + + Qt::ApplicationModal + + + + 0 + 0 + 889 + 572 + + + + JSON Editor + + + true + + + + + + Qt::Horizontal + + + false + + + + + + + + Monospace + + + + QTextEdit::NoWrap + + + false + + + + + + + Format JSON + + + + + + + Remove All Comments + + + + + + + Json Editor + + + + + + + + + + + Structure Preview + + + + + + + QAbstractItemView::NoEditTriggers + + + true + + + 15 + + + true + + + true + + + true + + + 132 + + + 152 + + + + + + + + + + + OK + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + jsonEditor + formatJsonBtn + removeCommentsBtn + jsonTree + + + + + buttonBox + accepted() + JsonEditor + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + JsonEditor + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/qv2ray/utils/HTTPRequestHelper.cpp b/qv2ray/utils/HTTPRequestHelper.cpp new file mode 100644 index 0000000..ce3bd3d --- /dev/null +++ b/qv2ray/utils/HTTPRequestHelper.cpp @@ -0,0 +1,100 @@ +#include "HTTPRequestHelper.hpp" + +#include +#include +#include +#include + +#include "main/NekoRay.hpp" + +#define QV_MODULE_NAME "NetworkCore" + +#include "qv2ray/wrapper.hpp" + +namespace Qv2ray::common::network { + void NetworkRequestHelper::setHeader(QNetworkRequest &request, const QByteArray &key, const QByteArray &value) { + DEBUG("Adding HTTP request header: " + key + ":" + value); + request.setRawHeader(key, value); + } + + void + NetworkRequestHelper::setAccessManagerAttributes(QNetworkRequest &request, QNetworkAccessManager &accessManager) { + + // Use proxy + if (NekoRay::dataStore->sub_use_proxy) { + QNetworkProxy p{QNetworkProxy::Socks5Proxy, "127.0.0.1", + static_cast(NekoRay::dataStore->inbound_socks_port)}; + accessManager.setProxy(p); + if (NekoRay::dataStore->started_id < 0) { + showLog(QObject::tr("Request with proxy but no profile started.")); + } + } + + if (accessManager.proxy().type() == QNetworkProxy::Socks5Proxy) { + DEBUG("Adding HostNameLookupCapability to proxy."); + accessManager.proxy().setCapabilities( + accessManager.proxy().capabilities() | QNetworkProxy::HostNameLookupCapability); + } + + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + // request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true); +#else + // request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true); +#endif + + request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, NekoRay::dataStore->user_agent); + } + + HTTPResponse NetworkRequestHelper::HttpGet(const QUrl &url) { + QNetworkRequest request; + QNetworkAccessManager accessManager; + request.setUrl(url); + setAccessManagerAttributes(request, accessManager); + auto _reply = accessManager.get(request); + // + { + QEventLoop loop; + QObject::connect(&accessManager, &QNetworkAccessManager::finished, &loop, &QEventLoop::quit); + loop.exec(); + } + // + return HTTPResponse{_reply->error() == QNetworkReply::NetworkError::NoError ? "" : _reply->errorString(), + _reply->readAll(), _reply->rawHeaderPairs()}; + } + + void NetworkRequestHelper::AsyncHttpGet(const QString &url, std::function funcPtr) { + QNetworkRequest request; + request.setUrl(url); + auto accessManagerPtr = new QNetworkAccessManager(); + setAccessManagerAttributes(request, *accessManagerPtr); + auto reply = accessManagerPtr->get(request); + QObject::connect(reply, &QNetworkReply::finished, [=]() { + { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + bool h2Used = reply->attribute(QNetworkRequest::Http2WasUsedAttribute).toBool(); +#else + bool h2Used = reply->attribute(QNetworkRequest::HTTP2WasUsedAttribute).toBool(); +#endif + if (h2Used) + DEBUG("HTTP/2 was used."); + + if (reply->error() != QNetworkReply::NoError) + LOG("Network error: " + + QString(QMetaEnum::fromType().key(reply->error()))); + + funcPtr(reply->readAll()); + accessManagerPtr->deleteLater(); + } + }); + } + + QString NetworkRequestHelper::GetHeader(const QList> &header, const QString &name) { + for (auto p: header) { + if (QString(p.first).toLower() == name.toLower()) return p.second; + } + return ""; + } + +} // namespace Qv2ray::common::network diff --git a/qv2ray/utils/HTTPRequestHelper.hpp b/qv2ray/utils/HTTPRequestHelper.hpp new file mode 100644 index 0000000..41b9a75 --- /dev/null +++ b/qv2ray/utils/HTTPRequestHelper.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Qv2ray::common::network { + struct HTTPResponse { + QString error; + QByteArray data; + QList> header; + }; + + class NetworkRequestHelper : QObject { + Q_OBJECT + + explicit NetworkRequestHelper(QObject *parent) : QObject(parent) {}; + + ~NetworkRequestHelper() {}; + + public: + static void AsyncHttpGet(const QString &url, std::function funcPtr); + + static HTTPResponse HttpGet(const QUrl &url); + + static QString GetHeader(const QList>& header, const QString& name); + + private: + static void setAccessManagerAttributes(QNetworkRequest &request, QNetworkAccessManager &accessManager); + + static void setHeader(QNetworkRequest &request, const QByteArray &key, const QByteArray &value); + }; +} // namespace Qv2ray::common::network + +using namespace Qv2ray::common::network; diff --git a/qv2ray/wrapper.hpp b/qv2ray/wrapper.hpp new file mode 100644 index 0000000..15c12dc --- /dev/null +++ b/qv2ray/wrapper.hpp @@ -0,0 +1,38 @@ +#pragma once + +// Qv2ray wrapper + +#include + +#define LOG(...) Qv2ray::base::log_internal(__VA_ARGS__) +#define DEBUG(...) Qv2ray::base::log_internal(__VA_ARGS__) +namespace Qv2ray::base { + template + inline void log_internal(T... v) {} +} + +#define JsonToString(a) QJsonObject2QString(a,false) +#define JsonFromString(a) QString2QJsonObject(a) +#define QvMessageBoxWarn(a, b, c) MessageBoxWarning(b,c) + +inline QString VerifyJsonString(const QString &source) { + QJsonParseError error{}; + QJsonDocument doc = QJsonDocument::fromJson(source.toUtf8(), &error); + Q_UNUSED(doc) + + if (error.error == QJsonParseError::NoError) { + return ""; + } else { + //LOG("WARNING: Json parse returns: " + error.errorString()); + return error.errorString(); + } +} + +#define RED(obj) \ + { \ + auto _temp = obj->palette(); \ + _temp.setColor(QPalette::Text, Qt::red); \ + obj->setPalette(_temp); \ + } + +#define BLACK(obj) obj->setPalette(QWidget::palette()); diff --git a/res/icon/dialog-question.svg b/res/icon/dialog-question.svg new file mode 100644 index 0000000..7ff4bc4 --- /dev/null +++ b/res/icon/dialog-question.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/icon/internet-web-browser.svg b/res/icon/internet-web-browser.svg new file mode 100644 index 0000000..138a1a3 --- /dev/null +++ b/res/icon/internet-web-browser.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/icon/network-server.svg b/res/icon/network-server.svg new file mode 100644 index 0000000..0806722 --- /dev/null +++ b/res/icon/network-server.svg @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/res/icon/preferences.svg b/res/icon/preferences.svg new file mode 100644 index 0000000..875b939 --- /dev/null +++ b/res/icon/preferences.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/icon/system-run.svg b/res/icon/system-run.svg new file mode 100644 index 0000000..45494f4 --- /dev/null +++ b/res/icon/system-run.svg @@ -0,0 +1,190 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/icon/system-software-update.svg b/res/icon/system-software-update.svg new file mode 100644 index 0000000..e8567fb --- /dev/null +++ b/res/icon/system-software-update.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/neko.qrc b/res/neko.qrc new file mode 100644 index 0000000..aa7701b --- /dev/null +++ b/res/neko.qrc @@ -0,0 +1,16 @@ + + + icon/internet-web-browser.svg + icon/system-run.svg + icon/preferences.svg + icon/network-server.svg + icon/dialog-question.svg + icon/system-software-update.svg + + + nekoray.png + nekoray.css + ../examples/vpn-run-root.sh + ../examples/sing-box-vpn.json + + diff --git a/res/nekoray.css b/res/nekoray.css new file mode 100644 index 0000000..512e281 --- /dev/null +++ b/res/nekoray.css @@ -0,0 +1,3 @@ +QMessageBox { + messagebox-text-interaction-flags: 5; +} diff --git a/res/nekoray.ico b/res/nekoray.ico new file mode 100644 index 0000000000000000000000000000000000000000..4c355a17979278bc5ce9042174945557693210b3 GIT binary patch literal 83936 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 + + qss/flatgray.css + qss/lightblue.css + qss/flatgray/add_bottom.png + qss/flatgray/add_left.png + qss/flatgray/add_right.png + qss/flatgray/add_top.png + qss/flatgray/arrow_bottom.png + qss/flatgray/arrow_left.png + qss/flatgray/arrow_right.png + qss/flatgray/arrow_top.png + qss/flatgray/branch_close.png + qss/flatgray/branch_open.png + qss/flatgray/calendar_nextmonth.png + qss/flatgray/calendar_prevmonth.png + qss/flatgray/checkbox_checked.png + qss/flatgray/checkbox_checked_disable.png + qss/flatgray/checkbox_parcial.png + qss/flatgray/checkbox_parcial_disable.png + qss/flatgray/checkbox_unchecked.png + qss/flatgray/checkbox_unchecked_disable.png + qss/flatgray/menu_checked.png + qss/flatgray/radiobutton_checked.png + qss/flatgray/radiobutton_checked_disable.png + qss/flatgray/radiobutton_unchecked.png + qss/flatgray/radiobutton_unchecked_disable.png + qss/lightblue/add_bottom.png + qss/lightblue/add_left.png + qss/lightblue/add_right.png + qss/lightblue/add_top.png + qss/lightblue/arrow_bottom.png + qss/lightblue/arrow_left.png + qss/lightblue/arrow_right.png + qss/lightblue/arrow_top.png + qss/lightblue/branch_close.png + qss/lightblue/branch_open.png + qss/lightblue/calendar_nextmonth.png + qss/lightblue/calendar_prevmonth.png + qss/lightblue/checkbox_checked.png + qss/lightblue/checkbox_checked_disable.png + qss/lightblue/checkbox_parcial.png + qss/lightblue/checkbox_parcial_disable.png + qss/lightblue/checkbox_unchecked.png + qss/lightblue/checkbox_unchecked_disable.png + qss/lightblue/menu_checked.png + qss/lightblue/radiobutton_checked.png + qss/lightblue/radiobutton_checked_disable.png + qss/lightblue/radiobutton_unchecked.png + qss/lightblue/radiobutton_unchecked_disable.png + qss/blacksoft.css + qss/blacksoft/add_bottom.png + qss/blacksoft/add_left.png + qss/blacksoft/add_right.png + qss/blacksoft/add_top.png + qss/blacksoft/arrow_bottom.png + qss/blacksoft/arrow_left.png + qss/blacksoft/arrow_right.png + qss/blacksoft/arrow_top.png + qss/blacksoft/branch_close.png + qss/blacksoft/branch_open.png + qss/blacksoft/calendar_nextmonth.png + qss/blacksoft/calendar_prevmonth.png + qss/blacksoft/checkbox_checked.png + qss/blacksoft/checkbox_checked_disable.png + qss/blacksoft/checkbox_parcial.png + qss/blacksoft/checkbox_parcial_disable.png + qss/blacksoft/checkbox_unchecked.png + qss/blacksoft/checkbox_unchecked_disable.png + qss/blacksoft/menu_checked.png + qss/blacksoft/radiobutton_checked.png + qss/blacksoft/radiobutton_checked_disable.png + qss/blacksoft/radiobutton_unchecked.png + qss/blacksoft/radiobutton_unchecked_disable.png + + diff --git a/res/theme/feiyangqingyun/qss/blacksoft.css b/res/theme/feiyangqingyun/qss/blacksoft.css new file mode 100644 index 0000000..09a607a --- /dev/null +++ b/res/theme/feiyangqingyun/qss/blacksoft.css @@ -0,0 +1,679 @@ +QPalette{background:#444444;}*{outline:0px;color:#DCDCDC;} + +QGraphicsView{ +border:1px solid #242424; +qproperty-backgroundBrush:#444444; +} + +QWidget[form="true"],QLabel[frameShape="1"]{ +border:1px solid #242424; +border-radius:0px; +} + +QWidget[form="bottom"]{ +background:#484848; +} + +QWidget[form="bottom"] .QFrame{ +border:1px solid #DCDCDC; +} + +QWidget[form="bottom"] QLabel,QWidget[form="title"] QLabel{ +border-radius:0px; +color:#DCDCDC; +background:none; +border-style:none; +} + +QWidget[form="title"],QWidget[nav="left"],QWidget[nav="top"] QAbstractButton{ +border-style:none; +border-radius:0px; +padding:5px; +color:#DCDCDC; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +QWidget[nav="top"] QAbstractButton:hover,QWidget[nav="top"] QAbstractButton:pressed,QWidget[nav="top"] QAbstractButton:checked{ +border-style:solid; +border-width:0px 0px 2px 0px; +padding:4px 4px 2px 4px; +border-color:#AAAAAA; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); +} + +QWidget[nav="left"] QAbstractButton{ +border-radius:0px; +color:#DCDCDC; +background:none; +border-style:none; +} + +QWidget[nav="left"] QAbstractButton:hover{ +color:#FFFFFF; +background-color:#AAAAAA; +} + +QWidget[nav="left"] QAbstractButton:checked,QWidget[nav="left"] QAbstractButton:pressed{ +color:#DCDCDC; +border-style:solid; +border-width:0px 0px 0px 2px; +padding:4px 4px 4px 2px; +border-color:#AAAAAA; +background-color:#444444; +} + +QWidget[video="true"] QLabel{ +color:#DCDCDC; +border:1px solid #242424; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +QWidget[video="true"] QLabel:focus{ +border:1px solid #AAAAAA; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); +} + +QLineEdit:read-only{ +background-color:#484848; +} + +QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ +border:1px solid #242424; +border-radius:3px; +padding:2px; +background:none; +selection-background-color:#AAAAAA; +selection-color:#FFFFFF; +} + +QLineEdit:focus,QTextEdit:focus,QPlainTextEdit:focus,QSpinBox:focus,QDoubleSpinBox:focus,QComboBox:focus,QDateEdit:focus,QTimeEdit:focus,QDateTimeEdit:focus,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QSpinBox:hover,QDoubleSpinBox:hover,QComboBox:hover,QDateEdit:hover,QTimeEdit:hover,QDateTimeEdit:hover{ +border:1px solid #242424; +} + +QLineEdit[echoMode="2"]{ +lineedit-password-character:9679; +} + +.QFrame{ +border:1px solid #242424; +border-radius:3px; +} + +.QGroupBox{ +border:1px solid #242424; +border-radius:5px; +margin-top:3ex; +} + +.QGroupBox::title{ +subcontrol-origin:margin; +position:relative; +left:10px; +} + +.QPushButton,.QToolButton{ +border-style:none; +border:1px solid #242424; +color:#DCDCDC; +padding:5px; +min-height:15px; +border-radius:5px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +.QPushButton:hover,.QToolButton:hover{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); +} + +.QPushButton:pressed,.QToolButton:pressed{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +.QToolButton::menu-indicator{ +image:None; +} + +QToolButton#btnMenu,QPushButton#btnMenu_Min,QPushButton#btnMenu_Max,QPushButton#btnMenu_Close{ +border-radius:3px; +color:#DCDCDC; +padding:3px; +margin:0px; +background:none; +border-style:none; +} + +QToolButton#btnMenu:hover,QPushButton#btnMenu_Min:hover,QPushButton#btnMenu_Max:hover{ +color:#FFFFFF; +margin:1px 1px 2px 1px; +background-color:rgba(51,127,209,230); +} + +QPushButton#btnMenu_Close:hover{ +color:#FFFFFF; +margin:1px 1px 2px 1px; +background-color:rgba(238,0,0,128); +} + +QRadioButton::indicator{ +width:15px; +height:15px; +} + +QRadioButton::indicator::unchecked{ +image:url(:/qss/blacksoft/radiobutton_unchecked.png); +} + +QRadioButton::indicator::unchecked:disabled{ +image:url(:/qss/blacksoft/radiobutton_unchecked_disable.png); +} + +QRadioButton::indicator::checked{ +image:url(:/qss/blacksoft/radiobutton_checked.png); +} + +QRadioButton::indicator::checked:disabled{ +image:url(:/qss/blacksoft/radiobutton_checked_disable.png); +} + +QGroupBox::indicator,QTreeView::indicator,QListView::indicator,QTableView::indicator{ +padding:0px 0px 0px 0px; +} + +QCheckBox::indicator,QGroupBox::indicator,QTreeView::indicator,QListView::indicator,QTableView::indicator{ +width:13px; +height:13px; +} + +QCheckBox::indicator:unchecked,QGroupBox::indicator:unchecked,QTreeView::indicator:unchecked,QListView::indicator:unchecked,QTableView::indicator:unchecked{ +image:url(:/qss/blacksoft/checkbox_unchecked.png); +} + +QCheckBox::indicator:unchecked:disabled,QGroupBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QListView::indicator:unchecked:disabled,QTableView::indicator:unchecked:disabled{ +image:url(:/qss/blacksoft/checkbox_unchecked_disable.png); +} + +QCheckBox::indicator:checked,QGroupBox::indicator:checked,QTreeView::indicator:checked,QListView::indicator:checked,QTableView::indicator:checked{ +image:url(:/qss/blacksoft/checkbox_checked.png); +} + +QCheckBox::indicator:checked:disabled,QGroupBox::indicator:checked:disabled,QTreeView::indicator:checked:disabled,QListView::indicator:checked:disabled,QTableView::indicator:checked:disabled{ +image:url(:/qss/blacksoft/checkbox_checked_disable.png); +} + +QCheckBox::indicator:indeterminate,QGroupBox::indicator:indeterminate,QTreeView::indicator:indeterminate,QListView::indicator:indeterminate,QTableView::indicator:indeterminate{ +image:url(:/qss/blacksoft/checkbox_parcial.png); +} + +QCheckBox::indicator:indeterminate:disabled,QGroupBox::indicator:indeterminate:disabled,QTreeView::indicator:indeterminate:disabled,QListView::indicator:indeterminate:disabled,QTableView::indicator:indeterminate:disabled{ +image:url(:/qss/blacksoft/checkbox_parcial_disable.png); +} + +QTimeEdit::up-button,QDateEdit::up-button,QDateTimeEdit::up-button,QDoubleSpinBox::up-button,QSpinBox::up-button{ +image:url(:/qss/blacksoft/add_top.png); +width:10px; +height:10px; +padding:2px 5px 0px 0px; +} + +QTimeEdit::down-button,QDateEdit::down-button,QDateTimeEdit::down-button,QDoubleSpinBox::down-button,QSpinBox::down-button{ +image:url(:/qss/blacksoft/add_bottom.png); +width:10px; +height:10px; +padding:0px 5px 2px 0px; +} + +QTimeEdit::up-button:pressed,QDateEdit::up-button:pressed,QDateTimeEdit::up-button:pressed,QDoubleSpinBox::up-button:pressed,QSpinBox::up-button:pressed{ +top:-2px; +} + +QTimeEdit::down-button:pressed,QDateEdit::down-button:pressed,QDateTimeEdit::down-button:pressed,QDoubleSpinBox::down-button:pressed,QSpinBox::down-button:pressed,QSpinBox::down-button:pressed{ +bottom:-2px; +} + +QComboBox::down-arrow,QDateEdit[calendarPopup="true"]::down-arrow,QTimeEdit[calendarPopup="true"]::down-arrow,QDateTimeEdit[calendarPopup="true"]::down-arrow{ +image:url(:/qss/blacksoft/add_bottom.png); +width:10px; +height:10px; +right:2px; +} + +QComboBox::drop-down,QDateEdit::drop-down,QTimeEdit::drop-down,QDateTimeEdit::drop-down{ +subcontrol-origin:padding; +subcontrol-position:top right; +width:15px; +border-left-width:0px; +border-left-style:solid; +border-top-right-radius:3px; +border-bottom-right-radius:3px; +border-left-color:#242424; +} + +QComboBox::drop-down:on{ +top:1px; +} + +QMenuBar::item{ +color:#DCDCDC; +background-color:#484848; +margin:0px; +padding:3px 10px; +} + +QMenu,QMenuBar,QMenu:disabled,QMenuBar:disabled{ +color:#DCDCDC; +background-color:#484848; +border:1px solid #242424; +margin:0px; +} + +QMenu::item{ +padding:3px 20px; +} + +QMenu::indicator{ +width:20px; +height:13px; +} + +QMenu::indicator::checked{ +image:url(:/qss/blacksoft/menu_checked.png); +} + +QMenu::right-arrow{ +image:url(:/qss/blacksoft/arrow_right.png); +width:13px; +height:13px; +padding:0px 3px 0px 0px; +} + +QMenu::item:selected,QMenuBar::item:selected{ +color:#DCDCDC; +border:0px solid #242424; +background:#646464; +} + +QMenu::separator{ +height:1px; +background:#242424; +} + +QProgressBar{ +min-height:10px; +background:#484848; +border-radius:5px; +text-align:center; +border:1px solid #484848; +} + +QProgressBar:chunk{ +border-radius:5px; +background-color:#242424; +} + +QSlider::groove:horizontal{ +height:8px; +border-radius:4px; +background:#484848; +} + +QSlider::add-page:horizontal{ +height:8px; +border-radius:4px; +background:#484848; +} + +QSlider::sub-page:horizontal{ +height:8px; +border-radius:4px; +background:#242424; +} + +QSlider::handle:horizontal{ +width:13px; +margin-top:-3px; +margin-bottom:-3px; +border-radius:6px; +background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #444444,stop:0.8 #242424); +} + +QSlider::groove:vertical{ +width:8px; +border-radius:4px; +background:#484848; +} + +QSlider::add-page:vertical{ +width:8px; +border-radius:4px; +background:#242424; +} + +QSlider::sub-page:vertical{ +width:8px; +border-radius:4px; +background:#484848; +} + +QSlider::handle:vertical{ +height:14px; +margin-left:-3px; +margin-right:-3px; +border-radius:6px; +background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #444444,stop:0.8 #242424); +} + +QScrollBar:horizontal{ +background:#484848; +padding:0px; +border-radius:6px; +max-height:12px; +} + +QScrollBar::handle:horizontal{ +background:#242424; +min-width:50px; +border-radius:6px; +} + +QScrollBar::handle:horizontal:hover{ +background:#AAAAAA; +} + +QScrollBar::handle:horizontal:pressed{ +background:#AAAAAA; +} + +QScrollBar::add-page:horizontal{ +background:none; +} + +QScrollBar::sub-page:horizontal{ +background:none; +} + +QScrollBar::add-line:horizontal{ +background:none; +} + +QScrollBar::sub-line:horizontal{ +background:none; +} + +QScrollBar:vertical{ +background:#484848; +padding:0px; +border-radius:6px; +max-width:12px; +} + +QScrollBar::handle:vertical{ +background:#242424; +min-height:50px; +border-radius:6px; +} + +QScrollBar::handle:vertical:hover{ +background:#AAAAAA; +} + +QScrollBar::handle:vertical:pressed{ +background:#AAAAAA; +} + +QScrollBar::add-page:vertical{ +background:none; +} + +QScrollBar::sub-page:vertical{ +background:none; +} + +QScrollBar::add-line:vertical{ +background:none; +} + +QScrollBar::sub-line:vertical{ +background:none; +} + +QScrollArea{ +border:0px; +} + +QTreeView,QListView,QTableView,QTabWidget::pane{ +border:1px solid #242424; +selection-background-color:#646464; +selection-color:#DCDCDC; +alternate-background-color:#525252; +gridline-color:#242424; +} + +QTreeView::branch:closed:has-children{ +margin:4px; +border-image:url(:/qss/blacksoft/branch_open.png); +} + +QTreeView::branch:open:has-children{ +margin:4px; +border-image:url(:/qss/blacksoft/branch_close.png); +} + +QTreeView,QListView,QTableView,QSplitter::handle,QTreeView::branch{ +background:#444444; +} + +QTableView::item:selected,QListView::item:selected,QTreeView::item:selected{ +color:#DCDCDC; +background:#383838; +} + +QTableView::item:hover,QListView::item:hover,QTreeView::item:hover,QHeaderView,QHeaderView::section,QTableCornerButton:section{ +color:#DCDCDC; +background:#525252; +} + +QTableView::item,QListView::item,QTreeView::item{ +padding:1px; +margin:0px; +border:0px; +} + +QHeaderView::section,QTableCornerButton:section{ +padding:3px; +margin:0px; +border:1px solid #242424; +border-left-width:0px; +border-right-width:1px; +border-top-width:0px; +border-bottom-width:1px; +} + +QTabBar::tab{ +border:1px solid #242424; +color:#DCDCDC; +margin:0px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); +} + +QTabBar::tab:selected{ +border-style:solid; +border-color:#AAAAAA; +background:#444444; +} + +QTabBar::tab:top,QTabBar::tab:bottom{ +padding:3px 8px 3px 8px; +} + +QTabBar::tab:left,QTabBar::tab:right{ +padding:8px 3px 8px 3px; +} + +QTabBar::tab:top:selected{ +border-width:2px 0px 0px 0px; +} + +QTabBar::tab:right:selected{ +border-width:0px 0px 0px 2px; +} + +QTabBar::tab:bottom:selected{ +border-width:0px 0px 2px 0px; +} + +QTabBar::tab:left:selected{ +border-width:0px 2px 0px 0px; +} + +QTabBar::tab:first:top:selected,QTabBar::tab:first:bottom:selected{ +border-left-width:1px; +border-left-color:#242424; +} + +QTabBar::tab:first:left:selected,QTabBar::tab:first:right:selected{ +border-top-width:1px; +border-top-color:#242424; +} + +QTabBar::tab:last:top:selected,QTabBar::tab:last:bottom:selected{ +border-right-width:1px; +border-right-color:#242424; +} + +QTabBar::tab:last:left:selected,QTabBar::tab:last:right:selected{ +border-bottom-width:1px; +border-bottom-color:#242424; +} + +QStatusBar::item{ +border:0px solid #484848; +border-radius:3px; +} + +QToolBox::tab,QGroupBox#gboxDevicePanel,QGroupBox#gboxDeviceTitle,QFrame#gboxDevicePanel,QFrame#gboxDeviceTitle{ +padding:3px; +border-radius:5px; +color:#DCDCDC; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +QToolTip{ +border:0px solid #DCDCDC; +padding:1px; +color:#DCDCDC; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +QToolBox::tab:selected{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); +} + +QPrintPreviewDialog QToolButton{ +border:0px solid #DCDCDC; +border-radius:0px; +margin:0px; +padding:3px; +background:none; +} + +QColorDialog QPushButton,QFileDialog QPushButton{ +min-width:80px; +} + +QToolButton#qt_calendar_prevmonth{ +icon-size:0px; +min-width:20px; +image:url(:/qss/blacksoft/calendar_prevmonth.png); +} + +QToolButton#qt_calendar_nextmonth{ +icon-size:0px; +min-width:20px; +image:url(:/qss/blacksoft/calendar_nextmonth.png); +} + +QToolButton#qt_calendar_prevmonth,QToolButton#qt_calendar_nextmonth,QToolButton#qt_calendar_monthbutton,QToolButton#qt_calendar_yearbutton{ +border:0px solid #DCDCDC; +border-radius:3px; +margin:3px 3px 3px 3px; +padding:3px; +background:none; +} + +QToolButton#qt_calendar_prevmonth:hover,QToolButton#qt_calendar_nextmonth:hover,QToolButton#qt_calendar_monthbutton:hover,QToolButton#qt_calendar_yearbutton:hover,QToolButton#qt_calendar_prevmonth:pressed,QToolButton#qt_calendar_nextmonth:pressed,QToolButton#qt_calendar_monthbutton:pressed,QToolButton#qt_calendar_yearbutton:pressed{ +border:1px solid #242424; +} + +QCalendarWidget QSpinBox#qt_calendar_yearedit{ +margin:2px; +} + +QCalendarWidget QToolButton::menu-indicator{ +image:None; +} + +QCalendarWidget QTableView{ +border-width:0px; +} + +QCalendarWidget QWidget#qt_calendar_navigationbar{ +border:1px solid #242424; +border-width:1px 1px 0px 1px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); +} + +QTableView[model="true"]::item{ +padding:0px; +margin:0px; +} + +QTableView QLineEdit,QTableView QComboBox,QTableView QSpinBox,QTableView QDoubleSpinBox,QTableView QDateEdit,QTableView QTimeEdit,QTableView QDateTimeEdit{ +border-width:0px; +border-radius:0px; +} + +QTableView QLineEdit:focus,QTableView QComboBox:focus,QTableView QSpinBox:focus,QTableView QDoubleSpinBox:focus,QTableView QDateEdit:focus,QTableView QTimeEdit:focus,QTableView QDateTimeEdit:focus{ +border-width:0px; +border-radius:0px; +} + +QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ +background:#444444; +} + +QTabWidget::pane:top{top:-1px;} +QTabWidget::pane:bottom{bottom:-1px;} +QTabWidget::pane:left{right:-1px;} +QTabWidget::pane:right{left:-1px;} + +QDialog,QDial,#QUIWidgetMain{ +background-color:#444444; +color:#DCDCDC; +} + +QDialogButtonBox>QPushButton{ +min-width:50px; +} + +QListView[noborder="true"],QTreeView[noborder="true"],QTabWidget[noborder="true"]::pane{ +border-width:0px; +} + +QToolBar>*,QStatusBar>*{ +margin:2px; +} + +*:disabled,QMenu::item:disabled,QTabBar:tab:disabled,QHeaderView::section:disabled{ +background:#444444; +border-color:#484848; +color:#242424; +} + +/*TextColor:#DCDCDC*/ +/*PanelColor:#444444*/ +/*BorderColor:#242424*/ +/*NormalColorStart:#484848*/ +/*NormalColorEnd:#383838*/ +/*DarkColorStart:#646464*/ +/*DarkColorEnd:#525252*/ +/*HighColor:#AAAAAA*/ \ No newline at end of file diff --git a/res/theme/feiyangqingyun/qss/blacksoft/add_bottom.png b/res/theme/feiyangqingyun/qss/blacksoft/add_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..b4a5f1494ac956b541c0d92818891026eb6d2c77 GIT binary patch literal 335 zcmV-V0kHmwP)Fyw;pPL{gHA$1n_Zjog@FP<|(=<)fG;JgC4SnC=ipV2yQP?nv$c-`P z-8ELO!(kZCMdX3=C#-~j@sx~}hm=Q5_BR^Vui zdGcL2^9{?ge6?-+Vy(Rbu1n#jX*vR@Xdo0t2I2k$LJ?#zE(i!&kx{r{AY?&CwA20{`t8J7fvDC90&G7y50yK%dKu!+oq+YJOKG8+yyzFV<%UDq^CbFkKa%=7$8 h2Arm8nx_4Oz5%Q)wpkE#fv zs|Xg>hMc4dT$5OdVB%WX2%d0BW)_A0%E0dairwtYLP|=?|Bl31B+Ifh5xE8)hheyA zn&xHKv3rg#0P;LP2X28Q;6zn7UDw_3I$qsmeBnX}H^Bb4&j(ZHa0C8=n;aLQ6%HJL zmN+m5TI0YF=z#+xpcf7dfSx#52=vCm0$>ar%ml{5!3?)gL>}t8z5_mDtecXO@)z<6 X;{S^MZkvK)00000NkvXXu0mjf8B?Pj literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/blacksoft/add_right.png b/res/theme/feiyangqingyun/qss/blacksoft/add_right.png new file mode 100644 index 0000000000000000000000000000000000000000..4c79925194c478dfaaedc732e6d87a616e3123ed GIT binary patch literal 352 zcmV-m0iXVfP)1g!BZ0 z0O<_`XQToMjz|>{oRCT&I3U$PC`7ISgm2_MnV%C_4+!u9EV)Z-0e3ORhf@~U0PbRp zn|Yr1r!1?1ML5s`i*cX>y5K+qbi;u^&=m*1KzAJY0VO!_0m^Vt4wT{`6SyqPd~lWH y0M2rTyRLg}n&usNY1{U09LGFBcjXjO4V5a0000nNXK-2( zWEL<2r!7V%0~2uCB4iRU1E*ypqk#)>S{5=2SOBLbBmV>z#A!*$Szra67L5!7E8?^$ zWau!7$X!L8wkD!SX1)%6FnPRf+j9U{p?Trf0JfpwB%tfMPXNyVzUr8MwFmGN8qPk6 un0W(WQ(E-1aU4JTzCYBwVOf@CRqPvMCWRUzRkYav0000!J5O8>CuBWpwprB2(=%x1scMnU(bOrSju9LV6gr&sAdac8EI;iaa z5EQULXF|uF+BZv8k9E%eyEE2~*CeC{<{vfC4=b1*3 eqqo#wFn{`>F!4%w&K6(@F?hQAxvXG>T zjD_{W%WL)kT8SV`tgPI^&CUdceC0xtUy*EP0xc~q|2tA+k^O#u0o;qoO9IbW*k)td3Hz?9N1=llR1eScHc<27@*0he$kaRFN4zyWB917n~y z4h(@FI4}Zw;lKdsiGz(mZyam@*1$m_uoeyqfb%?`(#$ z5Fn$05R6m+ArPqoLJ(33gaD)(2${$=fbfO9ardOh39JXi-QUd2Q}5OS?&`X}J?3x? z;I68wZQHi*#~iDHSvb%FvvHsUhTuR048uV@Fcb%|z;GPI0VOzy0m^WY50v5{{p0?6 zFv@WNb;@$nH23p7?}28TriZ@oKSZulsq}C91PgeNv|n*ItN;K207*qoM6N<$f(vYy AX8-^I literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/blacksoft/arrow_top.png b/res/theme/feiyangqingyun/qss/blacksoft/arrow_top.png new file mode 100644 index 0000000000000000000000000000000000000000..d2c71e8208394014e6e1b2c7b4f0d1f1c8052e17 GIT binary patch literal 349 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEoCO|{#S9EWz97u_dHRv%3=E9? zo-U3d5v^}8FAO?jAaLxXen$*TF;nB}RWFkjDj9Wz<~_W})O_%s^ank`x$9o@q+66W ztxWiKg-?A}_9V6DoBQvcd$(SxVOi!ZAYyBn$t!rb|8Yfl(R<%c)qB<6F7FxmE}PT} zUNknIEwnpa|Kq*r?6`QMvGvR*I2< zpn9tE{`8stg_RG@804G|M>!qGZhh#*9s6>P4iyJL{LY?aA)=M zv6>*dy!2lE-o%#&a23_2rOH*i`n4hjgJfk8eK^*=j`4hzU=RZd(&6#Iw|x0 zRfLJ}=2y{sLPJfN49GRWm~+I0a;k$!P+9TTqP^)M5p-SV1kB7aY3keBWOekxKw;DS8+HH;&^RxvtwD zY{b4`yD<^e3GWtD`8Hj}l?pFu0AB~yYT@|vv01ixOs zjns_dghX_mEDk*;!!SH47K@JncBTkj5|Ql*{dWL0B6^t=A|e4ZU%0M&pX{mrNm{Me zArW0oSn?(!d!FY#%sL}eXK)l&P>U7RVgs^BW-8=urM7^^3gv&?g1nq0A(38y5Dw`L2w{*5KoQaXe*eUHJdSLTTI=(`V-eZ# zft&#vV=eQv=3^nGvMS}S$k=SNZS~5ZvKMP z_n_xAfE)9J`w56r>d5@qegckxt@*(pfuDfE+BmbB0QZt4c{+b#3e2+Xty1dIx8ikX z0^Cj0^o~+$;odWSP(+k5<`i(&;f77E_3afK^-|md&^1L-G)3e&aM}kBJOV@Dz!NYO z4mP7AB3Z+gs2zwSgPFRaFkWSyJK! z2Vq_kZ#eMZX1wCSL!&Os^3ZTNyb83m*3Xh8d99QZ54OJ}UjZ)n9p&5Sv#S6A002ov JPDHLkV1h5pC4~S0 literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/blacksoft/calendar_prevmonth.png b/res/theme/feiyangqingyun/qss/blacksoft/calendar_prevmonth.png new file mode 100644 index 0000000000000000000000000000000000000000..46d4d625c9aba4d5b2e366139d6b98b913a57224 GIT binary patch literal 667 zcmV;M0%ZM(P)x{Q;$Pt0IEZw*;_z;eKuV=bDQNQU$oo0ela_0fd%rvKium2$5q|H( z?~<4I9`HZ^ctq6&hr?lWGMQWja1;c=QQ3O63QT_^rM#<@8VMnWLWm!696yhuXsJxi z$~qXw@fs1m25|YrV+~+aDYX%XVZy0fNe9otW%buR&wENltY(z%;0)Z!d;_rT`@S@- zU#f#~96unUx0sCs0FQ{SapqtH|KMH$xK!lh^?LmaXC4;N5O+XCk9)n|A!jb*?;r!W z8U(>dbJ}r?fQ!Zf20$A)pdio|4k!S$i38>UO>jbppIYmO00tb0>$=lUr?W4F(1n18 zovSLjH*hA<5-!V*<2WnXx|v(3rIaf~v;{jj0JyHT_On3_NJNhSTv;gTbcEKro(*!q zTCL_T)-yY2b?^g@H<}kBIiOPN4S-G9Lms-W`!yTnfMFOWN~sOlK~9NitK04FWe?6> z2csxjYBUtNniedkN&J%>6I6jV!dA zv573aAGC!u+ELp;8lOoR$Zf}QZgUS!#-}>QI=MWZPT!42Bi81Fb?nTLMD!BHkT=KU z@eR&g?1Ot4hDnknPYjT(l)CybKz?dAo4cGjO5bi3KyEvZbFbBE{o>TGhoUqhmV4l?DFyuQz5fcJjXgMc^)mC7*7_U( zQnt83|KM#L07xkf5uG6LCkF$r;+Baa2LZ0bEhD8g%zW9kmK_+w#gY}6inG>gBARxs zr2rcMubKHo?0=@SSV=vxD=E4C0RaMnhopU0ZN-0h3165T$RZ4xyfeQgQLgvB& z;VRx^=1Xg>-k!D_4v6oAJ0bhHF{Yd(TOs?p&ZeHl8zK9=Yj4slBq94A1Z>*P1d%zW z6x+00oyc4xlx_RLe~@)uALN?vgf{Bb`~>i*4@_73=%};dfcwe9?z!v0--3ufmsSLF zC4>-X0OqA79WlxP++nE4|TMq(MWVCIXa fX`XAX7Xa`F3D;<%=aqnT00000NkvXXu0mjfN$mu% literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/blacksoft/checkbox_checked_disable.png b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_checked_disable.png new file mode 100644 index 0000000000000000000000000000000000000000..f6aab4033a67e3b67955f59fa60a4a93ba51370f GIT binary patch literal 656 zcmV;B0&o3^P)Lyh@AJ0$b`h#Qb#V|9yL1#BJVgh&$vY7T z2mb*laTWYAIvu#);3C{2IGvM|2t_z>%#tRhf|Cd>H)wmtybfpKdZusEHffgoj!j

xW9Rm}Jfn8t+_?haf@JqUsgW6T=Rw=GyEFgC_))@rq#3*L6+8@86VC_k7>qwJmP7S|>-N(M_%O$0BexU}j{N7P{>2yKIDXe` zHm}T2TMP&3``|*zJZ_9xz9e%Y^SaJv{T9!J%=4~2%V8lCGVejatfv_(vWO|gJjbm> zWKkoOdC!9rNY{056gA(mZPZ!%!@(qK1*qghnyk9u`U-G956HBRCkqSqTLF7zD=d9g z!27ZlmbR5r>KPE0ElH>yMbRte`+hf$^Pbr$|Dw@Q2kpVO+unV zm{&i7P49ce7Mnl9emmMeM<1MS|M0Sj?{UlL)(;xGTvAS1#+Su@XH#IPy}v%>>$0>P zeXcI&Zf%oIkvX1{=6@jW?JC*Z%QF48PANM3znl4n{?eG09Wqz-es<1WyQS<%ywU}2 zroDuSm$9xXvAZ9MpI^0V`+;p`C(YZ=rhV36IBX!(b%J>+|9^4gYF51 zE{-7)t#8j6dL0Q6Xn0tkro8RqimekOB5!4CPwSf6TK<7;Q}_oavqNUvs+b(t>~W~z z+$gGeI*ifz+OedGs$#)=()J1099nO4zQ1v1#~b16)&-(YD;QW7Xmn*ikh{?%5&e~Q zPP@Q8mGhs41uL1hC9VIrBIIk%q|}?r1p?KZk9{lJc_;LN#iYtFJ`S!qG zZp)_yQ`y(rWIT1$=8vkpaH=^BMM$bvdqc-wiH{2p6u&vzF>}=`-GHO>)_lD6{qNjq zsgeAL3piVDEEd)GoD^f?YdoJ}OYZNP%U(8jykTf}5YjXgpO|^Q)VwrSpP`Q7$qcir U<~IXh0fU0U)78&qol`;+0Ih9}NB{r; literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/blacksoft/checkbox_unchecked.png b/res/theme/feiyangqingyun/qss/blacksoft/checkbox_unchecked.png new file mode 100644 index 0000000000000000000000000000000000000000..8a23968e063407ca349c15b5cf9d6f666d8565c4 GIT binary patch literal 572 zcmV-C0>k}@P)@Cq#?L`XJM1VIiPTNf;@u(G@2 zN)^sdun-GNI|~af#IvwC>@81Xb7-TWh(dP5CLv&-MmS9FjaN+36I3V}*0?XszEY1V^J$J5AG<){GI6+gj_lCCj1L>(!gh<~D#WOYqxd zGPxFpp-}*krs<}=Nhx)+3ON7>!_ag(okt{h@{Hu!Mx${V0MtCYXzkf=x7(kpkh2+& z$4>zqkvNM$#6%dj0*4hAbL8a73Ywl{2Bx@DB zA&%n>lJ@1i2bMSZ%4*cJEPGs~krTjt zyo9kWPi7Q0XLE)c|H_$&Rqmv0F(VqT{BzgbULd* zny20L^mKK6e0&*LWUr&|RCTu1YJD42Yr&J-Qq}oRr?Zl`9KgoL#?K^4ZkyRXFFu#1 z=~V!A&P|HQd2ew&2!beYu>)ASTz&*3{ci=X0cfdII&NlxxA|=pMcX45WNK>atC@Z9 zq8jYK5Qj<>McX2>bud3Ng8!lr%t|4cl|nEpg zC}vhV80y&ASWiU0d)es`N)2f?n{`!nUi1e*<2de`*?VtcuF+_Oqt%06uQz9AXT9hZ z?6%l|XY`GMQ>ywT48xIC(m8iUL>|%~`FRR#!!2);qTeteDwL5!uPf zt!8G|MdT8DldV)lmfP+2-Ju{r7>12BP1k@kIm!GlPb-zmLLA4x!Mnw+R_mjx&H}Fw zNo)tW@5_N#K@WA#T{g2jB65NLlqpBPusb|`kR(Z*^S=s?9)AG`9HCqlvGOqh0000< KMNUMnLSTX-T@Yjd literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/blacksoft/menu_checked.png b/res/theme/feiyangqingyun/qss/blacksoft/menu_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..4fca11f958a859debbc44b1f9601e1e8e69bcdd1 GIT binary patch literal 501 zcmV;pQ5ZA!Ewf3Uh?LK6UTL~~pl09S09k4a&`W8Z*b~>Hgzhi3$X5i9fB5MWa;L^+? zYXdsq(u}2)hf1ke02!WE2nVoXj8SPLIItiNKualK(ne5V30y3tJXT75rX35P!6=Fj zthJXwWQ;lQ_xm3;;Et41Z@)V;fpHw4SZi;9wY19^Gwk(xNu9V%U00000NkvXXu0mjfgi6*S literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_checked.png b/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..69e499fb1c3033b4fa560c5567af20dc1bda0d48 GIT binary patch literal 1421 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 literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_unchecked_disable.png b/res/theme/feiyangqingyun/qss/blacksoft/radiobutton_unchecked_disable.png new file mode 100644 index 0000000000000000000000000000000000000000..f729f172f2a2341bc246de76b7f568780635e4ba GIT binary patch literal 1365 zcmV-b1*-aqP)LYClmr|s~cAMRK z?&-tMzWB^Dv#%3<=>KJA&OP_u-<b!sHv{5egk+4aApYR*MNw;+S%E;jY)>SEMPPmZFC%G zx2i6mBd|-Ps(U=od!)O&yRVc_Nx*nK9``(N57076KOWEn`~v(@uq^?uD|}8i9S(;= zp|$Pp?Y&b5bX_-Ut=(S+&H_h)*CLU~SIJ~@D9EX?v2jsVRn-H)3qW%j$V2&j{@$QS z76HcN@df#OevgRU64a8ab^{wTnan;w%XB(XCKiir0Nw;HE8(*<5{W$S|FcEmx7KbF zkyQZ=Sm2B5>gr{gOlI#i-~enoo&MMua}98)gwOh1F1Oxqyd;8!t>gf(H~{YbNF?%X zso|We5{bn8TrT$su+CqmsvXwa+p^j0Z=<$}5fp3fGXZb_pUnjvKr)%^&*$?`5D-Zb ziHAa=M=!CBekl{Zj4@_)DwXP*E!;%8uDiloyBApGD@upM;ag9fIB^ChL{L>%2f$fX z-8$EB0I5{!2(TldC^|emJhng$KvPpwHE?%;^D)P9cFi?hK?=O5s%Lz51KdwxbPa&x zI1Q@0tf&Rm{i#&yT!jEhr_)^`@}u7_B5jF8V$m2N&`3(YlD+O1O0tbqWs`Z0| zgY{4VG{#&3)cBnGR5e{;K!D?nACaS~6_F)lfU4^HYl%p|F=nvRbk^D)RsG9nt`U*T z#{f&H*KwS`D~C3sa5$V7k&-D_Nk9>CJkOi)m0+$k3V6z2psGt9$C+Pg0MGM+7N}L# zJdDr5UWzl@h53BGsnURq>>&O?^oz*&Nm5v?7kp0hMPxQeosqmV=mZPAdImMOee zSi{IRQZ6aEpsH&!nM~kybh5XZdyFwVDILk#>Xb2NT`HCOxs*fMcayHJu5W;wf#h71 z0f&q+E6c&b6rR)0pFe-LwzhUhC=?n7Rsi8yg7pDUr__%U975G zRCNQzDbi#*3%oUzg*9Wq2{O@W^d=E`xbVo-Oz~p62xNe-Jf&1R1?!*}ZcH5>f{ X=*#LR(ps-<00000NkvXXu0mjf;(CsV literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/flatgray.css b/res/theme/feiyangqingyun/qss/flatgray.css new file mode 100644 index 0000000..883e90f --- /dev/null +++ b/res/theme/feiyangqingyun/qss/flatgray.css @@ -0,0 +1,679 @@ +QPalette{background:#FFFFFF;}*{outline:0px;color:#57595B;} + +QGraphicsView{ +border:1px solid #B6B6B6; +qproperty-backgroundBrush:#FFFFFF; +} + +QWidget[form="true"],QLabel[frameShape="1"]{ +border:1px solid #B6B6B6; +border-radius:0px; +} + +QWidget[form="bottom"]{ +background:#E4E4E4; +} + +QWidget[form="bottom"] .QFrame{ +border:1px solid #57595B; +} + +QWidget[form="bottom"] QLabel,QWidget[form="title"] QLabel{ +border-radius:0px; +color:#57595B; +background:none; +border-style:none; +} + +QWidget[form="title"],QWidget[nav="left"],QWidget[nav="top"] QAbstractButton{ +border-style:none; +border-radius:0px; +padding:5px; +color:#57595B; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +QWidget[nav="top"] QAbstractButton:hover,QWidget[nav="top"] QAbstractButton:pressed,QWidget[nav="top"] QAbstractButton:checked{ +border-style:solid; +border-width:0px 0px 2px 0px; +padding:4px 4px 2px 4px; +border-color:#575959; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F6F6F6,stop:1 #F6F6F6); +} + +QWidget[nav="left"] QAbstractButton{ +border-radius:0px; +color:#57595B; +background:none; +border-style:none; +} + +QWidget[nav="left"] QAbstractButton:hover{ +color:#FFFFFF; +background-color:#575959; +} + +QWidget[nav="left"] QAbstractButton:checked,QWidget[nav="left"] QAbstractButton:pressed{ +color:#57595B; +border-style:solid; +border-width:0px 0px 0px 2px; +padding:4px 4px 4px 2px; +border-color:#575959; +background-color:#FFFFFF; +} + +QWidget[video="true"] QLabel{ +color:#57595B; +border:1px solid #B6B6B6; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +QWidget[video="true"] QLabel:focus{ +border:1px solid #575959; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F6F6F6,stop:1 #F6F6F6); +} + +QLineEdit:read-only{ +background-color:#E4E4E4; +} + +QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ +border:1px solid #B6B6B6; +border-radius:3px; +padding:2px; +background:none; +selection-background-color:#575959; +selection-color:#FFFFFF; +} + +QLineEdit:focus,QTextEdit:focus,QPlainTextEdit:focus,QSpinBox:focus,QDoubleSpinBox:focus,QComboBox:focus,QDateEdit:focus,QTimeEdit:focus,QDateTimeEdit:focus,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QSpinBox:hover,QDoubleSpinBox:hover,QComboBox:hover,QDateEdit:hover,QTimeEdit:hover,QDateTimeEdit:hover{ +border:1px solid #B6B6B6; +} + +QLineEdit[echoMode="2"]{ +lineedit-password-character:9679; +} + +.QFrame{ +border:1px solid #B6B6B6; +border-radius:3px; +} + +.QGroupBox{ +border:1px solid #B6B6B6; +border-radius:5px; +margin-top:3ex; +} + +.QGroupBox::title{ +subcontrol-origin:margin; +position:relative; +left:10px; +} + +.QPushButton,.QToolButton{ +border-style:none; +border:1px solid #B6B6B6; +color:#57595B; +padding:5px; +min-height:15px; +border-radius:5px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +.QPushButton:hover,.QToolButton:hover{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F6F6F6,stop:1 #F6F6F6); +} + +.QPushButton:pressed,.QToolButton:pressed{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +.QToolButton::menu-indicator{ +image:None; +} + +QToolButton#btnMenu,QPushButton#btnMenu_Min,QPushButton#btnMenu_Max,QPushButton#btnMenu_Close{ +border-radius:3px; +color:#57595B; +padding:3px; +margin:0px; +background:none; +border-style:none; +} + +QToolButton#btnMenu:hover,QPushButton#btnMenu_Min:hover,QPushButton#btnMenu_Max:hover{ +color:#FFFFFF; +margin:1px 1px 2px 1px; +background-color:rgba(51,127,209,230); +} + +QPushButton#btnMenu_Close:hover{ +color:#FFFFFF; +margin:1px 1px 2px 1px; +background-color:rgba(238,0,0,128); +} + +QRadioButton::indicator{ +width:15px; +height:15px; +} + +QRadioButton::indicator::unchecked{ +image:url(:/qss/flatgray/radiobutton_unchecked.png); +} + +QRadioButton::indicator::unchecked:disabled{ +image:url(:/qss/flatgray/radiobutton_unchecked_disable.png); +} + +QRadioButton::indicator::checked{ +image:url(:/qss/flatgray/radiobutton_checked.png); +} + +QRadioButton::indicator::checked:disabled{ +image:url(:/qss/flatgray/radiobutton_checked_disable.png); +} + +QGroupBox::indicator,QTreeView::indicator,QListView::indicator,QTableView::indicator{ +padding:0px 0px 0px 0px; +} + +QCheckBox::indicator,QGroupBox::indicator,QTreeView::indicator,QListView::indicator,QTableView::indicator{ +width:13px; +height:13px; +} + +QCheckBox::indicator:unchecked,QGroupBox::indicator:unchecked,QTreeView::indicator:unchecked,QListView::indicator:unchecked,QTableView::indicator:unchecked{ +image:url(:/qss/flatgray/checkbox_unchecked.png); +} + +QCheckBox::indicator:unchecked:disabled,QGroupBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QListView::indicator:unchecked:disabled,QTableView::indicator:unchecked:disabled{ +image:url(:/qss/flatgray/checkbox_unchecked_disable.png); +} + +QCheckBox::indicator:checked,QGroupBox::indicator:checked,QTreeView::indicator:checked,QListView::indicator:checked,QTableView::indicator:checked{ +image:url(:/qss/flatgray/checkbox_checked.png); +} + +QCheckBox::indicator:checked:disabled,QGroupBox::indicator:checked:disabled,QTreeView::indicator:checked:disabled,QListView::indicator:checked:disabled,QTableView::indicator:checked:disabled{ +image:url(:/qss/flatgray/checkbox_checked_disable.png); +} + +QCheckBox::indicator:indeterminate,QGroupBox::indicator:indeterminate,QTreeView::indicator:indeterminate,QListView::indicator:indeterminate,QTableView::indicator:indeterminate{ +image:url(:/qss/flatgray/checkbox_parcial.png); +} + +QCheckBox::indicator:indeterminate:disabled,QGroupBox::indicator:indeterminate:disabled,QTreeView::indicator:indeterminate:disabled,QListView::indicator:indeterminate:disabled,QTableView::indicator:indeterminate:disabled{ +image:url(:/qss/flatgray/checkbox_parcial_disable.png); +} + +QTimeEdit::up-button,QDateEdit::up-button,QDateTimeEdit::up-button,QDoubleSpinBox::up-button,QSpinBox::up-button{ +image:url(:/qss/flatgray/add_top.png); +width:10px; +height:10px; +padding:2px 5px 0px 0px; +} + +QTimeEdit::down-button,QDateEdit::down-button,QDateTimeEdit::down-button,QDoubleSpinBox::down-button,QSpinBox::down-button{ +image:url(:/qss/flatgray/add_bottom.png); +width:10px; +height:10px; +padding:0px 5px 2px 0px; +} + +QTimeEdit::up-button:pressed,QDateEdit::up-button:pressed,QDateTimeEdit::up-button:pressed,QDoubleSpinBox::up-button:pressed,QSpinBox::up-button:pressed{ +top:-2px; +} + +QTimeEdit::down-button:pressed,QDateEdit::down-button:pressed,QDateTimeEdit::down-button:pressed,QDoubleSpinBox::down-button:pressed,QSpinBox::down-button:pressed,QSpinBox::down-button:pressed{ +bottom:-2px; +} + +QComboBox::down-arrow,QDateEdit[calendarPopup="true"]::down-arrow,QTimeEdit[calendarPopup="true"]::down-arrow,QDateTimeEdit[calendarPopup="true"]::down-arrow{ +image:url(:/qss/flatgray/add_bottom.png); +width:10px; +height:10px; +right:2px; +} + +QComboBox::drop-down,QDateEdit::drop-down,QTimeEdit::drop-down,QDateTimeEdit::drop-down{ +subcontrol-origin:padding; +subcontrol-position:top right; +width:15px; +border-left-width:0px; +border-left-style:solid; +border-top-right-radius:3px; +border-bottom-right-radius:3px; +border-left-color:#B6B6B6; +} + +QComboBox::drop-down:on{ +top:1px; +} + +QMenuBar::item{ +color:#57595B; +background-color:#E4E4E4; +margin:0px; +padding:3px 10px; +} + +QMenu,QMenuBar,QMenu:disabled,QMenuBar:disabled{ +color:#57595B; +background-color:#E4E4E4; +border:1px solid #B6B6B6; +margin:0px; +} + +QMenu::item{ +padding:3px 20px; +} + +QMenu::indicator{ +width:20px; +height:13px; +} + +QMenu::indicator::checked{ +image:url(:/qss/flatgray/menu_checked.png); +} + +QMenu::right-arrow{ +image:url(:/qss/flatgray/arrow_right.png); +width:13px; +height:13px; +padding:0px 3px 0px 0px; +} + +QMenu::item:selected,QMenuBar::item:selected{ +color:#57595B; +border:0px solid #B6B6B6; +background:#F6F6F6; +} + +QMenu::separator{ +height:1px; +background:#B6B6B6; +} + +QProgressBar{ +min-height:10px; +background:#E4E4E4; +border-radius:5px; +text-align:center; +border:1px solid #E4E4E4; +} + +QProgressBar:chunk{ +border-radius:5px; +background-color:#B6B6B6; +} + +QSlider::groove:horizontal{ +height:8px; +border-radius:4px; +background:#E4E4E4; +} + +QSlider::add-page:horizontal{ +height:8px; +border-radius:4px; +background:#E4E4E4; +} + +QSlider::sub-page:horizontal{ +height:8px; +border-radius:4px; +background:#B6B6B6; +} + +QSlider::handle:horizontal{ +width:13px; +margin-top:-3px; +margin-bottom:-3px; +border-radius:6px; +background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #FFFFFF,stop:0.8 #B6B6B6); +} + +QSlider::groove:vertical{ +width:8px; +border-radius:4px; +background:#E4E4E4; +} + +QSlider::add-page:vertical{ +width:8px; +border-radius:4px; +background:#B6B6B6; +} + +QSlider::sub-page:vertical{ +width:8px; +border-radius:4px; +background:#E4E4E4; +} + +QSlider::handle:vertical{ +height:14px; +margin-left:-3px; +margin-right:-3px; +border-radius:6px; +background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #FFFFFF,stop:0.8 #B6B6B6); +} + +QScrollBar:horizontal{ +background:#E4E4E4; +padding:0px; +border-radius:6px; +max-height:12px; +} + +QScrollBar::handle:horizontal{ +background:#B6B6B6; +min-width:50px; +border-radius:6px; +} + +QScrollBar::handle:horizontal:hover{ +background:#575959; +} + +QScrollBar::handle:horizontal:pressed{ +background:#575959; +} + +QScrollBar::add-page:horizontal{ +background:none; +} + +QScrollBar::sub-page:horizontal{ +background:none; +} + +QScrollBar::add-line:horizontal{ +background:none; +} + +QScrollBar::sub-line:horizontal{ +background:none; +} + +QScrollBar:vertical{ +background:#E4E4E4; +padding:0px; +border-radius:6px; +max-width:12px; +} + +QScrollBar::handle:vertical{ +background:#B6B6B6; +min-height:50px; +border-radius:6px; +} + +QScrollBar::handle:vertical:hover{ +background:#575959; +} + +QScrollBar::handle:vertical:pressed{ +background:#575959; +} + +QScrollBar::add-page:vertical{ +background:none; +} + +QScrollBar::sub-page:vertical{ +background:none; +} + +QScrollBar::add-line:vertical{ +background:none; +} + +QScrollBar::sub-line:vertical{ +background:none; +} + +QScrollArea{ +border:0px; +} + +QTreeView,QListView,QTableView,QTabWidget::pane{ +border:1px solid #B6B6B6; +selection-background-color:#F6F6F6; +selection-color:#57595B; +alternate-background-color:#F6F6F6; +gridline-color:#B6B6B6; +} + +QTreeView::branch:closed:has-children{ +margin:4px; +border-image:url(:/qss/flatgray/branch_open.png); +} + +QTreeView::branch:open:has-children{ +margin:4px; +border-image:url(:/qss/flatgray/branch_close.png); +} + +QTreeView,QListView,QTableView,QSplitter::handle,QTreeView::branch{ +background:#FFFFFF; +} + +QTableView::item:selected,QListView::item:selected,QTreeView::item:selected{ +color:#57595B; +background:#E4E4E4; +} + +QTableView::item:hover,QListView::item:hover,QTreeView::item:hover,QHeaderView,QHeaderView::section,QTableCornerButton:section{ +color:#57595B; +background:#F6F6F6; +} + +QTableView::item,QListView::item,QTreeView::item{ +padding:1px; +margin:0px; +border:0px; +} + +QHeaderView::section,QTableCornerButton:section{ +padding:3px; +margin:0px; +border:1px solid #B6B6B6; +border-left-width:0px; +border-right-width:1px; +border-top-width:0px; +border-bottom-width:1px; +} + +QTabBar::tab{ +border:1px solid #B6B6B6; +color:#57595B; +margin:0px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F6F6F6,stop:1 #F6F6F6); +} + +QTabBar::tab:selected{ +border-style:solid; +border-color:#575959; +background:#FFFFFF; +} + +QTabBar::tab:top,QTabBar::tab:bottom{ +padding:3px 8px 3px 8px; +} + +QTabBar::tab:left,QTabBar::tab:right{ +padding:8px 3px 8px 3px; +} + +QTabBar::tab:top:selected{ +border-width:2px 0px 0px 0px; +} + +QTabBar::tab:right:selected{ +border-width:0px 0px 0px 2px; +} + +QTabBar::tab:bottom:selected{ +border-width:0px 0px 2px 0px; +} + +QTabBar::tab:left:selected{ +border-width:0px 2px 0px 0px; +} + +QTabBar::tab:first:top:selected,QTabBar::tab:first:bottom:selected{ +border-left-width:1px; +border-left-color:#B6B6B6; +} + +QTabBar::tab:first:left:selected,QTabBar::tab:first:right:selected{ +border-top-width:1px; +border-top-color:#B6B6B6; +} + +QTabBar::tab:last:top:selected,QTabBar::tab:last:bottom:selected{ +border-right-width:1px; +border-right-color:#B6B6B6; +} + +QTabBar::tab:last:left:selected,QTabBar::tab:last:right:selected{ +border-bottom-width:1px; +border-bottom-color:#B6B6B6; +} + +QStatusBar::item{ +border:0px solid #E4E4E4; +border-radius:3px; +} + +QToolBox::tab,QGroupBox#gboxDevicePanel,QGroupBox#gboxDeviceTitle,QFrame#gboxDevicePanel,QFrame#gboxDeviceTitle{ +padding:3px; +border-radius:5px; +color:#57595B; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +QToolTip{ +border:0px solid #57595B; +padding:1px; +color:#57595B; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +QToolBox::tab:selected{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F6F6F6,stop:1 #F6F6F6); +} + +QPrintPreviewDialog QToolButton{ +border:0px solid #57595B; +border-radius:0px; +margin:0px; +padding:3px; +background:none; +} + +QColorDialog QPushButton,QFileDialog QPushButton{ +min-width:80px; +} + +QToolButton#qt_calendar_prevmonth{ +icon-size:0px; +min-width:20px; +image:url(:/qss/flatgray/calendar_prevmonth.png); +} + +QToolButton#qt_calendar_nextmonth{ +icon-size:0px; +min-width:20px; +image:url(:/qss/flatgray/calendar_nextmonth.png); +} + +QToolButton#qt_calendar_prevmonth,QToolButton#qt_calendar_nextmonth,QToolButton#qt_calendar_monthbutton,QToolButton#qt_calendar_yearbutton{ +border:0px solid #57595B; +border-radius:3px; +margin:3px 3px 3px 3px; +padding:3px; +background:none; +} + +QToolButton#qt_calendar_prevmonth:hover,QToolButton#qt_calendar_nextmonth:hover,QToolButton#qt_calendar_monthbutton:hover,QToolButton#qt_calendar_yearbutton:hover,QToolButton#qt_calendar_prevmonth:pressed,QToolButton#qt_calendar_nextmonth:pressed,QToolButton#qt_calendar_monthbutton:pressed,QToolButton#qt_calendar_yearbutton:pressed{ +border:1px solid #B6B6B6; +} + +QCalendarWidget QSpinBox#qt_calendar_yearedit{ +margin:2px; +} + +QCalendarWidget QToolButton::menu-indicator{ +image:None; +} + +QCalendarWidget QTableView{ +border-width:0px; +} + +QCalendarWidget QWidget#qt_calendar_navigationbar{ +border:1px solid #B6B6B6; +border-width:1px 1px 0px 1px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #E4E4E4,stop:1 #E4E4E4); +} + +QTableView[model="true"]::item{ +padding:0px; +margin:0px; +} + +QTableView QLineEdit,QTableView QComboBox,QTableView QSpinBox,QTableView QDoubleSpinBox,QTableView QDateEdit,QTableView QTimeEdit,QTableView QDateTimeEdit{ +border-width:0px; +border-radius:0px; +} + +QTableView QLineEdit:focus,QTableView QComboBox:focus,QTableView QSpinBox:focus,QTableView QDoubleSpinBox:focus,QTableView QDateEdit:focus,QTableView QTimeEdit:focus,QTableView QDateTimeEdit:focus{ +border-width:0px; +border-radius:0px; +} + +QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ +background:#FFFFFF; +} + +QTabWidget::pane:top{top:-1px;} +QTabWidget::pane:bottom{bottom:-1px;} +QTabWidget::pane:left{right:-1px;} +QTabWidget::pane:right{left:-1px;} + +QDialog,QDial,#QUIWidgetMain{ +background-color:#FFFFFF; +color:#57595B; +} + +QDialogButtonBox>QPushButton{ +min-width:50px; +} + +QListView[noborder="true"],QTreeView[noborder="true"],QTabWidget[noborder="true"]::pane{ +border-width:0px; +} + +QToolBar>*,QStatusBar>*{ +margin:2px; +} + +*:disabled,QMenu::item:disabled,QTabBar:tab:disabled,QHeaderView::section:disabled{ +background:#FFFFFF; +border-color:#E4E4E4; +color:#B6B6B6; +} + +/*TextColor:#57595B*/ +/*PanelColor:#FFFFFF*/ +/*BorderColor:#B6B6B6*/ +/*NormalColorStart:#E4E4E4*/ +/*NormalColorEnd:#E4E4E4*/ +/*DarkColorStart:#F6F6F6*/ +/*DarkColorEnd:#F6F6F6*/ +/*HighColor:#575959*/ \ No newline at end of file diff --git a/res/theme/feiyangqingyun/qss/flatgray/add_bottom.png b/res/theme/feiyangqingyun/qss/flatgray/add_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..868e68710ff1bf5a9d02044f9404f9a913849805 GIT binary patch literal 336 zcmV-W0k8gvP)ly0>K-Zgo6Yi{EJM+K_U>G$SgR>0E9otY&ggS1Q>>)Zkpz~s;Z+g i<}(LQ(=<)f?w||cl8Oc-KhUxO0000&qqgOM%||O^opYIQ+gAV?0_ih734|OGyH>!hry2i34Mx zH4Y4c3LF>#RX8vJDsiw9sK&txU=AE40(0RY0oeEbag6bn3^+&tj^p?Nyb3I{Ee1AC z^BzLD23`c7A0<75&beI(;fxH(Y^xEhAxNj+z&RQgMO0!j<7!$!&7 Q{Qv*}07*qoM6N<$g3{cXX#fBK literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/flatgray/add_right.png b/res/theme/feiyangqingyun/qss/flatgray/add_right.png new file mode 100644 index 0000000000000000000000000000000000000000..be8dd1acec168ba524cbd56ec0c067db8155781a GIT binary patch literal 358 zcmV-s0h#`ZP)LS*?IqfGGF)LJ@}O~P%4%Fc9ya{j^jmDRZnJi8e`n_ zegCS0eOIL{*L8hwX4k-_nY~QYbR}~Almv`1o*j(m%d$MHkx9T`k|T40AVzusL5TDM zf(Ype1Od_;2+l|a5FC*zAUGkFKyW~+fslz@0|;Np+Zbcg;{?_N0=%19Og&l)xD6rP z9doz_a2rCn8HVBGm}50C3kOe<~2JAv^~rZn!y&ccS1mq$8WHt_`_<^yMdbf(Pl=ToXRk51b6o zKN_lk{r}VLazbMPtGcQq8*_On_|?Zm39qz$ zH=cEBcf81vUdJ?R+0V3Rj0bF|XI-4yVRgIU^F)c{K8C)Np54zX|7G3&rv984=x*KQ Z`V-SCY9*@s7Xd?p!PC{xWt~$(695EChtU84 literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/flatgray/arrow_bottom.png b/res/theme/feiyangqingyun/qss/flatgray/arrow_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..37307b75041d36df89dff1f3ef9f49e8d0722815 GIT binary patch literal 337 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEoCO|{#S9EWz97u_dHRv%3=E9S zo-U3d5v^~pS$nY>O0+#R7v$^c?H0JQnYVhQq+T}mD5xl@NtjTauz96Jh2xuj#fLZNZ`*fx-+S|( z6OqL-4T;>=b6e8%3nbdi4gBAR?4Na@BA%bg!C~3@@4*v(yRDe7CK$PM_UY~mty?b` z^gTXaRQ~*6#BScSn_T7P)q9y#BsqL12JWzLyuc#a+s)(@nj)kp#SuE;K7^ zdg8zt=#2wMpaKU@Kot%gfJz*U1gdc`0+<5_iNIVqNC4J#y=`!j0|yDfw%uwCo)wtB z3J54VE`UNd6`0cP4uUidaysw731mL?#Cw^a94M7ae>+QAuIsz2D(q$$=e;p~ zY1=myY+seM9J)u~hRE1@7WHIuEpi>+OkBU%PX?D3?NyCT1paOaWEK$g$ZQ~JkuE^c zA>DwWLAnCLAL$MRU!(*Gen=S*e2`KgggUz;PnTAfN+|i$;0_J#btU(hC@Xiu literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/flatgray/branch_close.png b/res/theme/feiyangqingyun/qss/flatgray/branch_close.png new file mode 100644 index 0000000000000000000000000000000000000000..f5b6d34625ad9470f379c703553e6021608d3cbd GIT binary patch literal 263 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEoCO|{#S9EWz97u_dHRv%K*61! zE{-7)t#8j6avd^|IQsGX(X7J4()Z64;ugjoIJ>f${R3Cty~QF67t9vTU(o(RqN{uD ztR2U$_;QNg%RlX{P~Xte(eYw_r?t=Ilocjs3~X}NTNuBE-k6uW`Atr|#&3b+g$w3I zRP`}4s2)(!dH?xVYwZiyKyior(P6zkcU)_$uBq_+2|LX6^=Rxprt-|H)&~#tPpGL* z5qT%Iqq#1GJG0c!%ixra6J<(U>M#4r$GOU mb{oU7@B5FE9Ttn_uk#6&*j+33@dYjb0000%pStyx1_=4QR zO-qq3qJM(ZT?B^?&Mq!#AqI31^j~lgM3U=bK*Yhpf-Xg>z4tg&6S=KvJolXVg1_;c z=RWz6aL&UG{LeogvC+i${iC^DZhsud>#bJnvyGnLlL_eieqMR`72pPt1J+4cYBpD& zSkZlJYL3el?jT(Mq0a$?xpKAAj@s>4Hh{|Hp<1oxDKEc*=xe+57#A&#`FedWJI@e! zd}89PKz+dT){sU(1!Efa?NMe0X$DM^*TB0WN;4yk4oRoe*(lFcpNr-)P~4-uD2VaF zF+RmnyZyFQnSLtBMI0iHfdDT%oj0zLnSkIPnHdOfkye1<4!L6%hheC?-R=pkwGlp! znbpEwU~Gu;4_fr%{k65nLjmW5#Y=*|2OKfM8D=1|_bmlMa15jYoE#Z#I|G02-Xuws zngS+9O6zBP;w&tjPj48FI!76oL|;uW{z*VU9p16>I~YI77Yz^7i+$kM)_V;bBPI2d zXsvITPER${8@*E;MbXFdbmgFk%wp+f>KAagN@q@mVHhUq%``-UASlJCbEcoBL?*S? zHyQ4t9*Elwm?^%$G^xDg5$LK799RUpz=5?FcQ-h&0Cb6i%>OL7#X$z(C~#mnNiHle zA5vc2aE${)U~zM^49pBsnh6JPXK`j67y>^P3K8&PK#3I`xH%-&aA5zMv5EsL7qwp$ pFMma#Ou~a^bM+aZE!h5!d<9TqM+;NWIKBV?002ovPDHLkV1gUqAq@Zk literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/flatgray/calendar_prevmonth.png b/res/theme/feiyangqingyun/qss/flatgray/calendar_prevmonth.png new file mode 100644 index 0000000000000000000000000000000000000000..8a17d0f4e4fe94e90b2f2078c92ded3f8604dc07 GIT binary patch literal 740 zcmVnY+iKO~GDF?%rH- zMf`4e9MALN-WT21s&*Taqt)^d zH^A+Nfp4r*TIBPegVt~94`$3U6QJjfZ~#C+Tw(5!=@FBXVH-!pPrx|P{A>DB(PCwp zK2pG7IIOyGZq82S*JOGHo`b=0?yx8pKS(2Lu2{fdhO3qrw3`fKlRrCcuEW z4F+A;6fQ|Yh@50HxsuIhU0=X}xUk|Gfx0V6L^uHGQJ_8?86WSiz3>1{^V&rMKZ*di z6Yh;~Imz0S2k4S}1Hhqh7*^-XUhvI?8Dh50IJX0JwF&jU-;6W9I-oKvYP= znnv`s_UPI2TbAXd`upZdQ4;`$(1wFmgE$OK7GD(J)n5FyxnUT(64UPjn1H{H?FxW< z46MrPRJfmt#i9j(n*t?~C0kR6Ur$eemqqSP!&9Baa351$zHsX7nK$L7rOmbj_XE<` zxj0Is?}K|($Urv>Qb>o>CkF>!l@}Mc+76UL5{}0lcd+-w(9H7k!mptHqz>*v2-~gN zcfj-*{#KlGU0s_gnO@R23qlCnbt<<4Af?jM)kgs2$Bm7ICDWtj>$mMWd3ug0b`*pG z?46U0_m#MI`R&W6KO+G)L6&S?y)>K4{gAC4_D+Qi(>$+0$uO|MReMGV5z3OcZ}S^B WE<_{l;^y+Z$@MBe0IcD17sXpNC<1319%8Si6bQ6W2HZfqN4*J!@?nK&-Xx7;}QM;u<#{ z9gbSJ9Z@wmE1C;(7*U`wi89s82IRKJ$(uCs3{!eoZt)&fNpqt z2-<53mkCV6l|)vG%LICuH)*&6WaYR_U~J;mq6cS?wcs*=GU0VH6BHpQ$U$8=0P(k; zWt0ycdZK$l5cKEy!76c(TL)z=Ryitjl6B#ruuw0DtYTs;@g`XeS=BgO@?AV1vdUR| zj%J}8vg$>^oZU=5vW_jq0_|2hvaS`%g8kq$()YJF>e}z*6zXzOxDdEo6=*g~chq&_ z;AXS1e6Bn2w*mR&T9FkI3C*bGy#*b*#>_^ce?IvB-t>--hkp#a3YV@?|0DYKYW>b` z6h&i6K6|*o-@5(t*9(OQ&KBybBQg5!d@u-;ndmQ$Rxdid=;6bnzWS&r<)JBY$s z_y;WQL{J+G8wCrS11+!A#$*VVLJ$tD#4?3od54E=l#M!Lp#c-KJDX%TYjJNoJK2Jjp>R3+}Oc12_}48yl#j9CL-0Y|D< zP1Lv6t}EaX{~r$Elv3(}0?t+~$tX}jt!hO^$5hn&_SSE0UyBkH(5I}3)12)J&E(`b%Zj=wX3}7m5l-9Z@A`hpHl@0Xb ze3c2zjWfoqIp^+78_NUyAR;$`*MKbpN5*Ew2ka$Daxo0UtvHT1fUO+J!f-xdFG-Rs ztyXIvAc~@49LKjj$U<>hz%kq?53+Dv8t@x%e-h_GmV!$It~%!qXHA+w{wNEV2CRw5 z({8tWJ}0tS77jp+F)n57lTzwhqtV!z!*{t9rs2uNB7Hwz2)T>)RJR%G;10qv?4X*uV%m9_RB(61UZ zrQUEjd?EmyPUnn>yaY~Hj2iW(Uawybf?!VpG@H%$wOZ|*h&%(53Ss-e?KqAbK@jW# Z`~s1oc)XT$aOVI3002ovPDHLkV1iM65RCu; literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/flatgray/checkbox_parcial.png b/res/theme/feiyangqingyun/qss/flatgray/checkbox_parcial.png new file mode 100644 index 0000000000000000000000000000000000000000..97376f3abaa68fd367b45cd948f157e36026d4c3 GIT binary patch literal 341 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEoCO|{#S9EWz97u_dHRv%3=E9y zo-U3d5v^};Ec9|N6gmF!z2)pXD-Ovy9Bz7VC?M~^UT>fyHSH+7LBtzvqp-ZY%o6XS zC3Kn!B4=1!C@Q?39O#%R;rB!Oocg~@=Kov#4?ld_xH6Xe%ZU>Y7CKjlO?xWjz~~nw zwZQDOEW5&n?|D}KnqL`JmR`xfDCuTie#d`XR@d6cdD&H*|8@Bs%DR@>h9}sUpGq^k zaEG(L(_uZwmo5Ut@^IFjXJa>Zc1*b?GFN*;=(`#BdESYCecXF)`Q@v#jf^6m_5D&S z`+n)RY-s4#Y3tu+*ywGWKTn^BZ?o*HwB8lIVx_TRpbDwB5ra`q&A zXPzjkekW4IrKC-G-NQfg9;nx3%u0(6eSSbovh&1Wi@x^K*m4FYfn*ydy~d?;cIwY_ zN!h<@m361#JBI5Air!r{kM+t7Z#4cF&zImE>MwfZjO|PT5se~!wKW;<4#wMLJayFO zkE*x{E${H}pHohMj7%rgAOBlEp>zqnHb!yaZuU-K{TUmsBe P1_XnrtDnm{r-UW|6gYnM^t-5em${H(@=}?G11zU@z6>@#`@Q$9 zo6h?UFQ%_I{``)om7?}SC9&TtqJ7tfI|{7Y`EkvzZ(6>V z4NeOu%(kg1iFEP4>y!BV^|x!R_rEhUUkEFFG`VM1cH{}hAN%C5?EO^RGBr=FAf+g4 z6~ozWpL_LP`SQPFqL~#M&dcpouDo2Ko5*mne|P2e)sr?q zpFWK*MR)syn0d}Bx8?*q^;KD+RwEdywew@x2LRk|(HCE}K=E?elsafWeo?ao(+cYQBmeQ|fuyC>i8$mFwzHa5=<^^lvPo?@r# zC^>K4^}Svtmqq73dcJzKec2~V_KW?QoPW7PHLtIFdi!I}3nj6b@@p%0t}Ad#$X^w9 q$Unr~<+i`g`QKAs?bca*BLC8&#k<;6*_ z(+PntIB_fb$FEjGNN$|GxO~_US1ya zj=2?n*3b1ACw4!sxNzp&5^fdUDW{)K-+LhSUgo)-?FZUZp3Etp&KR>YWEF$^rS`qw z*RQ_%DowwU;c3y&)id|{zdxJ9QI^;7KWb~#-6>nPRqso0s9(+SY%Ok-d!FH}PujKZ z)$jIv-{0{5HuLkoZz0pJye#4KvYIFPm#rW`YM~A= ziqg{jJ&znn^z4sb8+LwQip+8GM)m{yQocl9Qc_tC6kC1Y(p)*yWOE*Ch1yHj4Oe+* ze6Qo!q4d(`f3?2t&~WqS&E!e9Ci#^ye_lWH=H#nauXZYMm?>=To_+S2 z`}@BdA70tt+@^B(>?4KGtd|cNlqf4Ji|_evt~!^sZ5PjPrjj|u-?O&fx~;c=b;kRz Z?54?k9A`+BoB}2+22WQ%mvv4FO#opgI12y( literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/flatgray/menu_checked.png b/res/theme/feiyangqingyun/qss/flatgray/menu_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..6a1c7294bc0651f85199c5d7b34e6ab718675c38 GIT binary patch literal 542 zcmV+(0^$9MP){-P@(K~F!$PlGMLWUv5E~0Y2uC7k z?+XY%fiGbpvToR0Y))(hZ3KH+PY~U+(L(SIWM^z#-E|W)$xb31`86TKKba39nfwRJ zlqpjt*T`p0?5(a=T*bDAuNt-aI}8t^>fEAX0A3KxiIxLk3K$cdmFb!3+rQ0b^L=gM zz!%)Uz_+g9FW@XiAqxe@;5vy!7WNJ%;JWjOl7)J`dJfMjk}EJhPFl*9V$GzRe1Zfl32aLy=A#LM&fWGffwQ)`a#*%Ith0Fs7U0<;h zsU+h7^kiE*M?nz0u2p&Apz8^G-rBh1wvQ3Mn@-}2T6}`hyl_200MGa59h^VFsM%yZ zz8nqf)8YDnkO^rzRcai>tb_KE*>Lc|pTr(AI}Q@st-+Cl;lQ9D%#Iu!2Zjx6dZZm3 zr0A&4k@j$qvgJ=k+QmVJJxM&$J`OT&n);FX;2`6+^YY^2t};7wC|VBzr>^Mr`rcm4 gvaL*+GXF)M0T62ge$3|rApigX07*qoM6N<$f*dLJ+5i9m literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/flatgray/radiobutton_checked.png b/res/theme/feiyangqingyun/qss/flatgray/radiobutton_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..513a41e342995511d72dc2992c375750b915aa2a GIT binary patch literal 1513 zcmV zm@#Vp6^!d3{HT#UABjYM`Nsggfk2JO@J0}=1VbyR5p$XZb!0}gPvh~n+kXq7&*yuJ zN!tscrWgknG0q9f?21OCLxohI6j0Z7t+J~69cH{XEwJ%kO@cgdG!i*7c~u9k+pwX` zoi+~xsD5N{`C?ocxZSrUkw}*9xT_L$XfWA(+AadYU~uMx5xGVPybh*HC&*N?Z@sD?C19gJ z5Fwz}rn4auyek$CpL2k>$m*OTbX~ks+pyx8CNs|fRH3M{a7q2r>)n?wT^om;{}KoUJR=gv)cK8%$D-j6 ziUpe}UDvg_p6Y!7o>%x79!TC=lRwlV-~6YEO=>vmd=zj1kVquVfn?uy2CgXllvP$$ z`SVM1fPp~3LjtREl&iqP5%*nz2{w}Bd7KuczHTeT^0R9Q4(m7RCI)F|fFM^Uux56Qr zO7?x_1m`o1;FEWLO9;3=f~4J$v|F$^G`DhI!s`zxM&zNSnLenBBC+emv1b7QUaz;5 zK=qa$m^z)J(ByA^k6CO}&RZh40QNNcnp^DJ$K&yPU`km1%BRNxYTAs2st#PFXgsQH zbZDKO{UIm4<@c|*e*72<_kpna#t5l7X$(ud4j)BdS!Hr2m+D1_$c~M#Z zAmhmax`-Ai>fEy%UT-K=L4c=FKoFj_GxhiP-&B=n$efb{w9%lQp(LGtqF^`xP6u13 zISx1hGF!I6e?tNQBy-AQQ4c$SYz_l(uTUopR?ag3FbqSvK!p@$4p1FfQ1`@u`7i?@l{wcF(0%3e)+qhlh)Vs8dmKaZ^+C zb9N3+w6>;WN88_oq!2R_45S!H0sMt@!5@o;w>z!UIiBiQ0F+uji*aV`Ik)_!;FChq z#Ely)Xd8gSEi)w2eY+5g87(+gZV|{l5uzU zrM|lQx&;7PZV!k^U$V6RPIuSE%hQEp=8Z}tp)DG2|Hc*&0BTmO_(98LUIpm!2_`|n z%DVcx1@m?N%+<4JGsS@U{r>VLOPB2fuvTRs{q@{=?KoFlsXppV{uA%xMz* z@$a)|e{T1~J*HqVICCV!Z!@sYeoyA%w7bNsI=S24EHoO%DuUQE*$oy0mx*v5#6OQW z&?10e0Mk6D_8^!p$n}v(M49v*?`E0;f#s0cU+^_s(c)nj@n$|thos*ComT$*UsKgzU+6ksTN`b& P00000NkvXXu0mjfzunN5 literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/flatgray/radiobutton_checked_disable.png b/res/theme/feiyangqingyun/qss/flatgray/radiobutton_checked_disable.png new file mode 100644 index 0000000000000000000000000000000000000000..8d16af56015b8e8c3ba8c2c715a74540bebb346c GIT binary patch literal 1628 zcmV-i2BZ0jP)TcV$Bzh!aI&h;sd+_zH?BzJJ&h2}q4udvE5jsBF+q5a$%ZZA@?*x=rhvb|&jR ze?K%0?d`c~`XczjKjcHsbDrn<-RGWr-hltOWU7wYY}T4Rd-la5as$aLMPxC6Q!3`) zN$v*Fqm=6I>FN2|wr%~d1Co?Np|DtM{Um^!07UC_?FX<$L|#axQV0GOK-YCIBl#Z5 zD*&j;BpD?Qpg?j%YisK-GXj{+W@C+wjW3dX5I|^(V15k(c&4|v_oazmeNDi8KEEIo z3grPTo*}R?BH39km+x$EZ|@806A0)y&LWbX04|syj|boofNx122LKV758!eD^D)72 z5RsjcNaUvG=H_E{18&{Ab)iyfTMal3V7G`o+uz@xTeD_OIUvqlrPNxIkBi9pH8^*a zN~JbaCC35ga=FHEINT{B%L9TPAh{u(P8URkHIi0k+qP`jFf*Ffg#r z|FGlAZ&}tx5m{!4p+zJci^VQ&ZEbbyg{uJSbUOXM=XsX^*kS4;BCF%^_-cRq7{JO@ zve5vp0ld}U-+xykkto&+=2s?@IT(w@k^nw2n4kZ*pzFGqX|1;#0QYIFZ)|I8+do~n(Q;k4No(x_ znCmm`RZ6W$CXE|ym*i%Wo0U>K;_>)z zj^o@z(m0ig2uZ#H;IPl6MC6tcKLr4#Qt4~}mm1=1%VaV~O$oButku-i^g4j&05ll7 z%pv)n>$*<{L8elv{Q$l)m@X<53UjM~TI&S>8hxEkh=^+n3;^+Xd@X?WsJ&8zNNy?= z3OAUHl`3vGn9lM%@2n~y$&yLD*L|2F#h!(29O{5Q#E>? zw~ORX1HdxNvK|duPp6`6Hfx2$;gf$Qw-JXb{C1ke%yXFPfutI^ffwG6`p)xRAdQJ;uB zKWo;k*C#qe<@5P@;c(a{c^}OCMxe&AqSnpAZ6x2V5flJCl6yqta}oJ&v_WgVL_{(e zo+S7bk;!CC%j+0TYRbsx^YcQX(9;0! z#rQ4KL^>iOuhjElO&M@Rwr#7WOP5|l@(uvY0i2KVAJbm|_KC=606KbldiG59p89`H aNB;nCuyXHkR2mWh0000DIO}r0oIPN^FfLCS;6F zgeZoC3*BKCX-zqZ@uC+JER24YJqv82>ex=_&6 z2$8ij@8_Z0vb*zU|Fpa6!S8AI_wRl7&Ad1Be!zcRw9-Uf*EPT2U++M)fvFLMO#s#= z+`oZw6NDc%lEbEH{_>9j2122F;cz#Iwt}JMl*Ak-L4(eM_Jw8j-YE)TM@PpqOxh6u z_4y3o663NU=TIaPxt~jTRzO|XwW{ix_nGnLs=!i5wF&am)24YUduRsj+_$f+ByRTr zs3{m+vKUwH-kCWNjYe&+5Kq8hFjy`jA^??T9|v<_j05-+0Enmzkc|MX&mni>kMgHF zzd3d+o-^R$%q>`1;BH9J$$PO|7rbU591gF0v>>+&fw#a^l?ie#HnUe%M+vwq7%~ZH z@F;el1Romc@4uV@-lafC=rsX82%-{o1Pu*DP903Q`dn}wd%B3|Rdo^mZZ3BF?Q?^J zGx@*)!1%S1iH5e;Gn#as2dF|3vT<`u)6KEb(a99-$iEv$xs!N4UgGwKwNReD%G580Uk*Ffj*;xT30By)>RdOsgJzi8g02;{= zrTvlP2NHljqB=#)gJRy^CC$#xPOIz=*_A-rKA5I^MIlO4wn^{?WOls<{|yNMkj!%~ zkNUP3T!vvN7pRcJ1`>dRq1=cK!%%+F6jC;z1{87{NB}x(*NiEXS+o@oZBfMZg4+n& zG|fpM0ciE~+yk>~Q4mz`=E7<5olETs*YMKC<3&O=qzK%!d-t9fi@*i|sPxyo4xlvM z&Wwvo*N@-Dg3qgpu%P#f0jKM_M&JW32f=1LKIj6p`uoQj?Ap}{Xy`D)`wI=Y!C!k= zb>?A-^Oj{jZ~+1UNt{#_W9IH%hM{^^R;0y0%5`KVSTGnY-`uq2D1aR*f8yx*(bUwGOl4NcFpLHV;!Drw5ObUafA;(O^W+*Q{C7UnS3UW6YDP`i6+islmCYSS+^t zRk8vwo6R-_gTW3FSz0C8QB_@^OeVWTL~Bf1Ryv(F&CShgfe(P|{Cre(+r^6)*LnYJ zMftU3g-tl38rK4nn z54Z(B>+9=#E*6UwMhoUuDwR4BjYi|Z_dezbuqm6(-rx(Us$#8u1(@NDRMl?_g~F@V z*4kJUi^U4XV(~@%D>*M54nIG1pu7eh$608t-Q@##*jl@yy}kX&MB#?YahzsrtpiN= zm<}0Zmc`@oQ!u=OD}3NA@a|;80kpQZ?h}!(eN1y**DY^Q1F(Dd?kTF;>h)CB1Fq|C zpKLe)5m8nB6gcHE84-D0RizAAEEaD8=KJK?nM$S3)B_M8kw_c?e(^Eg+11rGy$oor zoeea3oGy!qQ!h9uS8&1% zY#|T`T&^b^fa|&^fs3BusEAw-wJ-9sSFT+7r*32=MIw;_uAUaYss$Jbg+k+Q2_{Q} zfX97oHwS~k#(Dz`3=H^fPzww|3D6jGmdY|u>+kQst=@pDIvem7qM)kRZW3$lA>f?H zsnHnITyH>QObeAut=AZHtPB_k1Wo`aeBvL=<#G-6f^!__CRKgdJFcn+B9X}HGGII& z{~P$(C&yxA%p>&zwAQu*bG_ptvMUygU0Z`C{lL|3Yp5c?o7rsEyND;O(tF?QR6gEQ zKyL6y+333NK2_c8O(Y^q!r}01gO~SYD3i&AgTdeyV4jaTU%I1*1Ex}`OTb4!(Gy@) zbz?rCe|j?DR8^Xqnl_4vU%oTOn2(3H6T`~e(9qxj-}wYafY0;!{7Vz5WICNTj^k`r z)lJ^HOWNAn+PZ(}K*eMuw44_BCKL*7XlZFVKNh%-j*eSJWGm2G1!R9_@H`4qi2Mj_@9yqC gINtBn|7#-p7ZZ^7mcSw#Pyhe`07*qoM6N<$f@6P{@&Et; literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/lightblue.css b/res/theme/feiyangqingyun/qss/lightblue.css new file mode 100644 index 0000000..590189b --- /dev/null +++ b/res/theme/feiyangqingyun/qss/lightblue.css @@ -0,0 +1,679 @@ +QPalette{background:#EAF7FF;}*{outline:0px;color:#386487;} + +QGraphicsView{ +border:1px solid #C0DCF2; +qproperty-backgroundBrush:#EAF7FF; +} + +QWidget[form="true"],QLabel[frameShape="1"],QGraphicsView{ +border:1px solid #C0DCF2; +border-radius:0px; +} + +QWidget[form="bottom"]{ +background:#DEF0FE; +} + +QWidget[form="bottom"] .QFrame{ +border:1px solid #386487; +} + +QWidget[form="bottom"] QLabel,QWidget[form="title"] QLabel{ +border-radius:0px; +color:#386487; +background:none; +border-style:none; +} + +QWidget[form="title"],QWidget[nav="left"],QWidget[nav="top"] QAbstractButton{ +border-style:none; +border-radius:0px; +padding:5px; +color:#386487; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +QWidget[nav="top"] QAbstractButton:hover,QWidget[nav="top"] QAbstractButton:pressed,QWidget[nav="top"] QAbstractButton:checked{ +border-style:solid; +border-width:0px 0px 2px 0px; +padding:4px 4px 2px 4px; +border-color:#386488; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F2F9FF,stop:1 #DAEFFF); +} + +QWidget[nav="left"] QAbstractButton{ +border-radius:0px; +color:#386487; +background:none; +border-style:none; +} + +QWidget[nav="left"] QAbstractButton:hover{ +color:#FFFFFF; +background-color:#386488; +} + +QWidget[nav="left"] QAbstractButton:checked,QWidget[nav="left"] QAbstractButton:pressed{ +color:#386487; +border-style:solid; +border-width:0px 0px 0px 2px; +padding:4px 4px 4px 2px; +border-color:#386488; +background-color:#EAF7FF; +} + +QWidget[video="true"] QLabel{ +color:#386487; +border:1px solid #C0DCF2; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +QWidget[video="true"] QLabel:focus{ +border:1px solid #386488; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F2F9FF,stop:1 #DAEFFF); +} + +QLineEdit:read-only{ +background-color:#DEF0FE; +} + +QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ +border:1px solid #C0DCF2; +border-radius:3px; +padding:2px; +background:none; +selection-background-color:#386488; +selection-color:#FFFFFF; +} + +QLineEdit:focus,QTextEdit:focus,QPlainTextEdit:focus,QSpinBox:focus,QDoubleSpinBox:focus,QComboBox:focus,QDateEdit:focus,QTimeEdit:focus,QDateTimeEdit:focus,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QSpinBox:hover,QDoubleSpinBox:hover,QComboBox:hover,QDateEdit:hover,QTimeEdit:hover,QDateTimeEdit:hover{ +border:1px solid #C0DCF2; +} + +QLineEdit[echoMode="2"]{ +lineedit-password-character:9679; +} + +.QFrame{ +border:1px solid #C0DCF2; +border-radius:3px; +} + +.QGroupBox{ +border:1px solid #C0DCF2; +border-radius:5px; +margin-top:3ex; +} + +.QGroupBox::title{ +subcontrol-origin:margin; +position:relative; +left:10px; +} + +.QPushButton,.QToolButton{ +border-style:none; +border:1px solid #C0DCF2; +color:#386487; +padding:5px; +min-height:15px; +border-radius:5px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +.QPushButton:hover,.QToolButton:hover{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F2F9FF,stop:1 #DAEFFF); +} + +.QPushButton:pressed,.QToolButton:pressed{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +.QToolButton::menu-indicator{ +image:None; +} + +QToolButton#btnMenu,QPushButton#btnMenu_Min,QPushButton#btnMenu_Max,QPushButton#btnMenu_Close{ +border-radius:3px; +color:#386487; +padding:3px; +margin:0px; +background:none; +border-style:none; +} + +QToolButton#btnMenu:hover,QPushButton#btnMenu_Min:hover,QPushButton#btnMenu_Max:hover{ +color:#FFFFFF; +margin:1px 1px 2px 1px; +background-color:rgba(51,127,209,230); +} + +QPushButton#btnMenu_Close:hover{ +color:#FFFFFF; +margin:1px 1px 2px 1px; +background-color:rgba(238,0,0,128); +} + +QRadioButton::indicator{ +width:15px; +height:15px; +} + +QRadioButton::indicator::unchecked{ +image:url(:/qss/lightblue/radiobutton_unchecked.png); +} + +QRadioButton::indicator::unchecked:disabled{ +image:url(:/qss/lightblue/radiobutton_unchecked_disable.png); +} + +QRadioButton::indicator::checked{ +image:url(:/qss/lightblue/radiobutton_checked.png); +} + +QRadioButton::indicator::checked:disabled{ +image:url(:/qss/lightblue/radiobutton_checked_disable.png); +} + +QGroupBox::indicator,QTreeView::indicator,QListView::indicator,QTableView::indicator{ +padding:0px 0px 0px 0px; +} + +QCheckBox::indicator,QGroupBox::indicator,QTreeView::indicator,QListView::indicator,QTableView::indicator{ +width:13px; +height:13px; +} + +QCheckBox::indicator:unchecked,QGroupBox::indicator:unchecked,QTreeView::indicator:unchecked,QListView::indicator:unchecked,QTableView::indicator:unchecked{ +image:url(:/qss/lightblue/checkbox_unchecked.png); +} + +QCheckBox::indicator:unchecked:disabled,QGroupBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QListView::indicator:unchecked:disabled,QTableView::indicator:unchecked:disabled{ +image:url(:/qss/lightblue/checkbox_unchecked_disable.png); +} + +QCheckBox::indicator:checked,QGroupBox::indicator:checked,QTreeView::indicator:checked,QListView::indicator:checked,QTableView::indicator:checked{ +image:url(:/qss/lightblue/checkbox_checked.png); +} + +QCheckBox::indicator:checked:disabled,QGroupBox::indicator:checked:disabled,QTreeView::indicator:checked:disabled,QListView::indicator:checked:disabled,QTableView::indicator:checked:disabled{ +image:url(:/qss/lightblue/checkbox_checked_disable.png); +} + +QCheckBox::indicator:indeterminate,QGroupBox::indicator:indeterminate,QTreeView::indicator:indeterminate,QListView::indicator:indeterminate,QTableView::indicator:indeterminate{ +image:url(:/qss/lightblue/checkbox_parcial.png); +} + +QCheckBox::indicator:indeterminate:disabled,QGroupBox::indicator:indeterminate:disabled,QTreeView::indicator:indeterminate:disabled,QListView::indicator:indeterminate:disabled,QTableView::indicator:indeterminate:disabled{ +image:url(:/qss/lightblue/checkbox_parcial_disable.png); +} + +QTimeEdit::up-button,QDateEdit::up-button,QDateTimeEdit::up-button,QDoubleSpinBox::up-button,QSpinBox::up-button{ +image:url(:/qss/lightblue/add_top.png); +width:10px; +height:10px; +padding:2px 5px 0px 0px; +} + +QTimeEdit::down-button,QDateEdit::down-button,QDateTimeEdit::down-button,QDoubleSpinBox::down-button,QSpinBox::down-button{ +image:url(:/qss/lightblue/add_bottom.png); +width:10px; +height:10px; +padding:0px 5px 2px 0px; +} + +QTimeEdit::up-button:pressed,QDateEdit::up-button:pressed,QDateTimeEdit::up-button:pressed,QDoubleSpinBox::up-button:pressed,QSpinBox::up-button:pressed{ +top:-2px; +} + +QTimeEdit::down-button:pressed,QDateEdit::down-button:pressed,QDateTimeEdit::down-button:pressed,QDoubleSpinBox::down-button:pressed,QSpinBox::down-button:pressed,QSpinBox::down-button:pressed{ +bottom:-2px; +} + +QComboBox::down-arrow,QDateEdit[calendarPopup="true"]::down-arrow,QTimeEdit[calendarPopup="true"]::down-arrow,QDateTimeEdit[calendarPopup="true"]::down-arrow{ +image:url(:/qss/lightblue/add_bottom.png); +width:10px; +height:10px; +right:2px; +} + +QComboBox::drop-down,QDateEdit::drop-down,QTimeEdit::drop-down,QDateTimeEdit::drop-down{ +subcontrol-origin:padding; +subcontrol-position:top right; +width:15px; +border-left-width:0px; +border-left-style:solid; +border-top-right-radius:3px; +border-bottom-right-radius:3px; +border-left-color:#C0DCF2; +} + +QComboBox::drop-down:on{ +top:1px; +} + +QMenuBar::item{ +color:#386487; +background-color:#DEF0FE; +margin:0px; +padding:3px 10px; +} + +QMenu,QMenuBar,QMenu:disabled,QMenuBar:disabled{ +color:#386487; +background-color:#DEF0FE; +border:1px solid #C0DCF2; +margin:0px; +} + +QMenu::item{ +padding:3px 20px; +} + +QMenu::indicator{ +width:20px; +height:13px; +} + +QMenu::indicator::checked{ +image:url(:/qss/lightblue/menu_checked.png); +} + +QMenu::right-arrow{ +image:url(:/qss/lightblue/arrow_right.png); +width:13px; +height:13px; +padding:0px 3px 0px 0px; +} + +QMenu::item:selected,QMenuBar::item:selected{ +color:#386487; +border:0px solid #C0DCF2; +background:#F2F9FF; +} + +QMenu::separator{ +height:1px; +background:#C0DCF2; +} + +QProgressBar{ +min-height:10px; +background:#DEF0FE; +border-radius:5px; +text-align:center; +border:1px solid #DEF0FE; +} + +QProgressBar:chunk{ +border-radius:5px; +background-color:#C0DCF2; +} + +QSlider::groove:horizontal{ +height:8px; +border-radius:4px; +background:#DEF0FE; +} + +QSlider::add-page:horizontal{ +height:8px; +border-radius:4px; +background:#DEF0FE; +} + +QSlider::sub-page:horizontal{ +height:8px; +border-radius:4px; +background:#C0DCF2; +} + +QSlider::handle:horizontal{ +width:13px; +margin-top:-3px; +margin-bottom:-3px; +border-radius:6px; +background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #EAF7FF,stop:0.8 #C0DCF2); +} + +QSlider::groove:vertical{ +width:8px; +border-radius:4px; +background:#DEF0FE; +} + +QSlider::add-page:vertical{ +width:8px; +border-radius:4px; +background:#C0DCF2; +} + +QSlider::sub-page:vertical{ +width:8px; +border-radius:4px; +background:#DEF0FE; +} + +QSlider::handle:vertical{ +height:14px; +margin-left:-3px; +margin-right:-3px; +border-radius:6px; +background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #EAF7FF,stop:0.8 #C0DCF2); +} + +QScrollBar:horizontal{ +background:#DEF0FE; +padding:0px; +border-radius:6px; +max-height:12px; +} + +QScrollBar::handle:horizontal{ +background:#C0DCF2; +min-width:50px; +border-radius:6px; +} + +QScrollBar::handle:horizontal:hover{ +background:#386488; +} + +QScrollBar::handle:horizontal:pressed{ +background:#386488; +} + +QScrollBar::add-page:horizontal{ +background:none; +} + +QScrollBar::sub-page:horizontal{ +background:none; +} + +QScrollBar::add-line:horizontal{ +background:none; +} + +QScrollBar::sub-line:horizontal{ +background:none; +} + +QScrollBar:vertical{ +background:#DEF0FE; +padding:0px; +border-radius:6px; +max-width:12px; +} + +QScrollBar::handle:vertical{ +background:#C0DCF2; +min-height:50px; +border-radius:6px; +} + +QScrollBar::handle:vertical:hover{ +background:#386488; +} + +QScrollBar::handle:vertical:pressed{ +background:#386488; +} + +QScrollBar::add-page:vertical{ +background:none; +} + +QScrollBar::sub-page:vertical{ +background:none; +} + +QScrollBar::add-line:vertical{ +background:none; +} + +QScrollBar::sub-line:vertical{ +background:none; +} + +QScrollArea{ +border:0px; +} + +QTreeView,QListView,QTableView,QTabWidget::pane{ +border:1px solid #C0DCF2; +selection-background-color:#F2F9FF; +selection-color:#386487; +alternate-background-color:#DAEFFF; +gridline-color:#C0DCF2; +} + +QTreeView::branch:closed:has-children{ +margin:4px; +border-image:url(:/qss/lightblue/branch_open.png); +} + +QTreeView::branch:open:has-children{ +margin:4px; +border-image:url(:/qss/lightblue/branch_close.png); +} + +QTreeView,QListView,QTableView,QSplitter::handle,QTreeView::branch{ +background:#EAF7FF; +} + +QTableView::item:selected,QListView::item:selected,QTreeView::item:selected{ +color:#386487; +background:#C0DEF6; +} + +QTableView::item:hover,QListView::item:hover,QTreeView::item:hover,QHeaderView,QHeaderView::section,QTableCornerButton:section{ +color:#386487; +background:#DAEFFF; +} + +QTableView::item,QListView::item,QTreeView::item{ +padding:1px; +margin:0px; +border:0px; +} + +QHeaderView::section,QTableCornerButton:section{ +padding:3px; +margin:0px; +border:1px solid #C0DCF2; +border-left-width:0px; +border-right-width:1px; +border-top-width:0px; +border-bottom-width:1px; +} + +QTabBar::tab{ +border:1px solid #C0DCF2; +color:#386487; +margin:0px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F2F9FF,stop:1 #DAEFFF); +} + +QTabBar::tab:selected{ +border-style:solid; +border-color:#386488; +background:#EAF7FF; +} + +QTabBar::tab:top,QTabBar::tab:bottom{ +padding:3px 8px 3px 8px; +} + +QTabBar::tab:left,QTabBar::tab:right{ +padding:8px 3px 8px 3px; +} + +QTabBar::tab:top:selected{ +border-width:2px 0px 0px 0px; +} + +QTabBar::tab:right:selected{ +border-width:0px 0px 0px 2px; +} + +QTabBar::tab:bottom:selected{ +border-width:0px 0px 2px 0px; +} + +QTabBar::tab:left:selected{ +border-width:0px 2px 0px 0px; +} + +QTabBar::tab:first:top:selected,QTabBar::tab:first:bottom:selected{ +border-left-width:1px; +border-left-color:#C0DCF2; +} + +QTabBar::tab:first:left:selected,QTabBar::tab:first:right:selected{ +border-top-width:1px; +border-top-color:#C0DCF2; +} + +QTabBar::tab:last:top:selected,QTabBar::tab:last:bottom:selected{ +border-right-width:1px; +border-right-color:#C0DCF2; +} + +QTabBar::tab:last:left:selected,QTabBar::tab:last:right:selected{ +border-bottom-width:1px; +border-bottom-color:#C0DCF2; +} + +QStatusBar::item{ +border:0px solid #DEF0FE; +border-radius:3px; +} + +QToolBox::tab,QGroupBox#gboxDevicePanel,QGroupBox#gboxDeviceTitle,QFrame#gboxDevicePanel,QFrame#gboxDeviceTitle{ +padding:3px; +border-radius:5px; +color:#386487; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +QToolTip{ +border:0px solid #386487; +padding:1px; +color:#386487; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +QToolBox::tab:selected{ +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #F2F9FF,stop:1 #DAEFFF); +} + +QPrintPreviewDialog QToolButton{ +border:0px solid #386487; +border-radius:0px; +margin:0px; +padding:3px; +background:none; +} + +QColorDialog QPushButton,QFileDialog QPushButton{ +min-width:80px; +} + +QToolButton#qt_calendar_prevmonth{ +icon-size:0px; +min-width:20px; +image:url(:/qss/lightblue/calendar_prevmonth.png); +} + +QToolButton#qt_calendar_nextmonth{ +icon-size:0px; +min-width:20px; +image:url(:/qss/lightblue/calendar_nextmonth.png); +} + +QToolButton#qt_calendar_prevmonth,QToolButton#qt_calendar_nextmonth,QToolButton#qt_calendar_monthbutton,QToolButton#qt_calendar_yearbutton{ +border:0px solid #386487; +border-radius:3px; +margin:3px 3px 3px 3px; +padding:3px; +background:none; +} + +QToolButton#qt_calendar_prevmonth:hover,QToolButton#qt_calendar_nextmonth:hover,QToolButton#qt_calendar_monthbutton:hover,QToolButton#qt_calendar_yearbutton:hover,QToolButton#qt_calendar_prevmonth:pressed,QToolButton#qt_calendar_nextmonth:pressed,QToolButton#qt_calendar_monthbutton:pressed,QToolButton#qt_calendar_yearbutton:pressed{ +border:1px solid #C0DCF2; +} + +QCalendarWidget QSpinBox#qt_calendar_yearedit{ +margin:2px; +} + +QCalendarWidget QToolButton::menu-indicator{ +image:None; +} + +QCalendarWidget QTableView{ +border-width:0px; +} + +QCalendarWidget QWidget#qt_calendar_navigationbar{ +border:1px solid #C0DCF2; +border-width:1px 1px 0px 1px; +background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #DEF0FE,stop:1 #C0DEF6); +} + +QTableView[model="true"]::item{ +padding:0px; +margin:0px; +} + +QTableView QLineEdit,QTableView QComboBox,QTableView QSpinBox,QTableView QDoubleSpinBox,QTableView QDateEdit,QTableView QTimeEdit,QTableView QDateTimeEdit{ +border-width:0px; +border-radius:0px; +} + +QTableView QLineEdit:focus,QTableView QComboBox:focus,QTableView QSpinBox:focus,QTableView QDoubleSpinBox:focus,QTableView QDateEdit:focus,QTableView QTimeEdit:focus,QTableView QDateTimeEdit:focus{ +border-width:0px; +border-radius:0px; +} + +QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ +background:#EAF7FF; +} + +QTabWidget::pane:top{top:-1px;} +QTabWidget::pane:bottom{bottom:-1px;} +QTabWidget::pane:left{right:-1px;} +QTabWidget::pane:right{left:-1px;} + +QDialog,QDial,#QUIWidgetMain{ +background-color:#EAF7FF; +color:#386487; +} + +QDialogButtonBox>QPushButton{ +min-width:50px; +} + +QListView[noborder="true"],QTreeView[noborder="true"],QTabWidget[noborder="true"]::pane{ +border-width:0px; +} + +QToolBar>*,QStatusBar>*{ +margin:2px; +} + +*:disabled,QMenu::item:disabled,QTabBar:tab:disabled,QHeaderView::section:disabled{ +background:#EAF7FF; +border-color:#DEF0FE; +color:#C0DCF2; +} + +/*TextColor:#386487*/ +/*PanelColor:#EAF7FF*/ +/*BorderColor:#C0DCF2*/ +/*NormalColorStart:#DEF0FE*/ +/*NormalColorEnd:#C0DEF6*/ +/*DarkColorStart:#F2F9FF*/ +/*DarkColorEnd:#DAEFFF*/ +/*HighColor:#386488*/ \ No newline at end of file diff --git a/res/theme/feiyangqingyun/qss/lightblue/add_bottom.png b/res/theme/feiyangqingyun/qss/lightblue/add_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..99eadb9d4be435c331abbb1e72e6849868be17ed GIT binary patch literal 348 zcmV-i0i*tjP)$5BqXFo>|ij2edYaColnk;_~qq+Wm%SGS=NrkwV1EoCjxiCabd$s zTDfdS!{@GL)ir#0TK6lsr92McR4cu9Ejxi1%jR30p`oMLDf5$sc@B@_JcdC2U8JI*U?TO6hnI9zyk=`k={7)1VT1s z5Do%>kc;SVw!4l)4& u7B70Z{rWf;^tGvv9&_L<%d#wM5BdfXK749cAL0D~0000rWvbZ5${9EpVU&w8DV`&=Lndfz~+a0rbE@CeRB98NmDJW{V^j9yrJVu2t_J z0xuSrwsZ#W4~NST;~7vJczz^n4<76do~js+a6o3CVlQN`|DXr5_f2ArRJn^Sk!sb9 z6;ibxv_Ps?)Y?dso?~*b;c0fX@ah(9*UW~O zCwq@7*s4~}^1`bd-~#;2J>A_uzn_a-zhwb24*tA)DcMAg%me;Uj;sp=F){)OLSz&W zM94@W2$0c02u3P^5QtO(Aqc4iLI6??gqp}EfB?N^;>z8#J}1x|5OZc`=DBx`01c*I zrR#NW+eZKoExEZz$|Uid-N;M@7RKP=qMC04*h)fm_hf z(B=l*AV@)6zy-n`5)?Q^exxWM_^4K)@W#9AUD3?jjy3bRSU}S>P17_@13*$H)nfng zb+#LG?@o**Wyx$@&2PWJ_s_MwjjP!ONr|z$^OCY;_L0qRAaaGx53dEhJUJ{zBEo+I zT5zT)$Y5X#&eRzh1a!oiIw8G*o;Xu5(hC@fGX)`?fgN!sZ=@4263*m>Yz0QfnVgYZ zfr)S?C!`ga7-wolHXbG^b#Hl{7zY43A$Jee^4#`8bqP3uTpL%n-6Y^6FA z%t~Ry6I5HX=HSsbR<^_S?c=@?8T|ncU|?u>T%#3ue=&L!cqnK3Q47ZUdf)(pn5D&! zeGHeSRNuA0&q2L0ozE9m5!OoKX3cm4VuFEC6zPTg6$nL;-Z(EH zWJLzyyn&Df8H@`8LJ~3x7Yu|bWHc@c2tmjsTr?28kjc0tAao+L;F5u0MP|c6$9F5V udhcYJ*Y{09Uan8acR6sHrfHh?5BdV>AE&9- z32870-U5ec2pU0O0fHbc&>a+ptYByFnLsi}ZZb2UnAy2^ASWm1f5%cRQuI&vgK=iG z9yUG~$G2Cbm*0+UDPaJ`VB8C4*I+w=&|S9I59*AU0*k@87vK`;e18krW}P`?z$#o! zBtS152!NhAa0Ytgz!9jxffG=L0|%fI2MvL095eu?z(FK16%HbRMgM#&zzGR(5CME& zdtCs}0!w9T1Me?JZ>y3c2u}*o&qVFP*>pHxmA01x8GVXVAshV%Qy?4PBxwZRNJ&rPiIjD9UPx(QT!H*Bc&syr{AFP_9nP&??pk1iad@}Ayiu>2 dlauq8@&%GBmpnMGK-2&L002ovPDHLkV1gC@lsy0d literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/lightblue/arrow_right.png b/res/theme/feiyangqingyun/qss/lightblue/arrow_right.png new file mode 100644 index 0000000000000000000000000000000000000000..604def67731f59c53bb0ca0eb531b3a676044ac0 GIT binary patch literal 364 zcmV-y0h9iTP);#Z z5F)*RAVPWqL4foIf-_P91V^L_2u?^P5FC(dAS5E20Kx`21dDo3pgAC5?+`5Zswv>S ztiG@JIcx$rFRRsH_B8s~=U5F)!hsf;i~}9e1qT|S8xH(|t~l@oy5qnPD8YdbP=!0dn!!;0000< KMNUMnLSTXnW0C6s literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/lightblue/arrow_top.png b/res/theme/feiyangqingyun/qss/lightblue/arrow_top.png new file mode 100644 index 0000000000000000000000000000000000000000..3c0223187fbcf0917c5444e4a853595ba3a7ac64 GIT binary patch literal 361 zcmV-v0ha!WP)dTM&Ib@nU&2zW6d{DTX)+kZ_4dzPK$vg2v&?>)g99WtH9d9?w53a3>V@b6$EF_h}1W+#kz7*8+de&b6N{8+a2A<%e-{Vg%WDwARqgasM zz!V%6jPwFp;;0~`Gtd!7c_W>Go;b=2X$=g-QO-y!AQO&qLZ$+lag;UkA}}KyWrZ{X zGsaP=NW;TgC6#!PF%AG?4Lz+tjXI_aXCJ?Rd@TWJndZ#J(1)gJcowjl_urMq8-VYu zRDaq<2P{m(iyz6|!?28EvCMBe7A5)pdfa3Cf0QIilK#0L-yd+r>*i6c00000NkvXX Hu0mjf*SD6p literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/lightblue/branch_close.png b/res/theme/feiyangqingyun/qss/lightblue/branch_close.png new file mode 100644 index 0000000000000000000000000000000000000000..73492b3e8fa95df8e649a7791daa5a73ec2e77d2 GIT binary patch literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEoCO|{#S9EWz97u_dHRv%K*9Z< zE{-7)t#8j6avd^|IQnsV(2CeyRc!2qWtX`tSk1-x6}p@c9JS_9%Ij-l7r1g%*rJ-L zn@=~%j(xhxE>Wk}@3%h*osd^hQd0VreoFRMb7gQ(4ugqJnFL4K*6@4x*MB*?kK>KA z`)dKWw0V#1WV!b0^t9aq=`;CjZkHV;h?H zFKUX>ytym+1z*3(d^OJ!!Pa&2^%xw|6kdN$s$o%++ph|=QAz71?}IJc8AbvwwLq^h Nc)I$ztaD0e0suy!ViN!W literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/lightblue/branch_open.png b/res/theme/feiyangqingyun/qss/lightblue/branch_open.png new file mode 100644 index 0000000000000000000000000000000000000000..9abd65c4f9fe78e0088ded3d1e50005cd913b0af GIT binary patch literal 462 zcmV;<0WtoGP)f^JY6#kzg94Yf zAfyNq8gy%o3W>Jbf~H6a%zpg^O%1Iz_)4CmF&uOAyb6t@xpg)wo+gtyTQp8^>3&_jc9 z5vo_0uaA?3=7wVjXOc+^ns+^xYj_YAD^_r=wb4ZFB^+j8&^s>|gh_YF_h--uO7WK< z!*-V(g859M$Ux3JBL`I?K6e*e9+M@zx*drGpO`S)Es=pC0;a!NQUj0$@YW@&qQNJR zhnqLRmZKs1lf+h2OH4THxPb7^tmYq%68F8%@Yfk^gAtTs1f>{3DgRqg6PMArE!ski zazN3mWQItvWPe@(ybqw>rd?#>o!f(rXUPnM!SL7l0(CxMZa7OY82|tP07*qoM6N<$ Ef?(&ut^fc4 literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/lightblue/calendar_nextmonth.png b/res/theme/feiyangqingyun/qss/lightblue/calendar_nextmonth.png new file mode 100644 index 0000000000000000000000000000000000000000..36a453bc0d77f120e646e5d0bdce44ec58981865 GIT binary patch literal 670 zcmV;P0%84$P) zwzdW2Xocsqf{g7xS?sc>t;PyyjMO7!e$(azX$6Fpm#Vyp(U=%%1)Lr|_f}L3z>-_1?p5$Oa7{*caq;0O65 zFbspX+05p&Nn71Afoi9UUIOY}Aio2)7m5ds1Xl&!V1u(vMPwP+);qK*W0vm&Emfm^ z%`cJVJ(xBhw;(iC*Hundkg+|5Vn?)Ky{J7Ti^%cl;aUQM&=@^hOTcAO#-oR2U@DMz zqP(xJsT?CPFqvDr7A=%_-K)oU7jmr+)&re5Uf?ls=O=SZAz^5u#Rx=YXV2i)K-ppY zHi2f)e&Q|ainyNulVxx3K=X>xTfjCKIB*E`fdfZCUpR08^ofInK;Jk>0IUWMEdMNY zoEU5{=nY_(PaIfYbt?gR6}HD{O@sr#cX47ISOU|A8Bv~AXmNrAOW?$nvx})fx`Agt zao~Q&#;5aDd5+$jyf%eUCuY1@5oNC+n~;2mk?owEzGB07*qoM6N<$ Ef{{%rzW@LL literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/lightblue/calendar_prevmonth.png b/res/theme/feiyangqingyun/qss/lightblue/calendar_prevmonth.png new file mode 100644 index 0000000000000000000000000000000000000000..cce673fe979082d1bc214b934535162d4e682673 GIT binary patch literal 758 zcmV6&p#fryI}bIO-V1;wxCj3dhu9ZYPFuN!0^!Mah=!$0uKVv zIN{oIsVe7FvLag3Wf!h5HhL07Ndf}DhZq47kIyXyL(%DCA+3qjWwzl!69D|H>k}~S z4|qoaE;L&$PjLg>%6uk{NOpYWt=0N1{et0GVgR5=Xp930KAl*XNDpCPIF=aTz!-py zO<(4CC#jV;B7KB_mT;QBN7<5}ypU$F%2G!C!;bOHyM1UiKS zOaPt40Zo7wao<2R$T?+1ASzR}Tz73AvaFi|TEy8E!wjtXKj;t+0J?NWw-29@gY^Le z@IWkagcI(d^SF9;erHlo{lNfa;6vcp+*#9#5QrGue>{d4XhTOqO|r%s3u!GFicWicZi#>}+HkOH5REvl&yK~P)d&9CeD^8Y zvnk-cLclQ0ZOi5f{HhMNAga>|FX!%EEC9rt0tJy7UE%$+5941%k*m}2R3|Xq+sQ?T zy7wG?`tkMiO51_!0cq-7>?G3k!QCmOrJDsoWTa~IOt`tOi zw0!+mETpwSC_3f!xwZnd55_e)<5R0Avg2~u5g^bInb8$~Y$kd6t7vVTcPg9?4kr3E o#!dssF~4Urd+UNNpS*ROpK_KsEA==<=>Px#07*qoM6N<$f}sgjAOHXW literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/lightblue/checkbox_checked.png b/res/theme/feiyangqingyun/qss/lightblue/checkbox_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..524d243aaecc59a6452ebba3455f303b12d3aa73 GIT binary patch literal 636 zcmV-?0)zdDP)l8I|3s+I}F1b1!kGqH6el*_r4d$<+-+50D- zoa8*`Igju+rE+Uc2=+l9lWe~IGU->Jk6!-(5P;Iw?z*V#ChPhiMSV@BUJN=lPl}ax zL9T-L0_mh-<56Lly%=p66I|`wH5`B_IX|7P8&sYqjhT^kleEB=d*Gr;1gOFngj0n} z+q+BOy(*mayN!K-Vx?WcQsS1ng5x-VL8s0(MTce@pabTvY`RX;e1ER|bpuI4+T zg*s{!{VvFpxImLxxTAK7gZs(C;-x-odtS00zhP^W4Lg=W6t0l#!ULDC&#I+svx}vUjlU1N;WV W+didecXk&50000Pt7)xvD@y%a0W zIR%?QDr!|vRm*Xfm4X-ie>i}QE!HyyT#Q=cn&>43oQT?C=#+|D;MFm(PY#-0giVki zUOyf;&;eeiTZ#EXCb{aMmw|7l1>*!B;Pw-bb2(`NEM}7>2Uaj3#|8Wc*Ubw#4xlTp z+hR6Z0-pDK7BIR`xI_skElEXaLC-W~pF;5gH;Vgw45v}Iq-XA|q` zUlS{6z3u}U7|sY3C^?pWb73Y`2gv25m2M>;cpwACxdMA}-8_(ix5}h53&;$4nVdlP2K1FFG}e<*UnAuniiMKI$n!63e&C0tuSyVpl_s~MmhJG zwwW`IBX4&08kdKsjfMm7jd~mYXn?Q+}v!>?HPwk1Q+dw61)SrZ%cAHxZGpV`) y$Y&-#wxx3kcm?c7NUH-6@nrzMC4B`h^Bla-Jo{9AETi0O7!{+&K=D|mz z7G35J2GSaAiyIRjg#PGrG`~AX%CCa+c{!_pU5kJ3wFGYsKZk1`uRofmuz!Evex8Bp zLdN&mi!S-!{_1*h#nMafe!R(0`ug{=Z=}d|rpQ+t-iOS1mcMzfK$mO&%|{F_E05{C zXA3b4I{s&&WRUou8!V$Qgi^Xbo=1&2&Z&-hzKt~{#ExMDlo zR)@lkEbafBUpSkt-{OAQkB8y$>36}UTQ{wB%olyOa+gT2Qc+Y~fA!)-b*4Sc)dyTY UPPS2P1%?=dr>mdKI;Vst02P>%-~a#s literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/lightblue/checkbox_parcial_disable.png b/res/theme/feiyangqingyun/qss/lightblue/checkbox_parcial_disable.png new file mode 100644 index 0000000000000000000000000000000000000000..cdd377903dd668dfa2e2e1f347396cbfc43921d2 GIT binary patch literal 344 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEoCO|{#S9EWz97u_dHRv%3=E81 zo-U3d5v^~}8R|PHiZnd@KF3?vq&E3$ zT<6>+duQLd_gk{VWAe3vw)KZAx0P47%I`I~&vAiuT8GDk=ZPBr-){B3+1(gtX2I*2 z*RXR&as3`)i#wUu=RdJKYc87CnD%D1U}R9}?^OjSzno><;^nC)64702G_9qNY4b*V zwTLS>*#CcY)Ry1!2_IpiC2}G;-si(ImYvRA!uWF zML_gUA$Cf{i-p_Bn%2Ul_iMJL86w%fqVxfh?1#Wen*W$8!duPyM*!7Be zs+srumU(vADRu-yl6;G85i=* zfvHoSjrG-cX_^emwcsKl^}%PjNn$f+v)uymvH`*Qu-Ry(f+x=hhkhw}-fuvx9DU{G zgN1g^a;W&e+uhhPCs4B)!Ovq8(b<;o#|i*qS5*h!z`0a}900U@KMtD)pedszD1Tm9DFC40OA=_M z_7&-rg}~8tEQo>R$DMeSc4?ljob&e-Emyh_Obd~2j%fWl6(seGv;eq~>)kC<@pcTxZ_ivj5HkYIOf*G_mLG9}<$*Ik?Is=GM;vc@gC;4Q-8 zxZN*SiDOqsJD<`@Ra+UWD~^Y1G_-mQ3}bKJxidGvJ?*+DM^j=L|7W|ibUT9X%|2SxB;zKUB>&9pA<$AI) zUO4k5CvWTdGrt(bcO^)kIGTNFpKtzZh09O-#NG$8TA1yviMnJkflEx6(N^~Cy$LGz ze)qc1Z`-|L+Uw~rSj!j^_L$CJ_vYf2a(0E4>CDUboQyrOB)GZoF~hC?7wKwmV_A1c zN_GjKTPb{C@2ofqP#%eCY@Ky&3|&aiiO24#vV_Z**FU*6rKD zrZbIw5?^Sj*q2YIPsJ(3O8$sqFG%NGK8>eZJnFWs;qFUOsq(K`cQrnKdcUog(ZVxr zS^I&if_Dx4-{<>0UHwhPeR;8L+dBs9=8NI-^*l{?*Ixhpxpeis5F_X5GOQ@y4< zymoBor1;CK75iMb2t5juU|*;naqi5WA7@@!p4`u7#adrBGwF@A!JFA^R?&M-&Uim{ d%imwTuN<~q3SM^cEHG&?c)I$ztaD0e0su)rDTn|7 literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/lightblue/menu_checked.png b/res/theme/feiyangqingyun/qss/lightblue/menu_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..bb52701c9794a4831b572ecc2b39e31f9f3a4600 GIT binary patch literal 554 zcmV+_0@eMAP)Mdad8m*5B$;Y zU0NUN{5*DYEvOGJ9lO*`(9IvMAc#|GwG`T+;OZdsx$98V#y&}N$#eGzlFt%yTyifi z$>n=crc9YKUL$uk@aTN4f|88@x{Q(P(dP1YQolO^jxD|#F{0lBy*EwQ2#ZJS)i?k4 zRh^|J;s8QmruWg*mv1M#XYu|W1*YJlBqBTZh%<2UTxu;-9~#T20D*s6E*wC&Ql>vT z3H`BtCnpXApN!E5(Yena9_nEB{l?25QA_GOII>iq3@~-zvxNHj;p>&}zHv&c znf=C#kMY(-;Nzv*9LN^%AiB7zW%{^Lz33z^5!j0RdlMU^OI!;u9cPDhjk^ULezH2G zjM)PoX5BOi=>rF`uQ=4El8pn90Tq~?;f4C2waOO`Vlz1LbhYoNHoL%}?I2Ap)1d@C zzHqmI0OJd_F&)S|pvP`7E^m&O{qx~kfKUi&nfy3NSqI%Ci{aoue-d}d;yB1Cx7tUx zhXadxusE`P99R~t`H^mLkfWluN4mp7&XPYH=@thC_9W>@_c$oHX=+9K!9g3ly3zSs sMU*@f!JnC_Oqu(VUwqRt6^xAucmMzZ07*qoM6N<$f|LUQX8-^I literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/lightblue/radiobutton_checked.png b/res/theme/feiyangqingyun/qss/lightblue/radiobutton_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..cd6561d2a1540cc34d244ffe6304e0c0f3a23bb2 GIT binary patch literal 1516 zcmV@VAG;_hFG6WS$qfig0YieJ30(@Pi6&yy1c^M_z&tbw` zFp5Xq{z2fMEyDLM%X2Q?vHq8T4X~;qGK&R;z<8DbcRphnY=FeERFd91d!*^Ndjc4E zCGrST6d}f}LIUK9fwM{B-tukx`rG-~j|mvmb$6n#_zhxyX(C{mV!#wcZSm;F55}@} z1PrdyeTGLn42FNQ;IhR%Uca$6rt7B4LJ3$A-ZRz1=10WvjTr`flZY;W`3?Xuk&hU2 z06drnpx^lRmbMVV8bmBFNy2rG(j=VZVEKup(7Z+T)pz?XCTh~XRRD}Fh% zEe%)`-dhIH!rVbj==Yk5*2#d&gmhVWlL6bJu$b$Ex}Hu_`XR+bwX*DyW$lk0(fgeO zE_!{R_piZ$`Ibxg06-vhqo_Q2-l@KwKGbqG5ZZYZjI4|iP@3rT1_5*q`x+6$3g$Xt z@D1;bp)Z`^sza^I|Ge{ejD^@ojIG4j%Iu0JdcCo#hKOxOGNpME?XoNq(+i`|0sw+* z^inX*v%BC z5LrkTFIDwtM0>Teaku2`B-b5Ci7vLoHX+fTs>`y2S*^qOkAHql)Re=_@X&1Gqbe^i+mBep z=SH71S63&Hy)OcoTiMte=sviyOBP_{n7+EYVf$k&Tq~J*E&ved6qrBh>S(^Af&&2K zdeLGcluF(#ZUbatouHJ+1%!aL4uBMJRPm0@=K*YRvdPu(-=bJcoXZAND>9DVjX$F9 z1Dh_$R~yZ)Zj6Kr37(IPUTvHFpNCJx+VpgV$f|=Di>S;QgF>?7_;@w)!Zv>lm>!|E zXSQV(|@4mV;Q8)&L6x~PS(G6c`HrTcxEtz;H;n4!Q z=_kMq5}N~|ofQvgQ{Gnn$Es8j-ZRx>nOhlnN^ZfIHNRn-LFsR2oqVhRr(|JYsthi*~wjbvk(YHYQeN^tQ#x_E)n4z13%?B zo&&~n0LsT0Z?a3Q=swUSE&7gkGlB4~`LL}0`ChXXDkk)^5NlL$fWv790J_?ndKk0} zgx*P~Bfh956U&_7fIRO9gSze>>h;!Jpt^}>lfi~yio`pbkLP1w&<*R|58hhhaL1yE z#DfIUk!H(;i!L))KVb}eM$1z=3K0TT~0({%wp#ez?sY2P%V`uV@ctA79ib2vt4 SDaC;R0000=tURloyR5l-*_8 z0*OF15|W}uF%qN+0a9a=hKTwFgQAIsNPIy_T8QZvR8lBdcXkV1qmU3wH=(jK+tSje z-EBLanQdox=DEiYGo1`G_nDn;m&6bLzs)`Oob$Wq-sipy{Etf(>Nq~GttVS%K5L9! zugK>?*8tt+uqQ$HtH?i9G!c)5-xU%2p#!R_BqnFp7`2b0Yy{exbj_l=Lqqnb@mTMn z4+U^ImAl5X`i!F20p?@<%+EyS0l-@zSZ&&Kpg$h! z4?j8I)HeitJ$ZV0Q9~~RYZeErF8MC&`QM1gdeaU1xHX*0tTdKgK>s{>e9&VcZy{s= zL?x=|wLp~lm%~`Gz{U-$BBz=LeC?O^N5UkJ5746Jp8u%J72Hh*<|MfdJz;Q&O$4)sU=;QOJ^3G8!^g1e{N zGk4c>S7|tvyT(|%2UuDMr}~@dT;!3U>1{mKcz!Y90OG4J%h`~BUqB^ShOGT&bwdR(F*&maVWYz->G}4Divd@GjP<6) z=oa9N!+ihVLo=Va@FP?qwf0tAQwvWFT-B3mlyK~bcI5M!K`Cu37?IS{+3=f<1~m|i zyfK=}{t;zMjj2xyb^zE3Y^xbtIrETNmSL0Ok}ps&E}?!$ZiqYE+X3% z_D!{APmE4vzNM50eB=xNI&7`c9#(^R&xW@QSVv)FN7}0E%#m`Z9O>A^+2^po3S2r+_~<$!AsM zxp+*R%cZ=7)D|MAs>iDU%>?zwPL~z^Vo+A~{Ko@tCe;$2kLJVA1;!D*g}S9CX|E-8 zxB}SH>c0=XU+ZjCzVha=W3Ek@c&s;FDDY+A<$&C!>sda(Iq1kMpSd^Jk4fM5Cp)Jq zfHz*zeOgq8>qZARWY2f43jkols>rFK{_yP>20?cMhkzvTJ8R7KL;aCW@zs~*g8XgK zzXRy2YZu)Uk4v!xTh{Qq*&!pmW+z{(oaabNAg z`YU^qP&N0FCD}K?jyYnj^;_Dv1f8b~(fGKwhSWakfCJ^_bt}3nx2Fo=7nUtMry{=u zW@}uGs&3nx%6x4x;8azn+jHAcT=Ax@XMR=vocQ3^^Imwj!k-+DZK~$CqZ8ThUP>iP zkEh&Ys``Vv+@&5*$74(OSGUw9o9D@C6}A_p`0y?LA31w5aDPvo`IKk9r-9pCa6wN` zXElZ=GHVUI2=oTzORMk%`TSFjzEXh1!OT)Z`Xf>E?SOkDK+?1JR@covaJoj4XYWDH z?;8XK@KFwk@Heaa?+*kvay`nOl(vyZXZ{%rcdlCzac=qz*VAb7?3Yw!2f>xRR2?-& z?;7mu{#Q_khX0!k^>@EyRBi&s7n@9ApYMlmXara8H^0V?Xh&f#w^fy`^Gta&N{jHl z!M^ab&HU?nQCmjhU}mX!@)+p7)L)V2)07IoXy(IOFyIBLsu@XTZ#1f3S7ZPr=BVsh rkfaEI6_FPPVoMGz^ndFA>r(Vzu8V{&QEuFw00000NkvXXu0mjf4Xif@ literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/lightblue/radiobutton_unchecked.png b/res/theme/feiyangqingyun/qss/lightblue/radiobutton_unchecked.png new file mode 100644 index 0000000000000000000000000000000000000000..8f0b4fc9e3d974e8c133507dc8cc51a8c98cc789 GIT binary patch literal 1298 zcmV+t1?~EYP)4txAOlS+MiYUE!3eDyK+^?; zd7U@!bHNm*@7*^uZD))NztzjR=brnU^X8s+?*abfl8H_@1L^Lz-~Khgg=%A-R}v5a zP9N&H48c*~dO&?v8Dn_O|x?em(1n6`M$yVHO?prZ4TZf2m9g?a3ad+EVPX&#zW_N z24hVvvjw{Dfta%%f9|ir@4vn{SvW!x0mDbqU2l9-Xz(3Sjcn$9GW?#9IQ@UCzdu zFkqn;C3ZYUhL%vc0K{}3u<#ZsR~2}QiYL!>Zk<&9{9hB*KPsZqJCh#AfdBvi07*qo IM6N<$f{No|vH$=8 literal 0 HcmV?d00001 diff --git a/res/theme/feiyangqingyun/qss/lightblue/radiobutton_unchecked_disable.png b/res/theme/feiyangqingyun/qss/lightblue/radiobutton_unchecked_disable.png new file mode 100644 index 0000000000000000000000000000000000000000..57c0eb3d1afd574191d8ebe5020d6ba1d6357cb2 GIT binary patch literal 1385 zcmV-v1(y1WP)M6cZz`1a#{_U_>^c>pB=~ z@BRCsTP=N_c4KQ3KluOFd(J)Q*K_Z4?{gmDKQ0;T#I`n;o-66UT`9AWkb3~u0q_ll zJqPeGMSi9*913{;6cJN&K#~&f?5|TaFM(JAplp;=`#^4|%e)Z^_)ioCusIT+Z5e({ z@Gb!AT9RbQ0NA6IUb8UhKUomK)>ACMG8lVPD9-_~#t4?3_7cF?8mc^7Mq2v3fCoA* zOi$9a6F^;Iz--BP8B0GN3izXWe4N!BiPb2BT>z>^$fE&v0rDFJF#r%sCBZuZRAS`q z(3(tO#iE(s^P>j*@sDUwE44QdoPqE#jC#Ftu;T05S{ZbVb294JN$X*NFM;z^qWUZ}({706;`cLzVXvtzCBraM1Z*U{#Mhw#v?(q2@?@ zwlby(z+@X7`9bBN_k~=`Ia-C=&Q+*V_cjVvI+%|qm1Fb063F9Sy3MRwX17FvEYX8+y&X=0v^$w6S~uYb8qoo5K42l zBY`=6mn#~J1!$6b8GxW|T-X!}$-jFL05tGbs@u9sWkPH?aI8Cf6OUU>k@yV&*28|1 zoHd3!hkvAv-idvO0MqIaYo0D~uN%I+3!%0)mbz%R0I0DUiSHV#ed)KSG~m5cr(UAS zI{>a^O;IE_wnSo&76Oi>^tj_2K{&AqbI-Y4`#O3b z0o6x&f&zdBaa07~8}j$70xP*2#B!vLk$iXlMFT8vtoAy#qj~q!{*K-UNZF3ul^m~5 zE5()bgT7yKIpqCr(op65R#EN;&{k+N0S;>Inv)N1$Zuq|o?>}oFusArh7p#0G>VE~ z{rsS3+i1SJUel2gJ|3GamTUsJ8unMDk}OABcvXXfJ0?_ r0qGFIHzKm5J}~j +#include + +#ifndef NKR_NO_GRPC + +#include "main/NekoRay.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace QtGrpc { + const char *GrpcAcceptEncodingHeader = "grpc-accept-encoding"; + const char *AcceptEncodingHeader = "accept-encoding"; + const char *TEHeader = "te"; + const char *GrpcStatusHeader = "grpc-status"; + const char *GrpcStatusMessage = "grpc-message"; + const int GrpcMessageSizeHeaderSize = 5; + + class Http2GrpcChannelPrivate : public QObject { + private: + QString url_base; + QThread *thread; + QNetworkAccessManager *nm; + + QString nekoray_auth; + QString serviceName; + + // TODO WTF + // https://github.com/semlanik/qtprotobuf/issues/116 +// setCachingEnabled: 5 bytesDownloaded +// QNetworkReplyImpl: backend error: caching was enabled after some bytes had been written + + // async + QNetworkReply *post(const QString &method, const QString &service, const QByteArray &args) { + QUrl callUrl = url_base + "/" + service + "/" + method; +// qDebug() << "Service call url: " << callUrl; + + QNetworkRequest request(callUrl); + request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String{"application/grpc"}); + request.setRawHeader("Cache-Control", "no-store"); + request.setRawHeader(GrpcAcceptEncodingHeader, QByteArray{"identity,deflate,gzip"}); + request.setRawHeader(AcceptEncodingHeader, QByteArray{"identity,gzip"}); + request.setRawHeader(TEHeader, QByteArray{"trailers"}); + request.setAttribute(QNetworkRequest::Http2DirectAttribute, true); + request.setRawHeader("nekoray_auth", nekoray_auth.toLatin1()); + + QByteArray msg(GrpcMessageSizeHeaderSize, '\0'); + *reinterpret_cast(msg.data() + 1) = qToBigEndian((int) args.size()); + msg += args; + // qDebug() << "SEND: " << msg.size(); + + QNetworkReply *networkReply = nm->post(request, msg); + return networkReply; + } + + static QByteArray processReply(QNetworkReply *networkReply, QNetworkReply::NetworkError &statusCode) { + // Check if no network error occured + if (networkReply->error() != QNetworkReply::NoError) { + statusCode = networkReply->error(); + return {}; + } + + // Check if server answer with error + auto errCode = networkReply->rawHeader(GrpcStatusHeader).toInt(); + if (errCode != 0) { + QStringList errstr; + errstr << "grpc-status error code:" << Int2String(errCode) << ", error msg:" + << QLatin1String(networkReply->rawHeader(GrpcStatusMessage)); + showLog(errstr.join(" ")); + statusCode = QNetworkReply::NetworkError::ProtocolUnknownError; + return {}; + } + statusCode = QNetworkReply::NetworkError::NoError; + return networkReply->readAll().mid(GrpcMessageSizeHeaderSize); + } + + QNetworkReply::NetworkError + call(const QString &method, const QString &service, const QByteArray &args, QByteArray &qByteArray, + int timeout_ms) { + QNetworkReply *networkReply = post(method, service, args); + + QTimer *abortTimer = nullptr; + if (timeout_ms > 0) { + abortTimer = new QTimer; + abortTimer->setSingleShot(true); + abortTimer->setInterval(timeout_ms); + connect(abortTimer, &QTimer::timeout, abortTimer, [=]() { + networkReply->abort(); + }); + abortTimer->start(); + } + + { + QEventLoop loop; + QObject::connect(networkReply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + } + + if (abortTimer != nullptr) { + abortTimer->stop(); + abortTimer->deleteLater(); + } + + auto grpcStatus = QNetworkReply::NetworkError::ProtocolUnknownError; + qByteArray = processReply(networkReply, grpcStatus); + // qDebug() << __func__ << "RECV: " << qByteArray.toHex() << "grpcStatus" << grpcStatus; + + networkReply->deleteLater(); + return grpcStatus; + } + + public: + Http2GrpcChannelPrivate(const QString &url_, const QString &nekoray_auth_, const QString &serviceName_) { + url_base = "http://" + url_; + nekoray_auth = nekoray_auth_; + serviceName = serviceName_; + // + thread = new QThread; + nm = new QNetworkAccessManager(); + nm->moveToThread(thread); + thread->start(); + } + + QNetworkReply::NetworkError Call(const QString &methodName, + const google::protobuf::Message &req, google::protobuf::Message *rsp, + int timeout_ms = 0) { + std::string reqStr; + req.SerializeToString(&reqStr); + auto requestArray = QByteArray::fromStdString(reqStr); + + QByteArray responseArray; + QNetworkReply::NetworkError err; + QMutex lock; + lock.lock(); + + runOnUiThread([&] { + err = call(methodName, serviceName, requestArray, responseArray, timeout_ms); + lock.unlock(); + }, nm); + + lock.lock(); + lock.unlock(); +// qDebug() << "rsp err" << err; +// qDebug() << "rsp array" << responseArray; + + if (err != QNetworkReply::NetworkError::NoError) { + return err; + } + if (!rsp->ParseFromArray(responseArray.data(), responseArray.size())) { + return QNetworkReply::NetworkError(-114514); + } + return QNetworkReply::NetworkError::NoError; + } + + }; +} + +namespace NekoRay::rpc { + Client::Client(std::function onError, const QString &target, const QString &token) { + this->grpc_channel = std::make_unique(target, token, "libcore.LibcoreService"); + this->onError = std::move(onError); + } + +#define NOT_OK *rpcOK = false; \ + onError( \ + QString("QNetworkReply::NetworkError code: %1\n").arg(status) \ + ); + + void Client::Exit() { + libcore::EmptyReq request; + libcore::EmptyResp reply; + grpc_channel->Call("Exit", request, &reply, 500); + } + + QString Client::Start(bool *rpcOK, const libcore::LoadConfigReq &request) { + libcore::ErrorResp reply; + auto status = grpc_channel->Call("Start", request, &reply); + + if (status == QNetworkReply::NoError) { + *rpcOK = true; + return {reply.error().c_str()}; + } else { + NOT_OK + return ""; + } + } + + QString Client::SetTun(bool *rpcOK, const libcore::SetTunReq &request) { + libcore::ErrorResp reply; + auto status = grpc_channel->Call("SetTun", request, &reply); + + if (status == QNetworkReply::NoError) { + *rpcOK = true; + return {reply.error().c_str()}; + } else { + NOT_OK + return ""; + } + } + + QString Client::Stop(bool *rpcOK) { + libcore::EmptyReq request; + libcore::ErrorResp reply; + auto status = grpc_channel->Call("Stop", request, &reply); + + if (status == QNetworkReply::NoError) { + *rpcOK = true; + return {reply.error().c_str()}; + } else { + NOT_OK + return ""; + } + } + + bool Client::KeepAlive() { + libcore::EmptyReq request; + libcore::EmptyResp reply; + auto status = grpc_channel->Call("KeepAlive", request, &reply, 500); + + if (status == QNetworkReply::NoError) { + return true; + } else { + return false; + } + } + + long long Client::QueryStats(const std::string &tag, const std::string &direct) { + libcore::QueryStatsReq request; + request.set_tag(tag); + request.set_direct(direct); + + libcore::QueryStatsResp reply; + auto status = grpc_channel->Call("QueryStats", request, &reply, 500); + + if (status == QNetworkReply::NoError) { + return reply.traffic(); + } else { + return 0; + } + } + + std::string Client::ListV2rayConnections() { + libcore::EmptyReq request; + libcore::ListV2rayConnectionsResp reply; + auto status = grpc_channel->Call("ListV2rayConnections", request, &reply, 500); + + if (status == QNetworkReply::NoError) { + return reply.matsuri_connections_json(); + } else { + return ""; + } + } + + // + + libcore::TestResp Client::Test(bool *rpcOK, const libcore::TestReq &request) { + libcore::TestResp reply; + auto status = grpc_channel->Call("Test", request, &reply); + + if (status == QNetworkReply::NoError) { + *rpcOK = true; + return reply; + } else { + NOT_OK + return reply; + } + } + + libcore::UpdateResp Client::Update(bool *rpcOK, const libcore::UpdateReq &request) { + libcore::UpdateResp reply; + auto status = grpc_channel->Call("Update", request, &reply); + + if (status == QNetworkReply::NoError) { + *rpcOK = true; + return reply; + } else { + NOT_OK + return reply; + } + } +} + +#endif diff --git a/rpc/gRPC.h b/rpc/gRPC.h new file mode 100644 index 0000000..c2e6852 --- /dev/null +++ b/rpc/gRPC.h @@ -0,0 +1,49 @@ +#pragma once + +#ifdef NKR_NO_EXTERNAL +#define NKR_NO_GRPC +#endif + +#ifndef NKR_NO_GRPC + +#include "go/gen/libcore.pb.h" +#include + +namespace QtGrpc { + class Http2GrpcChannelPrivate; +} + +namespace NekoRay::rpc { + class Client { + public: + explicit Client(std::function onError, const QString &target, const QString &token); + + void Exit(); + + bool KeepAlive(); + + // QString returns is error string + + QString Start(bool *rpcOK, const libcore::LoadConfigReq &request); + + QString SetTun(bool *rpcOK, const libcore::SetTunReq &request); + + QString Stop(bool *rpcOK); + + long long QueryStats(const std::string &tag, const std::string &direct); + + std::string ListV2rayConnections(); + + libcore::TestResp Test(bool *rpcOK, const libcore::TestReq &request); + + libcore::UpdateResp Update(bool *rpcOK, const libcore::UpdateReq &request); + + private: + std::unique_ptr grpc_channel; + std::function onError; + }; + + inline Client *defaultClient; +} +#endif + diff --git a/sub/GroupUpdater.cpp b/sub/GroupUpdater.cpp new file mode 100644 index 0000000..45cecb3 --- /dev/null +++ b/sub/GroupUpdater.cpp @@ -0,0 +1,354 @@ +#include "qv2ray/utils/HTTPRequestHelper.hpp" + +#include "db/Database.hpp" +#include "db/ProfileFilter.hpp" +#include "fmt/includes.h" + +#include "GroupUpdater.hpp" + +#include + +#ifndef NKR_NO_EXTERNAL + +#include + +#endif + +#define FIRST_OR_SECOND(a, b) a.isEmpty() ? b : a + +namespace NekoRay::sub { + + GroupUpdater *groupUpdater = new GroupUpdater; + + void RawUpdater::update(const QString &str) { + // Base64 encoded subscription + if (auto str2 = DecodeB64IfValid(str);!str2.isEmpty()) { + update(str2); + return; + } + + // Clash + if (str.contains("proxies:")) { + updateClash(str); + return; + } + + // Multi line + if (str.count("\n") > 0) { + auto list = str.split("\n"); + for (const auto &str2: list) { + update(str2.trimmed()); + } + return; + } + + QSharedPointer ent; + + // Nekoray format + if (str.startsWith("nekoray://")) { + auto link = QUrl(str); + if (!link.isValid()) return; + ent = ProfileManager::NewProxyEntity(link.host()); + if (ent->bean->version == -114514) return; + auto j = DecodeB64IfValid(link.fragment().toUtf8(), QByteArray::Base64UrlEncoding); + if (j.isEmpty()) return; + ent->bean->FromJsonBytes(j.toUtf8()); + } + + // SOCKS + if (str.startsWith("socks5://") || str.startsWith("socks4://") || + str.startsWith("socks4a://") || str.startsWith("socks://")) { + ent = ProfileManager::NewProxyEntity("socks"); + auto ok = ent->SocksHTTPBean()->TryParseLink(str); + if (!ok) return; + } + + // HTTP + if (str.startsWith("http://") || str.startsWith("https://")) { + ent = ProfileManager::NewProxyEntity("http"); + auto ok = ent->SocksHTTPBean()->TryParseLink(str); + if (!ok) return; + } + + // ShadowSocks + if (str.startsWith("ss://")) { + ent = ProfileManager::NewProxyEntity("shadowsocks"); + auto ok = ent->ShadowSocksBean()->TryParseLink(str); + if (!ok) return; + } + + // VMess + if (str.startsWith("vmess://")) { + ent = ProfileManager::NewProxyEntity("vmess"); + auto ok = ent->VMessBean()->TryParseLink(str); + if (!ok) return; + } + + // VMess + if (str.startsWith("vless://")) { + ent = ProfileManager::NewProxyEntity("vless"); + auto ok = ent->TrojanVLESSBean()->TryParseLink(str); + if (!ok) return; + } + + // Trojan + if (str.startsWith("trojan://")) { + ent = ProfileManager::NewProxyEntity("trojan"); + auto ok = ent->TrojanVLESSBean()->TryParseLink(str); + if (!ok) return; + } + + // Naive + if (str.startsWith("https+naive://")) { + ent = ProfileManager::NewProxyEntity("naive"); + auto ok = ent->NaiveBean()->TryParseLink(str); + if (!ok) return; + } + + // End + if (ent == nullptr) return; + profileManager->AddProfile(ent, gid_add_to); + update_counter++; + } + +#ifndef NKR_NO_EXTERNAL + + QString Node2QString(const YAML::Node &n, const QString &def = "") { + try { + return n.as().c_str(); + } catch (const YAML::Exception &ex) { + return def; + } + } + + int Node2Int(const YAML::Node &n, const int &def = 0) { + try { + return n.as(); + } catch (const YAML::Exception &ex) { + return def; + } + } + + bool Node2Bool(const YAML::Node &n, const bool &def = false) { + try { + return n.as(); + } catch (const YAML::Exception &ex) { + return def; + } + } + +#endif + +// https://github.com/Dreamacro/clash/wiki/configuration + void RawUpdater::updateClash(const QString &str) { +#ifndef NKR_NO_EXTERNAL + try { + auto proxies = YAML::Load(str.toStdString())["proxies"]; + for (auto proxy: proxies) { + auto type = Node2QString(proxy["type"]); + if (type == "ss" || type == "ssr") type = "shadowsocks"; + if (type == "socks5") type = "socks"; + + auto ent = ProfileManager::NewProxyEntity(type); + if (ent->bean->version == -114514) continue; + + // common + ent->bean->name = Node2QString(proxy["name"]); + ent->bean->serverAddress = Node2QString(proxy["server"]); + ent->bean->serverPort = Node2Int(proxy["port"]); + + if (type == "shadowsocks") { + auto bean = ent->ShadowSocksBean(); + bean->method = Node2QString(proxy["cipher"]).replace("dummy", "none"); + bean->password = Node2QString(proxy["password"]); + auto plugin_n = proxy["plugin"]; + auto pluginOpts_n = proxy["plugin-opts"]; + if (plugin_n.IsDefined() && pluginOpts_n.IsDefined()) { + if (Node2QString(plugin_n) == "obfs") { + bean->plugin = "obfs-local;obfs=" + Node2QString(pluginOpts_n["mode"]) + ";obfs-host=" + + Node2QString(pluginOpts_n["host"]); + } + } + auto protocol_n = proxy["protocol"]; + if (protocol_n.IsDefined()) { + continue; // SSR + } + } else if (type == "socks" || type == "http") { + auto bean = ent->SocksHTTPBean(); + bean->password = Node2QString(proxy["username"]); + bean->password = Node2QString(proxy["password"]); + if (Node2Bool(proxy["tls"])) bean->stream->security = "tls"; + if (Node2Bool(proxy["skip-cert-verify"])) bean->stream->allow_insecure = true; + } else if (type == "trojan") { + auto bean = ent->TrojanVLESSBean(); + bean->password = Node2QString(proxy["password"]); + bean->stream->security = "tls"; + bean->stream->network = Node2QString(proxy["network"], "tcp"); + bean->stream->sni = FIRST_OR_SECOND(Node2QString(proxy["sni"]), Node2QString(proxy["servername"])); + if (Node2Bool(proxy["skip-cert-verify"])) bean->stream->allow_insecure = true; + } else if (type == "vmess") { + auto bean = ent->VMessBean(); + bean->uuid = Node2QString(proxy["uuid"]); + bean->aid = Node2Int(proxy["alterId"]); + bean->security = Node2QString(proxy["cipher"]); + bean->stream->network = Node2QString(proxy["network"], "tcp"); + bean->stream->sni = FIRST_OR_SECOND(Node2QString(proxy["sni"]), Node2QString(proxy["servername"])); + if (Node2Bool(proxy["tls"])) bean->stream->security = "tls"; + if (Node2Bool(proxy["skip-cert-verify"])) bean->stream->allow_insecure = true; + + auto ws = proxy["ws-opts"]; + if (ws.IsMap()) { + auto headers = ws["headers"]; + for (auto header: headers) { + if (Node2QString(header.first).toLower() == "host") { + bean->stream->host = Node2QString(header.second); + } + } + bean->stream->path = Node2QString(ws["path"]); + bean->stream->max_early_data = Node2Int(proxy["max-early-data"]); + bean->stream->early_data_header_name = Node2QString(ws["early-data-header-name"]); + } + + auto grpc = proxy["grpc-opts"]; + if (grpc.IsMap()) { + bean->stream->path = Node2QString(grpc["grpc-service-name"]); + } + + auto h2 = proxy["h2-opts"]; + if (h2.IsMap()) { + auto hosts = ws["host"]; + for (auto host: hosts) { + bean->stream->host = Node2QString(host); + break; + } + bean->stream->path = Node2QString(h2["path"]); + } + } else { + continue; + } + + profileManager->AddProfile(ent, gid_add_to); + update_counter++; + } + } catch (const YAML::Exception &ex) { + runOnUiThread([=] { + MessageBoxWarning("YAML Exception", ex.what()); + }); + } +#endif + } + + // 不要刷新,下载导入完会自己刷新 + void GroupUpdater::AsyncUpdate(const QString &str, int _sub_gid, + QObject *caller, const std::function &callback) { + if (caller != nullptr && callback != nullptr) { + connectOnce(this, &GroupUpdater::AsyncUpdateCallback, caller, + [=](QObject *receiver) { + if (receiver == caller) callback(); + }); + } + + auto content = str.trimmed(); + bool asURL = false; + + if (_sub_gid < 0 && (content.startsWith("http://") || content.startsWith("https://"))) { + auto items = QStringList{QObject::tr("As Subscription"), QObject::tr("As link")}; + bool ok; + auto a = QInputDialog::getItem(nullptr, + QObject::tr("url detected"), + QObject::tr("%1\nHow to update?").arg(content), + items, 0, false, &ok); + if (!ok) return; + if (items.indexOf(a) == 0) asURL = true; + } + + runOnNewThread([=] { + Update(str, _sub_gid, asURL); + emit AsyncUpdateCallback(caller); + }); + } + + void GroupUpdater::Update(const QString &_str, int _sub_gid, bool _not_sub_as_url) { + // 创建 rawUpdater + NekoRay::dataStore->imported_count = 0; + auto rawUpdater = std::make_unique(); + rawUpdater->gid_add_to = _sub_gid; + + // 准备 + QString sub_user_info; + bool asURL = _sub_gid >= 0 || _not_sub_as_url; // 把 _str 当作 url 处理(下载内容) + auto content = _str.trimmed(); + auto group = profileManager->GetGroup(_sub_gid); + + // 网络请求 + if (asURL) { + auto groupName = group == nullptr ? content : group->name; + showLog(">>>>>>> " + QObject::tr("Requesting subscription: %1").arg(groupName)); + + auto resp = NetworkRequestHelper::HttpGet(content); + if (!resp.error.isEmpty()) { + showLog(">>>>>>> " + QObject::tr("Requesting subscription %1 error: %2") + .arg(groupName, resp.error + "\n" + resp.data)); + return; + } + + content = resp.data; + sub_user_info = NetworkRequestHelper::GetHeader(resp.header, "Subscription-UserInfo"); + } + + QList> in; // 更新前 + QList> out_all; // 更新前 + 更新后 + QList> out; // 更新后 + QList> only_in; // 只在更新前有的 + QList> only_out; // 只在更新后有的 + QList> update_del; // 更新前后都有的,删除更新后多余的 + + // 订阅解析前 + if (group != nullptr) { + in = group->Profiles(); + group->info = sub_user_info; + group->order.clear(); + group->Save(); + } + + // 解析并添加 profile + rawUpdater->update(content); + + if (group != nullptr) { + out_all = group->Profiles(); + + ProfileFilter::OnlyInSrc_ByPointer(out_all, in, out); + ProfileFilter::OnlyInSrc(in, out, only_in); + ProfileFilter::OnlyInSrc(out, in, only_out); + ProfileFilter::Common(in, out, update_del, false, true); + update_del += only_in; + + for (const auto &ent: update_del) { + profileManager->DeleteProfile(ent->id); + } + + QString notice_added; + for (const auto &ent: only_out) { + notice_added += ent->bean->DisplayTypeAndName() + "\n"; + } + QString notice_deleted; + for (const auto &ent: only_in) { + notice_deleted += ent->bean->DisplayTypeAndName() + "\n"; + } + + runOnUiThread([=] { + auto change = "\n" + QObject::tr("Added %1 profiles:\n%2\nDeleted %3 Profiles:\n%4") + .arg(only_out.length()).arg(notice_added) + .arg(only_in.length()).arg(notice_deleted); + if (only_out.length() + only_in.length() == 0) change = QObject::tr("Nothing"); + showLog(">>>>>>> " + QObject::tr("Change of %1:").arg(group->name) + " " + change); + dialog_message("SubUpdater", "finish-dingyue"); + }); + } else { + NekoRay::dataStore->imported_count = rawUpdater->update_counter; + runOnUiThread([=] { + dialog_message("SubUpdater", "finish"); + }); + } + } +} diff --git a/sub/GroupUpdater.hpp b/sub/GroupUpdater.hpp new file mode 100644 index 0000000..2fb9105 --- /dev/null +++ b/sub/GroupUpdater.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +namespace NekoRay::sub { + class RawUpdater { + public: + void updateClash(const QString &str); + + void update(const QString &str); + + int gid_add_to = -1; // 导入到指定组 -1 为当前选中组 + + int update_counter = 0; // 新增了多少个配置 + }; + + class GroupUpdater : public QObject { + Q_OBJECT + + public: + void AsyncUpdate(const QString &str, int _sub_gid = -1, + QObject *caller = nullptr, const std::function &callback = nullptr); + + void Update(const QString &_str, int _sub_gid = -1, bool _not_sub_as_url = false); + + signals: + + void AsyncUpdateCallback(QObject *caller); + }; + + extern GroupUpdater *groupUpdater; +} + diff --git a/sys/AutoRun.cpp b/sys/AutoRun.cpp new file mode 100644 index 0000000..4570388 --- /dev/null +++ b/sys/AutoRun.cpp @@ -0,0 +1,69 @@ +#include "AutoRun.hpp" + +#include + +#ifdef Q_OS_WIN + +#include +#include + +//设置程序自启动 appPath程序路径 +void SetProcessAutoRunSelf(bool enable) { + auto appPath = QApplication::applicationFilePath(); + + QSettings settings("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", + QSettings::NativeFormat); + + //以程序名称作为注册表中的键 + //根据键获取对应的值(程序路径) + QFileInfo fInfo(appPath); + QString name = fInfo.baseName(); + QString path = settings.value(name).toString(); + + //如果注册表中的路径和当前程序路径不一样, + //则表示没有设置自启动或自启动程序已经更换了路径 + //toNativeSeparators的意思是将"/"替换为"\" + QString newPath = QDir::toNativeSeparators(appPath); + + if (enable) { + if (path != newPath) { + settings.setValue(name, newPath); + } + } else { + settings.remove(name); + } +} + +bool GetProcessAutoRunSelf() { + auto appPath = QApplication::applicationFilePath(); + + QSettings settings("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", + QSettings::NativeFormat); + + //以程序名称作为注册表中的键 + //根据键获取对应的值(程序路径) + QFileInfo fInfo(appPath); + QString name = fInfo.baseName(); + QString path = settings.value(name).toString(); + + //如果注册表中的路径和当前程序路径不一样, + //则表示没有设置自启动或自启动程序已经更换了路径 + //toNativeSeparators的意思是将"/"替换为"\" + QString newPath = QDir::toNativeSeparators(appPath); + return path == newPath; +} + + +#else + +#include + +void SetProcessAutoRunSelf(bool enable) { + QMessageBox::warning(nullptr, "Error", "Autorun is not yet implemented on your platform."); +} + +bool GetProcessAutoRunSelf() { + return false; +} + +#endif diff --git a/sys/AutoRun.hpp b/sys/AutoRun.hpp new file mode 100644 index 0000000..ef92937 --- /dev/null +++ b/sys/AutoRun.hpp @@ -0,0 +1,5 @@ +#pragma once + +void SetProcessAutoRunSelf(bool enable); + +bool GetProcessAutoRunSelf(); diff --git a/sys/ExternalProcess.cpp b/sys/ExternalProcess.cpp new file mode 100644 index 0000000..d6e74c8 --- /dev/null +++ b/sys/ExternalProcess.cpp @@ -0,0 +1,70 @@ +#include "ExternalProcess.hpp" + +namespace NekoRay::sys { + ExternalProcess::ExternalProcess(const QString &tag, + const QString &program, + const QStringList &arguments, + const QStringList &env) + : QProcess() { + this->tag = tag; + this->program = program; + this->arguments = arguments; + this->env = env; + this->running_list = &running_ext; + } + + void ExternalProcess::Start() { + if (started) return; + started = true; + *running_list += this; + + if (show_log) { + connect(this, &QProcess::readyReadStandardOutput, this, + [&]() { + showLog_ext_vt100(readAllStandardOutput().trimmed()); + }); + connect(this, &QProcess::readyReadStandardError, this, + [&]() { + showLog_ext_vt100(readAllStandardError().trimmed()); + }); + } + + connect(this, &QProcess::errorOccurred, this, + [&](QProcess::ProcessError error) { + if (!killed) { + crashed = true; + showLog_ext(tag, "[Error] Crashed:" + QProcess::errorString()); + dialog_message("ExternalProcess", "Crashed"); + } + }); + connect(this, &QProcess::stateChanged, this, + [&](QProcess::ProcessState state) { + if (state == QProcess::NotRunning) { + if (killed) { + showLog_ext(tag, "Stopped"); + } else if (!crashed) { + crashed = true; + Kill(); + showLog_ext(tag, "[Error] Crashed?"); + dialog_message("ExternalProcess", "Crashed"); + } + } + }); + + showLog_ext(tag, "[Starting] " + env.join(" ") + " " + program + " " + arguments.join(" ")); + + QProcess::setEnvironment(env); + QProcess::start(program, arguments); + } + + void ExternalProcess::Kill() { + if (killed) return; + killed = true; + running_list->removeAll(this); + if (!crashed) { + QProcess::kill(); + QProcess::waitForFinished(500); + } + } + +} diff --git a/sys/ExternalProcess.hpp b/sys/ExternalProcess.hpp new file mode 100644 index 0000000..3350ed7 --- /dev/null +++ b/sys/ExternalProcess.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "main/NekoRay.hpp" + +#include + +namespace NekoRay::sys { + class ExternalProcess : public QProcess { + public: + QString tag; + QString program; + QStringList arguments; + QStringList env; + + bool show_log = true; + QList *running_list; + + ExternalProcess(const QString &tag, + const QString &program, + const QStringList &arguments, + const QStringList &env); + + // start & kill is one time + + void Start(); + + void Kill(); + + private: + bool started = false; + bool killed = false; + bool crashed = false; + }; + + // start & kill change this list + inline QList running_ext; +} diff --git a/sys/windows/MiniDump.cpp b/sys/windows/MiniDump.cpp new file mode 100644 index 0000000..58d61e2 --- /dev/null +++ b/sys/windows/MiniDump.cpp @@ -0,0 +1,68 @@ +#include "MiniDump.h" + +#include +#include +#include + +#include +#include +#include +#include + +typedef BOOL( WINAPI *MINIDUMPWRITEDUMP )( +HANDLE hProcess, +DWORD dwPid, +HANDLE hFile, +MINIDUMP_TYPE DumpType, +CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, +CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, +CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam); + +LONG CreateCrashHandler(EXCEPTION_POINTERS *pException) { + QDir::setCurrent(QApplication::applicationDirPath()); + + HMODULE DllHandle = NULL; + DllHandle = LoadLibrary(_T("DBGHELP.DLL")); + + if (DllHandle) { + MINIDUMPWRITEDUMP Dump = (MINIDUMPWRITEDUMP) GetProcAddress(DllHandle, "MiniDumpWriteDump"); + if (Dump) { + //创建 Dump 文件 + QDateTime CurDTime = QDateTime::currentDateTime(); + QString current_date = CurDTime.toString("yyyy_MM_dd_hh_mm_ss"); + //dmp文件的命名 + QString dumpText = "Dump_" + current_date + ".dmp"; + EXCEPTION_RECORD *record = pException->ExceptionRecord; + QString errCode(QString::number(record->ExceptionCode, 16)); + QString errAddr(QString::number((uint) record->ExceptionAddress, 16)); + QString errFlag(QString::number(record->ExceptionFlags, 16)); + QString errPara(QString::number(record->NumberParameters, 16)); + HANDLE DumpHandle = CreateFile((LPCWSTR) dumpText.utf16(), + GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (DumpHandle != INVALID_HANDLE_VALUE) { + MINIDUMP_EXCEPTION_INFORMATION dumpInfo; + dumpInfo.ExceptionPointers = pException; + dumpInfo.ThreadId = GetCurrentThreadId(); + dumpInfo.ClientPointers = TRUE; + //将dump信息写入dmp文件 + Dump(GetCurrentProcess(), GetCurrentProcessId(), DumpHandle, MiniDumpNormal, &dumpInfo, + NULL, NULL); + CloseHandle(DumpHandle); + } else { + dumpText = ""; + } + //创建消息提示 + QMessageBox::warning(NULL, "Application crashed", + QString("ErrorCode: %1 ErrorAddr:%2 ErrorFlag: %3 ErrorPara: %4\nVersion: %5\nDump file at %6") + .arg(errCode).arg(errAddr).arg(errFlag).arg(errPara) + .arg(NKR_VERSION).arg(dumpText), + QMessageBox::Ok); + return EXCEPTION_EXECUTE_HANDLER; + } + } +} + +void Windows_SetCrashHandler() { + SetErrorMode(SEM_FAILCRITICALERRORS); + SetUnhandledExceptionFilter(CreateCrashHandler); +} diff --git a/sys/windows/MiniDump.h b/sys/windows/MiniDump.h new file mode 100644 index 0000000..ed5257e --- /dev/null +++ b/sys/windows/MiniDump.h @@ -0,0 +1,11 @@ +#pragma once + +#ifdef __MINGW32__ + +void Windows_SetCrashHandler(){} + +#else + +void Windows_SetCrashHandler(); + +#endif diff --git a/test/test-qt6-build.sh b/test/test-qt6-build.sh new file mode 100644 index 0000000..7c1247b --- /dev/null +++ b/test/test-qt6-build.sh @@ -0,0 +1,5 @@ +rm -rf build-qt6 +mkdir -p build-qt6 +cd build-qt6 +cmake -GNinja -DQT_VERSION_MAJOR=6 .. +ninja diff --git a/translations/translations.qrc b/translations/translations.qrc new file mode 100644 index 0000000..ee9968c --- /dev/null +++ b/translations/translations.qrc @@ -0,0 +1,5 @@ + + + zh_CN.qm + + diff --git a/translations/zh_CN.ts b/translations/zh_CN.ts new file mode 100644 index 0000000..6fb99ef --- /dev/null +++ b/translations/zh_CN.ts @@ -0,0 +1,1185 @@ + + + + + DialogBasicSettings + + Basic Settings + 基本设置 + + + Enable + 启用 + + + HTTP Listen Port + HTTP 监听端口 + + + Listen Address + 监听地址 + + + Socks Listen Port + Socks 监听端口 + + + concurrency + 并发 + + + User Agent + + + + Common + 通用 + + + Style + 样式 + + + Theme + 主题 + + + System + 系统 + + + Subscription + 订阅 + + + Core + 核心 + + + Extra Core + 其他核心 + + + Select + 选择 + + + Edit + 编辑 + + + Custom Inbound + 自定义入站 + + + Asset Location + 资源文件路径 + + + Default: dir of "nekoray-core" + 默认值:和 nekoray_core 同路径 + + + Settings changed + 设置改变 + + + Restart nekoray to take effect. + 重启 nekoray 生效。 + + + Concurrent + 并发 + + + Test URL + 测试 URL + + + Use proxy when updating subscription + 更新订阅时使用代理 + + + Language + 语言 + + + Security + 安全 + + + Insecure hint + 提示不安全的配置 + + + Skip TLS certificate authentication by default + 默认跳过 TLS 证书验证 + + + Traffic statistics refresh rate + 流量统计刷新率 + + + Fast + + + + Slow + + + + Off + 关闭 + + + Add + 添加 + + + Delete + 删除 + + + Please input the core name. + 请输入核心名. + + + Please select the core name. + 请选择核心名. + + + Connection statistics + 连接统计 + + + + DialogEditGroup + + Edit Group + 编辑分组 + + + Type + 类型 + + + Name + 名称 + + + Basic + 基本 + + + Subscription + 订阅 + + + URL + + + + Archive + 归档 + + + Warning + 警告 + + + Please input URL + 请输入 URL + + + Copy profile share links + 复制所有配置的分享链接 + + + Copied + 复制成功 + + + Copy profile share links (Nekoray) + 复制所有配置的分享链接 (Nekoray) + + + + DialogEditProfile + + Edit + 编辑 + + + Common + 通用 + + + Type + 类型 + + + Port + 端口 + + + Address + 地址 + + + Name + 名称 + + + Network + 传输 + + + Security + 传输层安全 + + + Network Settings (%1) + 传输设置 (%1) + + + Security Settings + 安全设置 + + + Allow insecure + 不检查服务器证书(不安全) + + + Certificate + 证书 + + + Custom Json Settings + 自定义 JSON 设置 + + + Not set + 未设置 + + + Already set + 已设置 + + + Path + 路径(Path) + + + Host + 主机(Host) + + + SNI + + + + Custom + 自定义 + + + Packet Encoding + 包编码 + + + Settings + 设置 + + + + DialogHotkey + + Hot key + 热键 + + + Show groups + 显示分组 + + + Show routes + 显示路由 + + + Trigger main window + 显示/隐藏主窗口 + + + + DialogManageGroups + + Groups + 分组 + + + New group + 新建分组 + + + Update all subscriptions + 更新所有订阅 + + + Confirmation + 确认 + + + Update all subscriptions? + 更新所有订阅? + + + + DialogManageRoutes + + Routes + 路由 + + + Outbound Domain Strategy + 出站域名策略 + + + Disable + 禁用 + + + Sniffing Mode + 流量探测 + + + The sniffing result is used for routing + 探测结果用于路由判断 + + + The sniffing result is used for destination + 探测结果用于目标地址 + + + Direct DNS + 直连 DNS + + + Remote DNS + 远程 DNS + + + Enable DNS Routing + 启用 DNS 路由 + + + Domain Strategy + 域名策略 + + + Matcher + 域名匹配器 + + + Block + 阻止 + + + Direct + 直连 + + + Domain + 域名 + + + Proxy + 代理 + + + Preset + 预设 + + + Bypass LAN and China + 绕过局域网和大陆 + + + Global + 全局 + + + IP + + + + Custom + 自定义 + + + Save + 保存 + + + Load + 加载 + + + Cancel + 取消 + + + Remove + 删除 + + + Save routing: %1 + 保存路由: %1 + + + Load routing: %1 + 加载路由: %1 + + + Remove routing: %1 + 删除路由: %1 + + + Mange route set + 管理路由规则 + + + Custom (global) + 自定义 (全局) + + + + EditChain + + EditChain + + + + Select Profile + 选择配置 + + + Traffic order is from top to bottom + 流量顺序是从上到下(最后一个配置为流量的出口) + + + + EditCustom + + EditCustom + + + + Core + 核心 + + + Json + + + + Command + 命令 + + + + EditNaive + + EditNaive + + + + Protocol + 协议 + + + Password + 密码 + + + Extra headers + 附加标头 + + + SNI + + + + Username + 用户名 + + + Certificate + 证书 + + + Insecure concurrency + 不安全并发 + + + + EditShadowSocks + + Plugin Args + 插件参数 + + + Password + 密码 + + + Encryption + 加密 + + + Plugin + 插件 + + + Form + + + + + EditSocksHttp + + Version + 版本 + + + Username + 用户名 + + + Password + 密码 + + + Form + + + + + EditTrojanVLESS + + Password + 密码 + + + + EditVMess + + Security + 加密 + + + EditVMess + + + + Alter Id + + + + UUID + + + + + GroupItem + + Update Subscription + 更新订阅 + + + Edit + 编辑 + + + Basic + 基本 + + + Subscription + 订阅 + + + Confirmation + 确认 + + + Remove + 删除 + + + Remove %1? + 删除 %1 ? + + + Archive + 归档 + + + Update %1? + 更新 %1 ? + + + + JsonEditor + + JSON Editor + + + + Format JSON + + + + Remove All Comments + + + + Json Editor + + + + Structure Preview + + + + OK + + + + Json Contains Syntax Errors + + + + Original Json may contain syntax errors. Json tree is disabled. + + + + You must correct these errors before continuing. + + + + Syntax Errors + + + + Please fix the JSON errors or remove the comments before continue + + + + + MainWindow + + Program + 程序 + + + Preferences + 首选项 + + + Server + 服务器 + + + Ads + 推广 + + + Type + 类型 + + + Address + 地址 + + + Name + 名称 + + + Test Result + 测试结果 + + + Traffic + 流量 + + + System Proxy + 系统代理 + + + Share + 分享 + + + Exit + 退出 + + + Basic Settings + 基本设置 + + + Groups + 分组 + + + Stop + 停止 + + + Routes + 路由 + + + Add profile from clipboard + 从剪切板添加 + + + Debug Info + 调试信息 + + + Copy Link + 复制链接 + + + Clear Test Result + 清理测试结果 + + + Scan QR Code + 扫描 QR Code + + + Disable + 禁用 + + + Error + 错误 + + + Default + 默认 + + + Confirmation + 确认 + + + Settings changed, restart proxy? + 设置已改变,是否重启代理? + + + Imported %1 profile(s) + 导入了 %1 个配置 + + + Running: %1 + 正在运行: %1 + + + None + + + + Unavailable + 不可用 + + + Remove %1 item(s) ? + 删除 %1 个项目? + + + Reset traffic of %1 item(s) ? + 重置 %1 个项目的流量? + + + Config copied + 配置已复制 + + + [%1] test error: %2 + [%1] 测试错误: %2 + + + Clear + 清除 + + + NekoRay + + + + fake + + + + Testing + 正在测试 + + + Http inbound is not enabled, can't set system proxy. + HTTP 入站未启用,无法设置系统代理。 + + + Update + 更新 + + + Document + 文档 + + + Select + 选择 + + + QR Code not found + 未扫描到 QR Code + + + Move + 移动 + + + Log + 日志 + + + Connection + 连接 + + + Status + 状态 + + + Outbound + 出站 + + + Destination + 目标地址 + + + End + 结束 + + + Active + 活动 + + + Start: %1 +End: %2 + 开始: %1 +结束: %2 + + + Starting profile %1 + 正在启动配置 %1 + + + Stopping profile %1 + 正在停止配置 %1 + + + Start with system + 跟随系统启动 + + + Remember last profile + 记住最后的配置 + + + Start minimal + 最小化启动 + + + Move %1 item(s) + 移动 %1 个项目 + + + Profile is insecure: %1 + 配置不安全: %1 + + + Remove Unavailable + 删除不可用 + + + Settings + 设置 + + + Input + 输入 + + + Please enter the items to be tested, separated by commas +1. Latency +2. Download speed +3. In and Out IP +4. NAT type + 请输入要测试的项目,用逗号分隔 +1. 延迟 +2. 下载速度 +3. 入口出口 IP +4. NAT 类型 + + + New profile + 手动输入配置 + + + Start [ Enter ] + 启动 [ Enter ] + + + Delete [ Delete ] + 删除 [ Delete ] + + + Hot key + 热键 + + + QR Code and link + 显示 QR Code 和分享链接 + + + Export V2ray config + 导出 V2ray 配置 + + + QR Code and link (Nekoray) + 显示 QR Code 和分享链接 (Nekoray) + + + Active Routing + 当前路由规则 + + + Active Server + 当前服务器 + + + Load routing and apply: %1 + 加载路由规则并应用: %1 + + + Copy links of selected + 复制选中的分享链接 + + + Copied %1 item(s) + 复制了%1 个项目 + + + New profile from clipboard + 从剪切板添加 + + + Full Test + 完整测试 + + + Current Group + 当前分组 + + + Reset Traffic + 重置流量 + + + Delete Repeat + 删除重复 + + + Select All + 全选 + + + VPN Mode + VPN 模式 + + + Failed to stop VPN process + 停止 VPN 失败 + + + Enable System Proxy + 启用系统代理 + + + Enable VPN + 启用 VPN + + + + ProxyItem + + Remove + 删除 + + + Confirmation + 确认 + + + Remove %1? + 删除 %1 ? + + + + QObject + + As Subscription + 作为订阅 + + + As link + 作为链接 + + + url detected + 检测到 URL + + + %1 +How to update? + %1 +如何处理? + + + Added %1 profiles: +%2 +Deleted %3 Profiles: +%4 + 增加了 %1 个配置: +%2 +删除了 %3 个配置: +%4 + + + Proxy: %1 +Direct: %2 + 代理: %1 +直连: %2 + + + Used: %1 Remain: %2 Expire: %3 + 已用 %1 剩余 %2 过期 %3 + + + Core not found: %1 + 找不到 %1 核心 + + + Update + 更新 + + + No update + 无更新 + + + Open in browser + 浏览器打开 + + + Close + 关闭 + + + Update is ready, restart to install? + 更新已下载好,重启应用? + + + Update found: %1 +Release note: +%2 + 检测到更新: %1 +更新日志: +%2 + + + Unavailable + 不可用 + + + Request with proxy but no profile started. + 即将使用代理请求,但是代理未启动。 + + + Chain Proxy + 链式代理 + + + The configuration (insecure) can be detected and identified, the transmission is fully visible to the censor and is not resistant to man-in-the-middle tampering with the content of the communication. + 该配置 (不安全) 能够被检测识别,传输的内容对审查者完全可见,并且无法抵抗中间人篡改通讯内容. + + + This configuration (Shadowsocks streaming cipher) can be accurately proactively detected and decrypted by censors without requiring a password, and cannot be mitigated by turning on IV replay filters on the server side. + +Learn more: https://github.com/net4people/bbs/issues/24 + 该配置 (Shadowsocks 流式密码) 可以被准确地主动探测、在不需要密码的情况下被审查者解密流量, 且服务端开启 IV 重放过滤器也无法缓解. + +了解更多: https://github.com/net4people/bbs/issues/24 + + + This configuration (VMess MD5 authentication) has been deprecated by upstream because of its questionable resistance to tampering and concealment. + +As of January 1, 2022, compatibility with MD5 authentication information will be disabled on the server side by default. Any client using MD5 authentication information will not be able to connect to a server with VMess MD5 authentication information disabled. + 该配置 (VMess MD5 认证) 抗篡改能力存疑, 隐蔽性存疑, 已被上游废弃. + +自 2022 年 1 月 1 日起, 服务器端将默认禁用对于 MD5 认证信息 的兼容. 任何使用 MD5 认证信息的客户端将无法连接到禁用 VMess MD5 认证信息的服务器端. + + + Requesting subscription: %1 + 正在请求订阅: %1 + + + Requesting subscription %1 error: %2 + 请求订阅 %1 错误: %2 + + + Nothing + + + + Change of %1: + %1 变化: + + + This profile is cleartext, don't use it if the server is not in your local network. + 该配置为明文传输,如果服务器不在本地局域网,请不要使用。 + + + Another program is running. + 另一个 Nekoray 实例正在运行。 + + + Select + 选择 + + + + Qv2ray::ui::widgets::AutoCompleteTextEdit + + You can not input space characters here. + + + + diff --git a/ui/GroupSort.hpp b/ui/GroupSort.hpp new file mode 100644 index 0000000..99e6e05 --- /dev/null +++ b/ui/GroupSort.hpp @@ -0,0 +1,21 @@ +#pragma once + +namespace NekoRay { + // implement in mainwindow + namespace GroupSortMethod { + enum GroupSortMethod { + Raw, + ByType, + ByAddress, + ByName, + ByLatency, + ById, + }; + } + + struct GroupSortAction { + GroupSortMethod::GroupSortMethod method = GroupSortMethod::Raw; + bool save_sort = false; //保存到文件 + bool descending = false; //默认升序,开这个就是降序 + }; +} diff --git a/ui/ThemeManager.cpp b/ui/ThemeManager.cpp new file mode 100644 index 0000000..025589d --- /dev/null +++ b/ui/ThemeManager.cpp @@ -0,0 +1,84 @@ +#include "ThemeManager.hpp" + +#include + +ThemeManager *themeManager = new ThemeManager; + +extern QString ReadFileText(const QString &path); + +void ThemeManager::ApplyTheme(const QString &theme) { + auto internal = [=] { + if (this->system_style_name.isEmpty()) { + this->system_style_name = qApp->style()->objectName(); + } + if (this->current_theme == theme) { + return; + } + + bool ok; + auto themeId = theme.toInt(&ok); + + if (ok) { + // System & Built-in + QString qss; + + if (themeId != 0) { + QString path; + std::map replace; + switch (themeId) { + case 1: + path = ":/themes/feiyangqingyun/qss/flatgray.css"; + replace[":/qss/"] = ":/themes/feiyangqingyun/qss/"; + break; + case 2: + path = ":/themes/feiyangqingyun/qss/lightblue.css"; + replace[":/qss/"] = ":/themes/feiyangqingyun/qss/"; + break; + case 3: + path = ":/themes/feiyangqingyun/qss/blacksoft.css"; + replace[":/qss/"] = ":/themes/feiyangqingyun/qss/"; + break; + default: + return; + } + qss = ReadFileText(path); + for (auto const &[a, b]: replace) { + qss = qss.replace(a, b); + } + } + + auto system_style = QStyleFactory::create(this->system_style_name); + + if (themeId == 0) { + // system theme + qApp->setPalette(system_style->standardPalette()); + qApp->setStyle(system_style); + qApp->setStyleSheet(""); + } else { + if (themeId == 1 || themeId == 2 || themeId == 3) { + // feiyangqingyun theme + QString paletteColor = qss.mid(20, 7); + qApp->setPalette(QPalette(paletteColor)); + } else { + // other theme + qApp->setPalette(system_style->standardPalette()); + } + qApp->setStyleSheet(qss); + } + } else { + // QStyleFactory + const auto &_style = QStyleFactory::create(theme); + if (_style != nullptr) { + qApp->setPalette(_style->standardPalette()); + qApp->setStyle(_style); + qApp->setStyleSheet(""); + } + } + + current_theme = theme; + }; + internal(); + + auto nekoray_css = ReadFileText(":nekoray/nekoray.css"); + qApp->setStyleSheet(qApp->styleSheet().append("\n").append(nekoray_css)); +} diff --git a/ui/ThemeManager.hpp b/ui/ThemeManager.hpp new file mode 100644 index 0000000..d902bf6 --- /dev/null +++ b/ui/ThemeManager.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +class ThemeManager { +public: + QString system_style_name = ""; + QString current_theme = "0"; // int: 0:system 1+:builtin string: QStyleFactory + + void ApplyTheme(const QString &theme); +}; + +extern ThemeManager *themeManager; diff --git a/ui/dialog_basic_settings.cpp b/ui/dialog_basic_settings.cpp new file mode 100644 index 0000000..238213f --- /dev/null +++ b/ui/dialog_basic_settings.cpp @@ -0,0 +1,221 @@ +#include "dialog_basic_settings.h" +#include "ui_dialog_basic_settings.h" + +#include "qv2ray/ui/widgets/editors/w_JsonEditor.hpp" +#include "ui/ThemeManager.hpp" +#include "main/GuiUtils.hpp" +#include "main/NekoRay.hpp" + +#include +#include +#include + +class ExtraCoreWidget : public QWidget { +public: + QString coreName; + + QLabel *label_name; + MyLineEdit *lineEdit_path; + QPushButton *pushButton_pick; + + explicit ExtraCoreWidget(QJsonObject *extraCore, const QString &coreName_, + QWidget *parent = nullptr) + : QWidget(parent) { + coreName = coreName_; + label_name = new QLabel; + label_name->setText(coreName); + lineEdit_path = new MyLineEdit; + lineEdit_path->setText(extraCore->value(coreName).toString()); + pushButton_pick = new QPushButton; + pushButton_pick->setText(QObject::tr("Select")); + auto layout = new QHBoxLayout; + layout->addWidget(label_name); + layout->addWidget(lineEdit_path); + layout->addWidget(pushButton_pick); + setLayout(layout); + setContentsMargins(0, 0, 0, 0); + // + connect(pushButton_pick, &QPushButton::clicked, this, [=] { + auto fn = QFileDialog::getOpenFileName(this, QObject::tr("Select"), QDir::currentPath(), + "", nullptr, QFileDialog::Option::ReadOnly); + if (!fn.isEmpty()) { + lineEdit_path->setText(fn); + } + }); + connect(lineEdit_path, &QLineEdit::textChanged, this, [=](const QString &newTxt) { + extraCore->insert(coreName, newTxt); + }); + } +}; + +DialogBasicSettings::DialogBasicSettings(QWidget *parent) + : QDialog(parent), ui(new Ui::DialogBasicSettings) { + ui->setupUi(this); + + // Common + + ui->socks_ip->setText(NekoRay::dataStore->inbound_address); + ui->log_level->setCurrentText(NekoRay::dataStore->log_level); + ui->connection_statistics->setChecked(NekoRay::dataStore->connection_statistics); + CACHE.custom_inbound = NekoRay::dataStore->custom_inbound; + + if (NekoRay::dataStore->traffic_loop_interval == 500) { + ui->rfsh_r->setCurrentIndex(0); + } else if (NekoRay::dataStore->traffic_loop_interval == 1000) { + ui->rfsh_r->setCurrentIndex(1); + } else { + ui->rfsh_r->setCurrentIndex(2); + } + + D_LOAD_INT(inbound_socks_port) + D_LOAD_INT_ENABLE(inbound_http_port, http_enable) + D_LOAD_INT_ENABLE(mux_cool, mux_cool_enable) + D_LOAD_INT(test_concurrent) + D_LOAD_STRING(test_url) + + connect(ui->custom_inbound_edit, &QPushButton::clicked, this, [=] { + C_EDIT_JSON_ALLOW_EMPTY(custom_inbound) + }); + + // Style + + ui->language->setCurrentIndex(NekoRay::dataStore->language); + connect(ui->language, QOverload::of(&QComboBox::currentIndexChanged), this, [=](int index) { + CACHE.needRestart = true; + }); + + int built_in_len = ui->theme->count(); + ui->theme->addItems(QStyleFactory::keys()); + // + bool ok; + auto themeId = NekoRay::dataStore->theme.toInt(&ok); + if (ok) { + ui->theme->setCurrentIndex(themeId); + } else { + ui->theme->setCurrentText(NekoRay::dataStore->theme); + } + // + connect(ui->theme, QOverload::of(&QComboBox::currentIndexChanged), this, [=](int index) { + if (index + 1 <= built_in_len) { + themeManager->ApplyTheme(Int2String(index)); + NekoRay::dataStore->theme = Int2String(index); + } else { + themeManager->ApplyTheme(ui->theme->currentText()); + NekoRay::dataStore->theme = ui->theme->currentText(); + } + repaint(); + mainwindow->repaint(); + NekoRay::dataStore->Save(); + }); + + // Subscription + + ui->user_agent->setText(NekoRay::dataStore->user_agent); + ui->sub_use_proxy->setChecked(NekoRay::dataStore->sub_use_proxy); + + // Core + + ui->core_v2ray_asset->setText(NekoRay::dataStore->v2ray_asset_dir); + // + CACHE.extraCore = QString2QJsonObject(NekoRay::dataStore->extraCore->core_map); + if (!CACHE.extraCore.contains("naive")) CACHE.extraCore.insert("naive", ""); + if (!CACHE.extraCore.contains("hysteria")) CACHE.extraCore.insert("hysteria", ""); + // + auto extra_core_layout = ui->extra_core_box->layout(); + for (const auto &s: CACHE.extraCore.keys()) { + extra_core_layout->addWidget(new ExtraCoreWidget(&CACHE.extraCore, s)); + } + + connect(ui->core_v2ray_asset, &QLineEdit::textChanged, this, [=] { + CACHE.needRestart = true; + }); + connect(ui->core_v2ray_asset_pick, &QPushButton::clicked, this, [=] { + auto fn = QFileDialog::getExistingDirectory(this, tr("Select"), QDir::currentPath(), + QFileDialog::Option::ShowDirsOnly | QFileDialog::Option::ReadOnly); + if (!fn.isEmpty()) { + ui->core_v2ray_asset->setText(fn); + } + }); + connect(ui->extra_core_add, &QPushButton::clicked, this, [=] { + bool ok; + auto s = QInputDialog::getText(nullptr, tr("Add"), + tr("Please input the core name."), + QLineEdit::Normal, "", &ok).trimmed(); + if (s.isEmpty() || !ok) return; + if (CACHE.extraCore.contains(s)) return; + extra_core_layout->addWidget(new ExtraCoreWidget(&CACHE.extraCore, s)); + CACHE.extraCore.insert(s, ""); + }); + connect(ui->extra_core_del, &QPushButton::clicked, this, [=] { + bool ok; + auto s = QInputDialog::getItem(nullptr, tr("Delete"), + tr("Please select the core name."), + CACHE.extraCore.keys(), 0, false, &ok); + if (s.isEmpty() || !ok) return; + for (int i = 0; i < extra_core_layout->count(); i++) { + auto item = extra_core_layout->itemAt(i); + auto ecw = dynamic_cast(item->widget()); + if (ecw != nullptr && ecw->coreName == s) { + ecw->deleteLater(); + CACHE.extraCore.remove(s); + return; + } + } + }); + + // Security + + ui->insecure_hint->setChecked(NekoRay::dataStore->insecure_hint); + ui->skip_cert->setChecked(NekoRay::dataStore->skip_cert); +} + +DialogBasicSettings::~DialogBasicSettings() { + delete ui; +} + +void DialogBasicSettings::accept() { + if (CACHE.needRestart) MessageBoxWarning(tr("Settings changed"), tr("Restart nekoray to take effect.")); + + // Common + + NekoRay::dataStore->inbound_address = ui->socks_ip->text(); + NekoRay::dataStore->log_level = ui->log_level->currentText(); + NekoRay::dataStore->connection_statistics = ui->connection_statistics->isChecked(); + NekoRay::dataStore->custom_inbound = CACHE.custom_inbound; + + if (ui->rfsh_r->currentIndex() == 0) { + NekoRay::dataStore->traffic_loop_interval = 500; + } else if (ui->rfsh_r->currentIndex() == 1) { + NekoRay::dataStore->traffic_loop_interval = 1000; + } else { + NekoRay::dataStore->traffic_loop_interval = 0; + } + + D_SAVE_INT(inbound_socks_port) + D_SAVE_INT_ENABLE(inbound_http_port, http_enable) + D_SAVE_INT_ENABLE(mux_cool, mux_cool_enable) + D_SAVE_INT(test_concurrent) + D_SAVE_STRING(test_url) + + // Style + + NekoRay::dataStore->language = ui->language->currentIndex(); + + // Subscription + + NekoRay::dataStore->user_agent = ui->user_agent->text(); + NekoRay::dataStore->sub_use_proxy = ui->sub_use_proxy->isChecked(); + + // Core + + NekoRay::dataStore->v2ray_asset_dir = ui->core_v2ray_asset->text(); + NekoRay::dataStore->extraCore->core_map = QJsonObject2QString(CACHE.extraCore, true); + + // Security + + NekoRay::dataStore->insecure_hint = ui->insecure_hint->isChecked(); + NekoRay::dataStore->skip_cert = ui->skip_cert->isChecked(); + + dialog_message(Dialog_DialogBasicSettings, "UpdateDataStore"); + QDialog::accept(); +} diff --git a/ui/dialog_basic_settings.h b/ui/dialog_basic_settings.h new file mode 100644 index 0000000..a6c69f1 --- /dev/null +++ b/ui/dialog_basic_settings.h @@ -0,0 +1,33 @@ +#ifndef DIALOG_BASIC_SETTINGS_H +#define DIALOG_BASIC_SETTINGS_H + +#include +#include + +namespace Ui { + class DialogBasicSettings; +} + +class DialogBasicSettings : public QDialog { +Q_OBJECT + +public: + explicit DialogBasicSettings(QWidget *parent = nullptr); + + ~DialogBasicSettings(); + +public slots: + + void accept(); + +private: + Ui::DialogBasicSettings *ui; + + struct { + QJsonObject extraCore; + QString custom_inbound; + bool needRestart = false; + } CACHE; +}; + +#endif // DIALOG_BASIC_SETTINGS_H diff --git a/ui/dialog_basic_settings.ui b/ui/dialog_basic_settings.ui new file mode 100644 index 0000000..660d015 --- /dev/null +++ b/ui/dialog_basic_settings.ui @@ -0,0 +1,593 @@ + + + DialogBasicSettings + + + + 0 + 0 + 500 + 400 + + + + + 0 + 0 + + + + Basic Settings + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + 0 + + + + Common + + + + + + + + + + + Listen Address + + + + + + + + + + + + + + + + Custom Inbound + + + + + + + Edit + + + + + + + + + + + + + + + + + Socks Listen Port + + + + + + + + + + + + + + + + HTTP Listen Port + + + + + + + + + + Enable + + + + + + + + + + + + + + + Test URL + + + + + + + + + + Concurrent + + + + + + + + + + + + + + + + + + Loglevel + + + + + + + + debug + + + + + info + + + + + warning + + + + + none + + + + + + + + + + + + + + mux.cool + + + + + + + Enable + + + + + + + concurrency + + + + + + + + + + + + + + + + + + + + Traffic statistics refresh rate + + + + + + + + Fast + + + + + Slow + + + + + Off + + + + + + + + + + + + + + Connection statistics + + + + + + + Enable + + + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + Style + + + + + + + + + 0 + 0 + + + + Theme + + + + + + + + System + + + + + flatgray + + + + + lightblue + + + + + blacksoft + + + + + + + + + + + + + 0 + 0 + + + + Language + + + + + + + + System + + + + + English + + + + + 简体中文 + + + + + + + + + + + Subscription + + + + + + + + + User Agent + + + + + + + Use proxy when updating subscription + + + + + + + + Core + + + + + + V2Ray + + + + + + + + Asset Location + + + + + + + Default: dir of "nekoray-core" + + + + + + + Select + + + + + + + + + + + + Extra Core + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Add + + + + + + + Delete + + + + + + + + + + + + + + Security + + + + + + Insecure hint + + + + + + + Skip TLS certificate authentication by default + + + + + + + + + + + + MyLineEdit + QLineEdit +

ui/widget/MyLineEdit.h
+ + + + tabWidget + socks_ip + custom_inbound_edit + inbound_socks_port + inbound_http_port + http_enable + test_url + test_concurrent + log_level + mux_cool_enable + mux_cool + rfsh_r + connection_statistics + theme + language + user_agent + sub_use_proxy + core_v2ray_asset + core_v2ray_asset_pick + extra_core_add + extra_core_del + insecure_hint + skip_cert + + + + + buttonBox + accepted() + DialogBasicSettings + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + DialogBasicSettings + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ui/dialog_hotkey.cpp b/ui/dialog_hotkey.cpp new file mode 100644 index 0000000..ea3e31e --- /dev/null +++ b/ui/dialog_hotkey.cpp @@ -0,0 +1,24 @@ +#include "dialog_hotkey.h" +#include "ui_dialog_hotkey.h" + +#include "ui/mainwindow.h" + +DialogHotkey::DialogHotkey(QWidget *parent) : + QDialog(parent), ui(new Ui::DialogHotkey) { + ui->setupUi(this); + ui->show_mainwindow->setKeySequence(NekoRay::dataStore->hotkey_mainwindow); + ui->show_groups->setKeySequence(NekoRay::dataStore->hotkey_group); + ui->show_routes->setKeySequence(NekoRay::dataStore->hotkey_route); + GetMainWindow()->RegisterHotkey(true); +} + +DialogHotkey::~DialogHotkey() { + if (result() == QDialog::Accepted) { + NekoRay::dataStore->hotkey_mainwindow = ui->show_mainwindow->keySequence().toString(); + NekoRay::dataStore->hotkey_group = ui->show_groups->keySequence().toString(); + NekoRay::dataStore->hotkey_route = ui->show_routes->keySequence().toString(); + NekoRay::dataStore->Save(); + } + GetMainWindow()->RegisterHotkey(false); + delete ui; +} diff --git a/ui/dialog_hotkey.h b/ui/dialog_hotkey.h new file mode 100644 index 0000000..6942a98 --- /dev/null +++ b/ui/dialog_hotkey.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include "main/NekoRay.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class DialogHotkey; } +QT_END_NAMESPACE + +class DialogHotkey : public QDialog { +Q_OBJECT + +public: + explicit DialogHotkey(QWidget *parent = nullptr); + + ~DialogHotkey() override; + +private: + Ui::DialogHotkey *ui; +}; diff --git a/ui/dialog_hotkey.ui b/ui/dialog_hotkey.ui new file mode 100644 index 0000000..ba055b0 --- /dev/null +++ b/ui/dialog_hotkey.ui @@ -0,0 +1,107 @@ + + + DialogHotkey + + + + 0 + 0 + 400 + 300 + + + + Hot key + + + + + + Trigger main window + + + + + + + + + + Qt::StrongFocus + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Show routes + + + + + + + + + + + + + Show groups + + + + + + + + QtExtKeySequenceEdit + QKeySequenceEdit +
3rdparty/QtExtKeySequenceEdit.h
+
+
+ + buttonBox + show_mainwindow + show_groups + show_routes + + + + + buttonBox + accepted() + DialogHotkey + accept() + + + 258 + 255 + + + 199 + 149 + + + + + buttonBox + rejected() + DialogHotkey + reject() + + + 258 + 255 + + + 199 + 149 + + + + +
diff --git a/ui/dialog_manage_groups.cpp b/ui/dialog_manage_groups.cpp new file mode 100644 index 0000000..d424be6 --- /dev/null +++ b/ui/dialog_manage_groups.cpp @@ -0,0 +1,97 @@ +#include "dialog_manage_groups.h" +#include "ui_dialog_manage_groups.h" + +#include "db/Database.hpp" +#include "sub/GroupUpdater.hpp" +#include "main/GuiUtils.hpp" +#include "ui/widget/GroupItem.h" +#include "ui/edit/dialog_edit_group.h" + +#include +#include +#include + +#define AddGroupToListIfExist(_id) \ +auto __ent = NekoRay::profileManager->GetGroup(_id); \ +if (__ent != nullptr) { \ +auto wI = new QListWidgetItem(); \ +auto w = new GroupItem(this, __ent, wI); \ +wI->setData(114514, _id); \ +ui->listWidget->addItem(wI); \ +ui->listWidget->setItemWidget(wI, w); \ +} + +DialogManageGroups::DialogManageGroups(QWidget *parent) : + QDialog(parent), ui(new Ui::DialogManageGroups) { + ui->setupUi(this); + + for (auto id: NekoRay::profileManager->_groups) { + AddGroupToListIfExist(id) + } + + connect(ui->listWidget, &QListWidget::itemDoubleClicked, this, [=](QListWidgetItem *wI) { + auto w = dynamic_cast(ui->listWidget->itemWidget(wI)); + emit w->edit_clicked(); + }); +} + +DialogManageGroups::~DialogManageGroups() { + delete ui; +} + +void DialogManageGroups::on_add_clicked() { + auto ent = NekoRay::ProfileManager::NewGroup(); + auto dialog = new DialogEditGroup(ent, this); + int ret = dialog->exec(); + dialog->deleteLater(); + + if (ret == QDialog::Accepted) { + NekoRay::profileManager->AddGroup(ent); + AddGroupToListIfExist(ent->id) + dialog_message(Dialog_DialogManageGroups, "refresh-1"); + } +} + +void DialogManageGroups::on_update_all_clicked() { + if (QMessageBox::question(this, tr("Confirmation"), tr("Update all subscriptions?")) + == QMessageBox::StandardButton::Yes) { + for (const auto &gid: NekoRay::profileManager->_groups) { + auto group = NekoRay::profileManager->GetGroup(gid); + if (group == nullptr || group->url.isEmpty()) continue; + _update_one_group(NekoRay::profileManager->_groups.indexOf(gid)); + return; + } + } +} + +void DialogManageGroups::_update_one_group(int _order) { + // calculate next group + int nextOrder = _order; + QSharedPointer nextGroup; + forever { + nextOrder += 1; + if (nextOrder >= NekoRay::profileManager->_groups.length()) break; + auto nextGid = NekoRay::profileManager->_groups[nextOrder]; + nextGroup = NekoRay::profileManager->GetGroup(nextGid); + if (nextGroup == nullptr || nextGroup->url.isEmpty()) continue; + break; + } + + // calculate this group + auto group = NekoRay::profileManager->GetGroup(NekoRay::profileManager->_groups[_order]); + if (group == nullptr) return; + + NekoRay::sub::groupUpdater->AsyncUpdate(group->url, group->id, this, [=] { + // refresh ui + for (int i = 0; i < ui->listWidget->count(); i++) { + auto w = ui->listWidget->itemWidget(ui->listWidget->item(i)); + if (w == nullptr) return; + auto item = dynamic_cast(w); + if (item->ent->id == group->id) { + item->refresh_data(); + } + } + // + if (nextGroup != nullptr) _update_one_group(nextOrder); + }); +} diff --git a/ui/dialog_manage_groups.h b/ui/dialog_manage_groups.h new file mode 100644 index 0000000..01baedb --- /dev/null +++ b/ui/dialog_manage_groups.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +#include "db/Group.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class DialogManageGroups; } +QT_END_NAMESPACE + +class DialogManageGroups : public QDialog { +Q_OBJECT + +public: + explicit DialogManageGroups(QWidget *parent = nullptr); + + ~DialogManageGroups() override; + +private: + Ui::DialogManageGroups *ui; + + void _update_one_group(int _order); + +private slots: + + void on_add_clicked(); + + void on_update_all_clicked(); +}; diff --git a/ui/dialog_manage_groups.ui b/ui/dialog_manage_groups.ui new file mode 100644 index 0000000..dcba1ff --- /dev/null +++ b/ui/dialog_manage_groups.ui @@ -0,0 +1,55 @@ + + + DialogManageGroups + + + + 0 + 0 + 640 + 480 + + + + Qt::TabFocus + + + Groups + + + + + + Qt::NoFocus + + + + + + + + + Qt::NoFocus + + + New group + + + + + + + Qt::NoFocus + + + Update all subscriptions + + + + + + + + + + diff --git a/ui/dialog_manage_routes.cpp b/ui/dialog_manage_routes.cpp new file mode 100644 index 0000000..f53534f --- /dev/null +++ b/ui/dialog_manage_routes.cpp @@ -0,0 +1,207 @@ +#include "dialog_manage_routes.h" +#include "ui_dialog_manage_routes.h" + +#include "qv2ray/ui/widgets/editors/w_JsonEditor.hpp" +#include "main/GuiUtils.hpp" + +#include +#include +#include + +#define REFRESH_ACTIVE_ROUTING(a, r) \ +active_routing = a; \ +ui->active_routing->setText("[" + active_routing + "]"); \ +setWindowTitle(title_base + " [" + a + "]"); \ +SetRouteConfig(*r); + +#define SAVE_TO_ROUTING(r) \ +r->direct_ip = directIPTxt->toPlainText(); \ +r->direct_domain = directDomainTxt->toPlainText(); \ +r->proxy_ip = proxyIPTxt->toPlainText(); \ +r->proxy_domain = proxyDomainTxt->toPlainText(); \ +r->block_ip = blockIPTxt->toPlainText(); \ +r->block_domain = blockDomainTxt->toPlainText(); \ +r->custom = CACHE.custom_route; + +DialogManageRoutes::DialogManageRoutes(QWidget *parent) : + QDialog(parent), ui(new Ui::DialogManageRoutes) { + ui->setupUi(this); + title_base = windowTitle(); + + ui->sniffing_mode->setCurrentIndex(NekoRay::dataStore->sniffing_mode); + ui->outbound_domain_strategy->setCurrentText(NekoRay::dataStore->outbound_domain_strategy); + ui->domainMatcherCombo->setCurrentIndex(NekoRay::dataStore->domain_matcher); + ui->domainStrategyCombo->setCurrentText(NekoRay::dataStore->domain_strategy); + ui->fake_dns->setChecked(NekoRay::dataStore->fake_dns); + ui->dns_routing->setChecked(NekoRay::dataStore->dns_routing); + ui->dns_remote->setText(NekoRay::dataStore->remote_dns); + ui->dns_direct->setText(NekoRay::dataStore->direct_dns); + ui->enhance_resolve_server_domain->setChecked(NekoRay::dataStore->enhance_resolve_server_domain); + D_C_LOAD_STRING(custom_route_global) + + connect(ui->custom_route_edit, &QPushButton::clicked, this, [=] { + C_EDIT_JSON_ALLOW_EMPTY(custom_route) + }); + connect(ui->custom_route_global_edit, &QPushButton::clicked, this, [=] { + C_EDIT_JSON_ALLOW_EMPTY(custom_route_global) + }); + + // + builtInSchemesMenu = new QMenu(this); + builtInSchemesMenu->addActions(this->getBuiltInSchemes()); + ui->preset->setMenu(builtInSchemesMenu); + + // + directDomainTxt = new AutoCompleteTextEdit("geosite", {}, this); + proxyDomainTxt = new AutoCompleteTextEdit("geosite", {}, this); + blockDomainTxt = new AutoCompleteTextEdit("geosite", {}, this); + // + directIPTxt = new AutoCompleteTextEdit("geoip", {}, this); + proxyIPTxt = new AutoCompleteTextEdit("geoip", {}, this); + blockIPTxt = new AutoCompleteTextEdit("geoip", {}, this); + // + ui->directTxtLayout->addWidget(directDomainTxt, 0, 0); + ui->proxyTxtLayout->addWidget(proxyDomainTxt, 0, 0); + ui->blockTxtLayout->addWidget(blockDomainTxt, 0, 0); + // + ui->directIPLayout->addWidget(directIPTxt, 0, 0); + ui->proxyIPLayout->addWidget(proxyIPTxt, 0, 0); + ui->blockIPLayout->addWidget(blockIPTxt, 0, 0); + // + REFRESH_ACTIVE_ROUTING(NekoRay::dataStore->active_routing, NekoRay::dataStore->routing) +} + +DialogManageRoutes::~DialogManageRoutes() { + delete ui; +} + +void DialogManageRoutes::accept() { + NekoRay::dataStore->sniffing_mode = ui->sniffing_mode->currentIndex(); + NekoRay::dataStore->domain_matcher = ui->domainMatcherCombo->currentIndex(); + NekoRay::dataStore->domain_strategy = ui->domainStrategyCombo->currentText(); + NekoRay::dataStore->outbound_domain_strategy = ui->outbound_domain_strategy->currentText(); + NekoRay::dataStore->dns_routing = ui->dns_routing->isChecked(); + NekoRay::dataStore->fake_dns = ui->fake_dns->isChecked(); + NekoRay::dataStore->remote_dns = ui->dns_remote->text(); + NekoRay::dataStore->direct_dns = ui->dns_direct->text(); + NekoRay::dataStore->enhance_resolve_server_domain = ui->enhance_resolve_server_domain->isChecked(); + D_C_SAVE_STRING(custom_route_global) + + // + SAVE_TO_ROUTING(NekoRay::dataStore->routing) + NekoRay::dataStore->active_routing = active_routing; + NekoRay::dataStore->routing->fn = "routes/" + NekoRay::dataStore->active_routing; + NekoRay::dataStore->routing->Save(); + + dialog_message(Dialog_DialogManageRoutes, "UpdateDataStore"); + QDialog::accept(); +} + +// built in settings + +QList DialogManageRoutes::getBuiltInSchemes() { + QList list; + list.append(this->schemeToAction(tr("Bypass LAN and China"), routing_cn_lan)); + list.append(this->schemeToAction(tr("Global"), routing_global)); + return list; +} + +QAction *DialogManageRoutes::schemeToAction(const QString &name, const NekoRay::Routing &scheme) { + auto *action = new QAction(this); + action->setText(name); + connect(action, &QAction::triggered, [this, &scheme] { this->SetRouteConfig(scheme); }); + return action; +} + +void DialogManageRoutes::SetRouteConfig(const NekoRay::Routing &conf) { + // + directDomainTxt->setPlainText(conf.direct_domain); + proxyDomainTxt->setPlainText(conf.proxy_domain); + blockDomainTxt->setPlainText(conf.block_domain); + // + blockIPTxt->setPlainText(conf.block_ip); + directIPTxt->setPlainText(conf.direct_ip); + proxyIPTxt->setPlainText(conf.proxy_ip); + // + CACHE.custom_route = conf.custom; +} + +void DialogManageRoutes::on_load_save_clicked() { + auto w = new QDialog; + auto layout = new QVBoxLayout; + w->setLayout(layout); + auto lineEdit = new QLineEdit; + layout->addWidget(lineEdit); + auto list = new QListWidget; + layout->addWidget(list); + for (const auto &name: NekoRay::Routing::List()) { + list->addItem(name); + } + connect(list, &QListWidget::currentTextChanged, lineEdit, &QLineEdit::setText); + auto bottom = new QHBoxLayout; + layout->addLayout(bottom); + auto load = new QPushButton; + load->setText(tr("Load")); + bottom->addWidget(load); + auto save = new QPushButton; + save->setText(tr("Save")); + bottom->addWidget(save); + auto remove = new QPushButton; + remove->setText(tr("Remove")); + bottom->addWidget(remove); + auto cancel = new QPushButton; + cancel->setText(tr("Cancel")); + bottom->addWidget(cancel); + connect(load, &QPushButton::clicked, w, [=] { + auto fn = lineEdit->text(); + if (!fn.isEmpty()) { + auto r = std::make_unique(); + r->load_control_force = true; + r->fn = "routes/" + fn; + if (r->Load()) { + auto btn = QMessageBox::question(nullptr, + "NekoRay", tr("Load routing: %1").arg(fn) + "\n" + r->toString()); + if (btn == QMessageBox::Yes) { + REFRESH_ACTIVE_ROUTING(fn, r) + w->accept(); + } + } + } + }); + connect(save, &QPushButton::clicked, w, [=] { + auto fn = lineEdit->text(); + if (!fn.isEmpty()) { + auto r = std::make_unique(); + SAVE_TO_ROUTING(r) + r->fn = "routes/" + fn; + auto btn = QMessageBox::question(nullptr, "NekoRay", tr("Save routing: %1").arg(fn) + "\n" + r->toString()); + if (btn == QMessageBox::Yes) { + r->Save(); + REFRESH_ACTIVE_ROUTING(fn, r) + w->accept(); + } + } + }); + connect(remove, &QPushButton::clicked, w, [=] { + auto fn = lineEdit->text(); + if (!fn.isEmpty() && NekoRay::Routing::List().length() > 1) { + auto btn = QMessageBox::question(nullptr, "NekoRay", tr("Remove routing: %1").arg(fn)); + if (btn == QMessageBox::Yes) { + QFile f("routes/" + fn); + f.remove(); + if (NekoRay::dataStore->active_routing == fn) { + NekoRay::Routing::SetToActive(NekoRay::Routing::List().first()); + REFRESH_ACTIVE_ROUTING(NekoRay::dataStore->active_routing, NekoRay::dataStore->routing) + } + w->accept(); + } + } + }); + connect(cancel, &QPushButton::clicked, w, &QDialog::accept); + connect(list, &QListWidget::itemDoubleClicked, this, [=](QListWidgetItem *item) { + lineEdit->setText(item->text()); + emit load->clicked(); + }); + w->exec(); + w->deleteLater(); +} diff --git a/ui/dialog_manage_routes.h b/ui/dialog_manage_routes.h new file mode 100644 index 0000000..a07eb5e --- /dev/null +++ b/ui/dialog_manage_routes.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +#include "qv2ray/ui/QvAutoCompleteTextEdit.hpp" +#include "main/NekoRay.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class DialogManageRoutes; } +QT_END_NAMESPACE + +class DialogManageRoutes : public QDialog { +Q_OBJECT + +public: + explicit DialogManageRoutes(QWidget *parent = nullptr); + + ~DialogManageRoutes() override; + +private: + Ui::DialogManageRoutes *ui; + + struct { + QString custom_route; + QString custom_route_global; + } CACHE; + + + QMenu *builtInSchemesMenu; + Qv2ray::ui::widgets::AutoCompleteTextEdit *directDomainTxt; + Qv2ray::ui::widgets::AutoCompleteTextEdit *proxyDomainTxt; + Qv2ray::ui::widgets::AutoCompleteTextEdit *blockDomainTxt; + // + Qv2ray::ui::widgets::AutoCompleteTextEdit *directIPTxt; + Qv2ray::ui::widgets::AutoCompleteTextEdit *blockIPTxt; + Qv2ray::ui::widgets::AutoCompleteTextEdit *proxyIPTxt; + // + NekoRay::Routing routing_cn_lan = NekoRay::Routing(1); + NekoRay::Routing routing_global = NekoRay::Routing(0); + // + QString title_base; + QString active_routing; +public slots: + + void accept() override; + + QList getBuiltInSchemes(); + + QAction *schemeToAction(const QString &name, const NekoRay::Routing &scheme); + + void SetRouteConfig(const NekoRay::Routing &conf); + + void on_load_save_clicked(); +}; + + diff --git a/ui/dialog_manage_routes.ui b/ui/dialog_manage_routes.ui new file mode 100644 index 0000000..acc1acf --- /dev/null +++ b/ui/dialog_manage_routes.ui @@ -0,0 +1,392 @@ + + + DialogManageRoutes + + + + 0 + 0 + 800 + 600 + + + + Routes + + + + + + + + + + Outbound Domain Strategy + + + + + + + + Disable + + + + + The sniffing result is used for routing + + + + + The sniffing result is used for destination + + + + + + + + Direct DNS + + + + + + + Sniffing Mode + + + + + + + Remote DNS + + + + + + + false + + + + AsIs + + + + + UseIP + + + + + UseIPv4 + + + + + UseIPv6 + + + + + PreferIPv4 + + + + + PreferIPv6 + + + + + + + + + + + + + 使用多个境外 DNS 查询服务器地址,一定程度上可缓解对服务器域名的 DNS 污染,可能有副作用。 + + + 增强域名解析 + + + + + + + + + + + + + + FakeDNS + + + + + + + + + + + + + + + Enable DNS Routing + + + + + + + Qt::Vertical + + + + + + + Domain Strategy + + + + + + + false + + + + AsIs + + + + + IPIfNonMatch + + + + + IPOnDemand + + + + + + + + Matcher + + + + + + + + Original + + + + + Minimal Perfect Hash Matcher + + + + + + + + + + + + Block + + + Qt::AlignCenter + + + + + + + + + + Direct + + + Qt::AlignCenter + + + + + + + Domain + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + Proxy + + + Qt::AlignCenter + + + + + + + + + + + + + IP + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + + + + + + + + Preset + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextBesideIcon + + + + + + + Custom + + + + + + + Custom (global) + + + + + + + Mange route set + + + + + + + + 0 + 0 + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + sniffing_mode + outbound_domain_strategy + dns_remote + fake_dns + dns_direct + enhance_resolve_server_domain + dns_routing + domainStrategyCombo + domainMatcherCombo + preset + custom_route_edit + + + + + buttonBox + accepted() + DialogManageRoutes + accept() + + + 399 + 574 + + + 399 + 299 + + + + + buttonBox + rejected() + DialogManageRoutes + reject() + + + 399 + 574 + + + 399 + 299 + + + + + diff --git a/ui/edit/dialog_edit_group.cpp b/ui/edit/dialog_edit_group.cpp new file mode 100644 index 0000000..b74cc19 --- /dev/null +++ b/ui/edit/dialog_edit_group.cpp @@ -0,0 +1,66 @@ +#include "dialog_edit_group.h" +#include "ui_dialog_edit_group.h" + +#include "db/Database.hpp" + +#include + +DialogEditGroup::DialogEditGroup(const QSharedPointer &ent, QWidget *parent) : + QDialog(parent), ui(new Ui::DialogEditGroup) { + ui->setupUi(this); + + connect(ui->type, QOverload::of(&QComboBox::currentIndexChanged), this, [=](int index) { + ui->cat_sub->setHidden(index == 0); + }); + + ui->name->setText(ent->name); + ui->archive->setChecked(ent->archive); + ui->url->setText(ent->url); + ui->type->setCurrentIndex(ent->url.isEmpty() ? 0 : 1); + ui->type->currentIndexChanged(ui->type->currentIndex()); + ui->copy_links->setVisible(false); + ui->copy_links_nkr->setVisible(false); + if (ent->id >= 0) { // already a group + ui->type->setDisabled(true); + if (!ent->Profiles().isEmpty()) { + ui->copy_links->setVisible(true); + ui->copy_links_nkr->setVisible(true); + } + } + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, [=] { + if (ent->id >= 0) { // already a group + if (!ent->url.isEmpty() && ui->url->text().isEmpty()) { + MessageBoxWarning(tr("Warning"), tr("Please input URL")); + return; + } + } + ent->name = ui->name->text(); + ent->url = ui->url->text(); + ent->archive = ui->archive->isChecked(); + QDialog::accept(); + }); + + connect(ui->copy_links, &QPushButton::clicked, this, [=] { + QStringList links; + for (const auto &profile: NekoRay::profileManager->profiles) { + if (profile->gid != ent->id) continue; + links += profile->bean->ToShareLink(); + } + QApplication::clipboard()->setText(links.join("\n")); + MessageBoxInfo("NekoRay", tr("Copied")); + }); + connect(ui->copy_links_nkr, &QPushButton::clicked, this, [=] { + QStringList links; + for (const auto &profile: NekoRay::profileManager->profiles) { + if (profile->gid != ent->id) continue; + links += profile->bean->ToNekorayShareLink(profile->type); + } + QApplication::clipboard()->setText(links.join("\n")); + MessageBoxInfo("NekoRay", tr("Copied")); + }); +} + +DialogEditGroup::~DialogEditGroup() { + delete ui; +} diff --git a/ui/edit/dialog_edit_group.h b/ui/edit/dialog_edit_group.h new file mode 100644 index 0000000..b7589bf --- /dev/null +++ b/ui/edit/dialog_edit_group.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include "db/Group.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class DialogEditGroup; } +QT_END_NAMESPACE + +class DialogEditGroup : public QDialog { +Q_OBJECT + +public: + explicit DialogEditGroup(const QSharedPointer &ent, QWidget *parent = nullptr); + + ~DialogEditGroup() override; + +private: + Ui::DialogEditGroup *ui; +}; + + diff --git a/ui/edit/dialog_edit_group.ui b/ui/edit/dialog_edit_group.ui new file mode 100644 index 0000000..8bd020a --- /dev/null +++ b/ui/edit/dialog_edit_group.ui @@ -0,0 +1,180 @@ + + + DialogEditGroup + + + + 0 + 0 + 400 + 300 + + + + Edit Group + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Type + + + + + + + + + + Name + + + + + + + + + + Basic + + + + + Subscription + + + + + + + + Archive + + + + + + + + + + + + + 0 + 0 + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + URL + + + + + + + + + + + + + + + + + Copy profile share links + + + + + + + Copy profile share links (Nekoray) + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + + + buttonBox + rejected() + DialogEditGroup + reject() + + + 199 + 275 + + + 199 + 149 + + + + +
diff --git a/ui/edit/dialog_edit_profile.cpp b/ui/edit/dialog_edit_profile.cpp new file mode 100644 index 0000000..601b5fb --- /dev/null +++ b/ui/edit/dialog_edit_profile.cpp @@ -0,0 +1,298 @@ +#include "dialog_edit_profile.h" +#include "ui_dialog_edit_profile.h" + +#include "ui/edit/edit_socks_http.h" +#include "ui/edit/edit_shadowsocks.h" +#include "ui/edit/edit_chain.h" +#include "ui/edit/edit_vmess.h" +#include "ui/edit/edit_trojan_vless.h" +#include "ui/edit/edit_naive.h" +#include "ui/edit/edit_custom.h" + +#include "fmt/includes.h" +#include "qv2ray/ui/widgets/editors/w_JsonEditor.hpp" +#include "main/GuiUtils.hpp" + +#include + +#define ADJUST_SIZE runOnUiThread([=] { adjustSize(); adjustPosition(mainwindow); }, this); +#define LOAD_TYPE(a) ui->type->addItem(NekoRay::ProfileManager::NewProxyEntity(a)->bean->DisplayType(), a); + +DialogEditProfile::DialogEditProfile(const QString &_type, int profileOrGroupId, QWidget *parent) + : QDialog(parent), + ui(new Ui::DialogEditProfile) { + // setup UI + ui->setupUi(this); + ui->dialog_layout->setAlignment(ui->left, Qt::AlignTop); + ui->dialog_layout->setAlignment(ui->right_all, Qt::AlignTop); + + // network changed + network_title_base = ui->network_box->title(); + connect(ui->network, &QComboBox::currentTextChanged, this, [=](const QString &txt) { + if (txt == "tcp" || txt == "quic") { + ui->network_box->setVisible(false); + } else { + ui->network_box->setVisible(true); + ui->network_box->setTitle(network_title_base.arg(txt)); + if (txt == "grpc") { + ui->host->setVisible(false); + ui->host_l->setVisible(false); + } else { + ui->host->setVisible(true); + ui->host_l->setVisible(true); + } + } + ADJUST_SIZE + }); + ui->network->removeItem(0); + + // security changed + connect(ui->security, &QComboBox::currentTextChanged, this, [=](const QString &txt) { + if (txt == "tls") { + ui->security_box->setVisible(true); + } else { + ui->security_box->setVisible(false); + } + ADJUST_SIZE + }); + ui->security->removeItem(0); + + // 确定模式和 ent + newEnt = _type != ""; + if (newEnt) { + this->groupId = profileOrGroupId; + this->type = _type; + + // load type to combo box + LOAD_TYPE("socks") + LOAD_TYPE("http") + LOAD_TYPE("shadowsocks"); + LOAD_TYPE("trojan"); + LOAD_TYPE("vmess"); + LOAD_TYPE("vless"); + LOAD_TYPE("naive"); + ui->type->addItem("Hysteria", "hysteria"); + ui->type->addItem(tr("Custom"), "custom"); + LOAD_TYPE("chain"); + + // type changed + connect(ui->type, QOverload::of(&QComboBox::currentIndexChanged), this, [=](int index) { + typeSelected(ui->type->itemData(index).toString()); + }); + } else { + this->ent = NekoRay::profileManager->GetProfile(profileOrGroupId); + if (this->ent == nullptr) return; + this->type = ent->type; + ui->type->setVisible(false); + ui->type_l->setVisible(false); + } + + typeSelected(this->type); +} + +DialogEditProfile::~DialogEditProfile() { + delete ui; +} + +void DialogEditProfile::typeSelected(const QString &newType) { + type = newType; + bool validType = true; + + if (type == "socks" || type == "http") { + auto _innerWidget = new EditSocksHttp(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + } else if (type == "shadowsocks") { + auto _innerWidget = new EditShadowSocks(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + } else if (type == "chain") { + auto _innerWidget = new EditChain(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + } else if (type == "vmess") { + auto _innerWidget = new EditVMess(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + } else if (type == "trojan" || type == "vless") { + auto _innerWidget = new EditTrojanVLESS(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + } else if (type == "naive") { + auto _innerWidget = new EditNaive(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + } else if (type == "custom" || type == "hysteria") { + auto _innerWidget = new EditCustom(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + // I don't want to write a settings + if (type == "hysteria") { + _innerWidget->preset_core = type; + _innerWidget->preset_command = "-c %config%"; + _innerWidget->preset_config = "{\n" + " \"server\": \"127.0.0.1:%mapping_port%\",\n" + " \"obfs\": \"fuck me till the daylight\",\n" + " \"up_mbps\": 10,\n" + " \"down_mbps\": 50,\n" + " \"server_name\": \"real.name.com\",\n" + " \"socks5\": {\n" + " \"listen\": \"127.0.0.1:%socks_port%\"\n" + " }\n" + "}"; + type = "custom"; + } + } else { + validType = false; + } + + if (!validType) { + MessageBoxWarning(newType, "Wrong type"); + return; + } + + if (newEnt) { + this->ent = NekoRay::ProfileManager::NewProxyEntity(type); + this->ent->gid = groupId; + } + + // hide some widget + auto notChain = type != "chain"; + ui->address->setVisible(notChain); + ui->address_l->setVisible(notChain); + ui->port->setVisible(notChain); + ui->port_l->setVisible(notChain); + + // 右边 Outbound: settings + auto stream = GetStreamSettings(ent->bean); + if (stream != nullptr) { + ui->right_all_w->setVisible(true); + ui->network->setCurrentText(stream->network); + ui->security->setCurrentText(stream->security); + ui->packet_encoding->setCurrentText(stream->packet_encoding); + ui->path->setText(stream->path); + ui->host->setText(stream->host); + ui->sni->setText(stream->sni); + ui->insecure->setChecked(stream->allow_insecure); + CACHE.certificate = stream->certificate; + } else { + ui->right_all_w->setVisible(false); + } + auto custom_item = ent->bean->_get("custom"); + if (custom_item != nullptr) { + ui->custom_box->setVisible(true); + CACHE.custom = *((QString *) custom_item->ptr); + } else { + ui->custom_box->setVisible(false); + } + + // 左边 bean + auto old = ui->bean->layout()->itemAt(0)->widget(); + ui->bean->layout()->removeWidget(old); + innerWidget->layout()->setContentsMargins(0, 0, 0, 0); + ui->bean->layout()->addWidget(innerWidget); + ui->bean->setTitle(ent->bean->DisplayType()); + delete old; + + // 左边 bean inner editor + innerEditor->get_edit_dialog = [&]() { + return (QWidget *) this; + }; + innerEditor->editor_cache_updated = [=] { + editor_cache_updated_impl(); + }; + innerEditor->onStart(ent); + + // 左边 common + ui->name->setText(ent->bean->name); + ui->address->setText(ent->bean->serverAddress); + ui->port->setText(Int2String(ent->bean->serverPort)); + ui->port->setValidator(QRegExpValidator_Number, this)); + + editor_cache_updated_impl(); + ADJUST_SIZE + + // 第一次显示 + if (isHidden()) { + show(); + } +} + +void DialogEditProfile::accept() { + // 左边 + ent->bean->name = ui->name->text(); + ent->bean->serverAddress = ui->address->text(); + ent->bean->serverPort = ui->port->text().toInt(); + + // bean + if (!innerEditor->onEnd()) { + return; + } + + // 右边 + auto stream = GetStreamSettings(ent->bean); + if (stream != nullptr) { + stream->network = ui->network->currentText(); + stream->security = ui->security->currentText(); + stream->packet_encoding = ui->packet_encoding->currentText(); + stream->path = ui->path->text(); + stream->host = ui->host->text(); + stream->sni = ui->sni->text(); + stream->allow_insecure = ui->insecure->isChecked(); + stream->certificate = CACHE.certificate; + } + auto custom_item = ent->bean->_get("custom"); + if (custom_item != nullptr) { + *((QString *) custom_item->ptr) = CACHE.custom; + } + + if (newEnt) { + auto ok = NekoRay::profileManager->AddProfile(ent); + if (!ok) { + MessageBoxWarning("???", "id exists"); + } + } else { + ent->Save(); + } + + dialog_message(Dialog_DialogEditProfile, "accept"); + QDialog::accept(); +} + +// cached editor (dialog) + +void DialogEditProfile::editor_cache_updated_impl() { + if (CACHE.certificate.isEmpty()) { + ui->certificate_edit->setText(tr("Not set")); + } else { + ui->certificate_edit->setText(tr("Already set")); + } + if (CACHE.custom.isEmpty()) { + ui->custom_edit->setText(tr("Not set")); + } else { + ui->custom_edit->setText(tr("Already set")); + } + + // CACHE macro + for (auto a: innerEditor->get_editor_cached()) { + if (a.second.isEmpty()) { + a.first->setText(tr("Not set")); + } else { + a.first->setText(tr("Already set")); + } + } +} + +void DialogEditProfile::on_custom_edit_clicked() { + C_EDIT_JSON_ALLOW_EMPTY(custom) + editor_cache_updated_impl(); +} + +void DialogEditProfile::on_certificate_edit_clicked() { + bool ok; + auto txt = QInputDialog::getMultiLineText(this, tr("Certificate"), "", CACHE.certificate, &ok); + if (ok) { + CACHE.certificate = txt; + editor_cache_updated_impl(); + } +} diff --git a/ui/edit/dialog_edit_profile.h b/ui/edit/dialog_edit_profile.h new file mode 100644 index 0000000..ee2a713 --- /dev/null +++ b/ui/edit/dialog_edit_profile.h @@ -0,0 +1,52 @@ +#ifndef DIALOG_EDIT_PROFILE_H +#define DIALOG_EDIT_PROFILE_H + +#include +#include "db/Database.hpp" +#include "profile_editor.h" + +namespace Ui { + class DialogEditProfile; +} + +class DialogEditProfile : public QDialog { +Q_OBJECT + +public: + explicit DialogEditProfile(const QString &_type, int profileOrGroupId, QWidget *parent = nullptr); + + ~DialogEditProfile() override; + +public slots: + + void accept() override; + +private slots: + + void on_custom_edit_clicked(); + + void on_certificate_edit_clicked(); + +private: + Ui::DialogEditProfile *ui; + QWidget *innerWidget{}; + ProfileEditor *innerEditor{}; + + QString type; + int groupId; + bool newEnt = false; + QSharedPointer ent; + + QString network_title_base; + + struct { + QString custom; + QString certificate; + } CACHE; + + void typeSelected(const QString &newType); + + void editor_cache_updated_impl(); +}; + +#endif // DIALOG_EDIT_PROFILE_H diff --git a/ui/edit/dialog_edit_profile.ui b/ui/edit/dialog_edit_profile.ui new file mode 100644 index 0000000..e801b96 --- /dev/null +++ b/ui/edit/dialog_edit_profile.ui @@ -0,0 +1,489 @@ + + + DialogEditProfile + + + + 0 + 0 + 1000 + 600 + + + + + 0 + 0 + + + + Edit + + + + + + + 0 + 0 + + + + + 400 + 0 + + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + Common + + + + + + + + + + + + + + + + + Type + + + + + + + Port + + + + + + + Address + + + + + + + Name + + + + + + + + + + + + + + + + 0 + 0 + + + + Bean + + + + + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + 0 + 0 + + + + + 400 + 0 + + + + + + + + 0 + 0 + + + + + QLayout::SetDefaultConstraint + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Settings + + + + + + + + + + + + + + none + + + + + tls + + + + + + + + 底层传输方式。必须与服务器一致,否则无法建立连接。 + + + Network + + + + + + + + 0 + 0 + + + + + + + + + + tcp + + + + + ws + + + + + h2 + + + + + grpc + + + + + quic + + + + + + + + 传输层安全。必须与服务器一致,否则无法建立连接。 + + + Security + + + + + + + 包编码,用于实现 UDP FullCone 等特性。需要服务器支持,选错无法连接。不懂请留空。 + + + Packet Encoding + + + + + + + + + + + + + packet + + + + + xudp + + + + + + + + + + + + + + 0 + 0 + + + + Network Settings (%1) + + + + + + http path (ws/http) 或 serviceName (gRPC) + + + Path + + + + + + + + + + http host + + + Host + + + + + + + + + + + + + + 0 + 0 + + + + Security Settings + + + + + + 开启后 V2Ray 不会检查远端主机所提供的 TLS 证书的有效性(安全性相当于明文) + + + Allow insecure + + + + + + + + + PushButton + + + + + + + 服务器名称指示,明文。 + + + SNI + + + + + + + 固定证书 + + + Certificate + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Custom Json Settings + + + + + + Edit + + + + + + + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + type + name + address + port + network + security + path + host + insecure + sni + certificate_edit + custom_edit + + + + + buttonBox + accepted() + DialogEditProfile + accept() + + + 151 + 500 + + + 299 + 299 + + + + + buttonBox + rejected() + DialogEditProfile + reject() + + + 151 + 500 + + + 299 + 299 + + + + +
diff --git a/ui/edit/edit_chain.cpp b/ui/edit/edit_chain.cpp new file mode 100644 index 0000000..04c93cc --- /dev/null +++ b/ui/edit/edit_chain.cpp @@ -0,0 +1,56 @@ +#include "edit_chain.h" +#include "ui_edit_chain.h" + +#include "ui/mainwindow.h" +#include "ui/widget/ProxyItem.h" + +#include "db/Database.hpp" +#include "fmt/ChainBean.hpp" + +EditChain::EditChain(QWidget *parent) : QWidget(parent), ui(new Ui::EditChain) { + ui->setupUi(this); +} + +EditChain::~EditChain() { + delete ui; +} + +void EditChain::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->ChainBean(); + + for (auto id: bean->list) { + AddProfileToListIfExist(id); + } +} + +bool EditChain::onEnd() { + auto bean = this->ent->ChainBean(); + + QList idList; + for (int i = 0; i < ui->listWidget->count(); i++) { + idList << ui->listWidget->item(i)->data(114514).toInt(); + } + bean->list = idList; + + return true; +} + +void EditChain::on_select_profile_clicked() { + get_edit_dialog()->hide(); + GetMainWindow()->start_select_mode(this, [=](int id) { + get_edit_dialog()->show(); + AddProfileToListIfExist(id); + }); +} + +void EditChain::AddProfileToListIfExist(int id) { + auto _ent = NekoRay::profileManager->GetProfile(id); + if (_ent != nullptr && _ent->type != "chain") { + auto wI = new QListWidgetItem(); + auto w = new ProxyItem(this, _ent, wI); + wI->setData(114514, id); + ui->listWidget->addItem(wI); + ui->listWidget->setItemWidget(wI, w); + } +} diff --git a/ui/edit/edit_chain.h b/ui/edit/edit_chain.h new file mode 100644 index 0000000..0b93ad6 --- /dev/null +++ b/ui/edit/edit_chain.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include "profile_editor.h" + +QT_BEGIN_NAMESPACE +namespace Ui { class EditChain; } +QT_END_NAMESPACE + +class EditChain : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + explicit EditChain(QWidget *parent = nullptr); + + ~EditChain() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + +private: + Ui::EditChain *ui; + QSharedPointer ent; + + void AddProfileToListIfExist(int id); + +private slots: + + void on_select_profile_clicked(); +}; diff --git a/ui/edit/edit_chain.ui b/ui/edit/edit_chain.ui new file mode 100644 index 0000000..49b9669 --- /dev/null +++ b/ui/edit/edit_chain.ui @@ -0,0 +1,57 @@ + + + EditChain + + + + 0 + 0 + 400 + 476 + + + + EditChain + + + + + + Traffic order is from top to bottom + + + + + + + + 0 + 400 + + + + Qt::ActionsContextMenu + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + QListView::Free + + + + + + + Select Profile + + + + + + + + diff --git a/ui/edit/edit_custom.cpp b/ui/edit/edit_custom.cpp new file mode 100644 index 0000000..63a7781 --- /dev/null +++ b/ui/edit/edit_custom.cpp @@ -0,0 +1,66 @@ +#include "edit_custom.h" +#include "ui_edit_custom.h" + +#include "qv2ray/ui/widgets/editors/w_JsonEditor.hpp" +#include "fmt/CustomBean.hpp" + +EditCustom::EditCustom(QWidget *parent) : + QWidget(parent), ui(new Ui::EditCustom) { + ui->setupUi(this); + ui->config_simple->setPlaceholderText("example:\n" + " server-address: \"127.0.0.1:%mapping_port%\"\n" + " listen-address: \"127.0.0.1\"\n" + " listen-port: %socks_port%\n" + " host: your-domain.com\n" + " sni: your-domain.com\n" + ); +} + +EditCustom::~EditCustom() { + delete ui; +} + +void EditCustom::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->CustomBean(); + + P_LOAD_COMBO(core) + ui->command->setText(bean->command.join(" ")); + P_LOAD_STRING(config_simple) + + // load known core + auto core_map = QString2QJsonObject(NekoRay::dataStore->extraCore->core_map); + for (const auto &key: core_map.keys()) { + ui->core->addItem(key); + } + + if (!preset_core.isEmpty()) { + bean->core = preset_core; + ui->core->setCurrentText(preset_core); + ui->core->setDisabled(true); + ui->command->setText(preset_command); + ui->config_simple->setText(preset_config); + } + if (!bean->core.isEmpty()) { + ui->core->setDisabled(true); + } + +} + +bool EditCustom::onEnd() { + auto bean = this->ent->CustomBean(); + + P_SAVE_COMBO(core) + bean->command = ui->command->text().split(" "); + P_SAVE_STRING_QTEXTEDIT(config_simple) + + return true; +} + +void EditCustom::on_as_json_clicked() { + auto editor = new JsonEditor(QString2QJsonObject(ui->config_simple->toPlainText()), this); + auto result = editor->OpenEditor(); + if (!result.isEmpty()) { + ui->config_simple->setText(QJsonObject2QString(result, false)); + } +} diff --git a/ui/edit/edit_custom.h b/ui/edit/edit_custom.h new file mode 100644 index 0000000..c7d7db1 --- /dev/null +++ b/ui/edit/edit_custom.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include "profile_editor.h" + + +QT_BEGIN_NAMESPACE +namespace Ui { class EditCustom; } +QT_END_NAMESPACE + +class EditCustom : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + QString preset_core; + QString preset_command; + QString preset_config; + + explicit EditCustom(QWidget *parent = nullptr); + + ~EditCustom() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + +private: + Ui::EditCustom *ui; + QSharedPointer ent; + +private slots: + + void on_as_json_clicked(); +}; diff --git a/ui/edit/edit_custom.ui b/ui/edit/edit_custom.ui new file mode 100644 index 0000000..3abd7eb --- /dev/null +++ b/ui/edit/edit_custom.ui @@ -0,0 +1,86 @@ + + + EditCustom + + + + 0 + 0 + 400 + 394 + + + + EditCustom + + + + + + + + + 0 + 0 + + + + Core + + + + + + + true + + + + + + + Json + + + + + + + + + + + Command + + + + + + + %config% + + + + + + + + + + 0 + 300 + + + + + + + + core + as_json + command + config_simple + + + + diff --git a/ui/edit/edit_naive.cpp b/ui/edit/edit_naive.cpp new file mode 100644 index 0000000..80660d5 --- /dev/null +++ b/ui/edit/edit_naive.cpp @@ -0,0 +1,67 @@ +#include "edit_naive.h" +#include "ui_edit_naive.h" + +#include "fmt/NaiveBean.hpp" + +#include + +EditNaive::EditNaive(QWidget *parent) : + QWidget(parent), ui(new Ui::EditNaive) { + ui->setupUi(this); +} + +EditNaive::~EditNaive() { + delete ui; +} + +void EditNaive::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->NaiveBean(); + + P_LOAD_STRING(username); + P_LOAD_STRING(password); + P_LOAD_COMBO(protocol); + P_C_LOAD_STRING(extra_headers); + P_LOAD_STRING(sni); + P_C_LOAD_STRING(certificate); + P_LOAD_INT(insecure_concurrency); +} + +bool EditNaive::onEnd() { + auto bean = this->ent->NaiveBean(); + + P_SAVE_STRING(username); + P_SAVE_STRING(password); + P_SAVE_COMBO(protocol); + P_C_SAVE_STRING(extra_headers); + P_SAVE_STRING(sni); + P_C_SAVE_STRING(certificate); + P_SAVE_INT(insecure_concurrency); + + return true; +} + +QList> EditNaive::get_editor_cached() { + return { + {ui->certificate, CACHE.certificate}, + {ui->extra_headers, CACHE.extra_headers}, + }; +} + +void EditNaive::on_certificate_clicked() { + bool ok; + auto txt = QInputDialog::getMultiLineText(this, tr("Certificate"), "", CACHE.certificate, &ok); + if (ok) { + CACHE.certificate = txt; + editor_cache_updated(); + } +} + +void EditNaive::on_extra_headers_clicked() { + bool ok; + auto txt = QInputDialog::getMultiLineText(this, tr("Extra headers"), "", CACHE.extra_headers, &ok); + if (ok) { + CACHE.extra_headers = txt; + editor_cache_updated(); + } +} diff --git a/ui/edit/edit_naive.h b/ui/edit/edit_naive.h new file mode 100644 index 0000000..874cdcd --- /dev/null +++ b/ui/edit/edit_naive.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include "profile_editor.h" + + +QT_BEGIN_NAMESPACE +namespace Ui { class EditNaive; } +QT_END_NAMESPACE + +class EditNaive : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + explicit EditNaive(QWidget *parent = nullptr); + + ~EditNaive() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + + QList> get_editor_cached() override; + +private: + Ui::EditNaive *ui; + QSharedPointer ent; + + struct { + QString certificate; + QString extra_headers; + } CACHE; + +private slots: + + void on_certificate_clicked(); + + void on_extra_headers_clicked(); +}; diff --git a/ui/edit/edit_naive.ui b/ui/edit/edit_naive.ui new file mode 100644 index 0000000..6486a5f --- /dev/null +++ b/ui/edit/edit_naive.ui @@ -0,0 +1,126 @@ + + + EditNaive + + + + 0 + 0 + 400 + 300 + + + + EditNaive + + + + + + Protocol + + + + + + + Password + + + + + + + + + + + https + + + + + quic + + + + + + + + Extra headers + + + + + + + SNI + + + + + + + PushButton + + + + + + + Username + + + + + + + + + + + + + Certificate + + + + + + + Insecure concurrency + + + + + + + + + + PushButton + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + username + password + protocol + extra_headers + sni + certificate + insecure_concurrency + + + +
diff --git a/ui/edit/edit_shadowsocks.cpp b/ui/edit/edit_shadowsocks.cpp new file mode 100644 index 0000000..76a3846 --- /dev/null +++ b/ui/edit/edit_shadowsocks.cpp @@ -0,0 +1,39 @@ +#include "edit_shadowsocks.h" +#include "ui_edit_shadowsocks.h" + +#include "fmt/ShadowSocksBean.hpp" + +EditShadowSocks::EditShadowSocks(QWidget *parent) : QWidget(parent), + ui(new Ui::EditShadowSocks) { + ui->setupUi(this); +} + +EditShadowSocks::~EditShadowSocks() { + delete ui; +} + +void EditShadowSocks::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->ShadowSocksBean(); + + ui->method->setCurrentText(bean->method); + ui->password->setText(bean->password); + auto ssPlugin = bean->plugin.split(";"); + if (!ssPlugin.empty()) { + ui->plugin->setCurrentText(ssPlugin[0]); + ui->plugin_opts->setText(SubStrAfter(bean->plugin, ";")); + } +} + +bool EditShadowSocks::onEnd() { + auto bean = this->ent->ShadowSocksBean(); + + bean->method = ui->method->currentText(); + bean->password = ui->password->text(); + bean->plugin = ui->plugin->currentText(); + if (!bean->plugin.isEmpty()) { + bean->plugin += ";" + ui->plugin_opts->text(); + } + + return true; +} \ No newline at end of file diff --git a/ui/edit/edit_shadowsocks.h b/ui/edit/edit_shadowsocks.h new file mode 100644 index 0000000..08eb822 --- /dev/null +++ b/ui/edit/edit_shadowsocks.h @@ -0,0 +1,28 @@ +#ifndef EDIT_SHADOWSOCKS_H +#define EDIT_SHADOWSOCKS_H + +#include +#include "profile_editor.h" + +namespace Ui { + class EditShadowSocks; +} + +class EditShadowSocks : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + explicit EditShadowSocks(QWidget *parent = nullptr); + + ~EditShadowSocks() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + +private: + Ui::EditShadowSocks *ui; + QSharedPointer ent; +}; + +#endif // EDIT_SHADOWSOCKS_H diff --git a/ui/edit/edit_shadowsocks.ui b/ui/edit/edit_shadowsocks.ui new file mode 100644 index 0000000..faa8d5c --- /dev/null +++ b/ui/edit/edit_shadowsocks.ui @@ -0,0 +1,192 @@ + + + EditShadowSocks + + + + 0 + 0 + 400 + 300 + + + + Form + + + + 0 + + + + + Plugin Args + + + + + + + + + + Password + + + + + + + Encryption + + + + + + + + + + + + + obfs-local + + + + + v2ray-plugin + + + + + ssr + + + + + + + + + + + true + + + + aes-128-gcm + + + + + aes-192-gcm + + + + + aes-256-gcm + + + + + chacha20-poly1305 + + + + + xchacha20-poly1305 + + + + + aes-128-ctr + + + + + aes-192-ctr + + + + + aes-256-ctr + + + + + aes-128-cfb + + + + + aes-192-cfb + + + + + aes-256-cfb + + + + + rc4 + + + + + rc4-md5 + + + + + salsa20 + + + + + chacha20 + + + + + chacha20-ietf + + + + + xchacha20 + + + + + none + + + + + + + + Plugin + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + method + password + plugin + plugin_opts + + + +
diff --git a/ui/edit/edit_socks_http.cpp b/ui/edit/edit_socks_http.cpp new file mode 100644 index 0000000..9e03254 --- /dev/null +++ b/ui/edit/edit_socks_http.cpp @@ -0,0 +1,48 @@ +#include "edit_socks_http.h" +#include "ui_edit_socks_http.h" + +#include "fmt/SocksHttpBean.hpp" + +EditSocksHttp::EditSocksHttp(QWidget *parent) : QWidget(parent), + ui(new Ui::EditSocksHttp) { + ui->setupUi(this); +} + +EditSocksHttp::~EditSocksHttp() { + delete ui; +} + +void EditSocksHttp::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->SocksHTTPBean(); + + if (bean->socks_http_type == NekoRay::fmt::SocksHttpBean::type_Socks4) { + ui->version->setCurrentIndex(1); + } else { + ui->version->setCurrentIndex(0); + } + if (bean->socks_http_type == NekoRay::fmt::SocksHttpBean::type_HTTP) { + ui->version->setVisible(false); + ui->version_l->setVisible(false); + } + + ui->username->setText(bean->username); + ui->password->setText(bean->password); +} + +bool EditSocksHttp::onEnd() { + auto bean = this->ent->SocksHTTPBean(); + + if (ui->version->isVisible()) { + if (ui->version->currentIndex() == 1) { + bean->socks_http_type = NekoRay::fmt::SocksHttpBean::type_Socks4; + } else { + bean->socks_http_type = NekoRay::fmt::SocksHttpBean::type_Socks5; + } + } + + bean->username = ui->username->text(); + bean->password = ui->password->text(); + + return true; +} diff --git a/ui/edit/edit_socks_http.h b/ui/edit/edit_socks_http.h new file mode 100644 index 0000000..7440e31 --- /dev/null +++ b/ui/edit/edit_socks_http.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "profile_editor.h" + +namespace Ui { + class EditSocksHttp; +} + +class EditSocksHttp : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + explicit EditSocksHttp(QWidget *parent = nullptr); + + ~EditSocksHttp() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + +private: + Ui::EditSocksHttp *ui; + QSharedPointer ent; +}; diff --git a/ui/edit/edit_socks_http.ui b/ui/edit/edit_socks_http.ui new file mode 100644 index 0000000..060be49 --- /dev/null +++ b/ui/edit/edit_socks_http.ui @@ -0,0 +1,70 @@ + + + EditSocksHttp + + + + 0 + 0 + 400 + 300 + + + + Form + + + + 0 + + + + + Version + + + + + + + + + + + + + Username + + + + + + + + 5 + + + + + 4 + + + + + + + + Password + + + + + + + version + username + password + + + + diff --git a/ui/edit/edit_trojan_vless.cpp b/ui/edit/edit_trojan_vless.cpp new file mode 100644 index 0000000..bac023e --- /dev/null +++ b/ui/edit/edit_trojan_vless.cpp @@ -0,0 +1,26 @@ +#include "edit_trojan_vless.h" +#include "ui_edit_trojan_vless.h" + +#include "fmt/TrojanVLESSBean.hpp" + +EditTrojanVLESS::EditTrojanVLESS(QWidget *parent) : + QWidget(parent), ui(new Ui::EditTrojanVLESS) { + ui->setupUi(this); +} + +EditTrojanVLESS::~EditTrojanVLESS() { + delete ui; +} + +void EditTrojanVLESS::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->TrojanVLESSBean(); + if (bean->proxy_type == NekoRay::fmt::TrojanVLESSBean::proxy_VLESS) ui->label->setText("UUID"); + ui->password->setText(bean->password); +} + +bool EditTrojanVLESS::onEnd() { + auto bean = this->ent->TrojanVLESSBean(); + bean->password = ui->password->text(); + return true; +} diff --git a/ui/edit/edit_trojan_vless.h b/ui/edit/edit_trojan_vless.h new file mode 100644 index 0000000..486b276 --- /dev/null +++ b/ui/edit/edit_trojan_vless.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include "profile_editor.h" + + +QT_BEGIN_NAMESPACE +namespace Ui { class EditTrojanVLESS; } +QT_END_NAMESPACE + +class EditTrojanVLESS : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + explicit EditTrojanVLESS(QWidget *parent = nullptr); + + ~EditTrojanVLESS() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + +private: + Ui::EditTrojanVLESS *ui; + QSharedPointer ent; +}; + + diff --git a/ui/edit/edit_trojan_vless.ui b/ui/edit/edit_trojan_vless.ui new file mode 100644 index 0000000..89ec992 --- /dev/null +++ b/ui/edit/edit_trojan_vless.ui @@ -0,0 +1,38 @@ + + + EditTrojanVLESS + + + + 0 + 0 + 400 + 300 + + + + + + + + + + Password + + + + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + +
diff --git a/ui/edit/edit_vmess.cpp b/ui/edit/edit_vmess.cpp new file mode 100644 index 0000000..1835b10 --- /dev/null +++ b/ui/edit/edit_vmess.cpp @@ -0,0 +1,32 @@ +#include "edit_vmess.h" +#include "ui_edit_vmess.h" + +#include "fmt/VMessBean.hpp" + +EditVMess::EditVMess(QWidget *parent) : + QWidget(parent), ui(new Ui::EditVMess) { + ui->setupUi(this); +} + +EditVMess::~EditVMess() { + delete ui; +} + +void EditVMess::onStart(QSharedPointer _ent) { + this->ent = _ent; + auto bean = this->ent->VMessBean(); + + ui->uuid->setText(bean->uuid); + ui->aid->setText(Int2String(bean->aid)); + ui->security->setCurrentText(bean->security); +} + +bool EditVMess::onEnd() { + auto bean = this->ent->VMessBean(); + + bean->uuid = ui->uuid->text(); + bean->aid = ui->aid->text().toInt(); + bean->security = ui->security->currentText(); + + return true; +} diff --git a/ui/edit/edit_vmess.h b/ui/edit/edit_vmess.h new file mode 100644 index 0000000..9e0bb20 --- /dev/null +++ b/ui/edit/edit_vmess.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include "profile_editor.h" + + +QT_BEGIN_NAMESPACE +namespace Ui { class EditVMess; } +QT_END_NAMESPACE + +class EditVMess : public QWidget, public ProfileEditor { +Q_OBJECT + +public: + explicit EditVMess(QWidget *parent = nullptr); + + ~EditVMess() override; + + void onStart(QSharedPointer _ent) override; + + bool onEnd() override; + +private: + Ui::EditVMess *ui; + QSharedPointer ent; +}; + + diff --git a/ui/edit/edit_vmess.ui b/ui/edit/edit_vmess.ui new file mode 100644 index 0000000..ea03ebe --- /dev/null +++ b/ui/edit/edit_vmess.ui @@ -0,0 +1,87 @@ + + + EditVMess + + + + 0 + 0 + 400 + 300 + + + + EditVMess + + + + + + Alter Id + + + + + + + + + + Security + + + + + + + + + + UUID + + + + + + + true + + + + auto + + + + + zero + + + + + none + + + + + chacha20-poly1305 + + + + + aes-128-gcm + + + + + + + + + MyLineEdit + QLineEdit +
ui/widget/MyLineEdit.h
+
+
+ + +
diff --git a/ui/edit/profile_editor.h b/ui/edit/profile_editor.h new file mode 100644 index 0000000..a7cd5c7 --- /dev/null +++ b/ui/edit/profile_editor.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include "db/ProxyEntity.hpp" +#include "main/GuiUtils.hpp" + +class ProfileEditor { +public: + virtual void onStart(QSharedPointer ent) = 0; + + virtual bool onEnd() = 0; + + std::function get_edit_dialog; + + // cached editor + + std::function editor_cache_updated; + + virtual QList> get_editor_cached() { return {}; }; +}; diff --git a/ui/mainwindow.cpp b/ui/mainwindow.cpp new file mode 100644 index 0000000..a74be5e --- /dev/null +++ b/ui/mainwindow.cpp @@ -0,0 +1,1436 @@ +#include "./ui_mainwindow.h" +#include "mainwindow.h" + +#include "db/ProfileFilter.hpp" +#include "db/ConfigBuilder.hpp" +#include "sub/GroupUpdater.hpp" +#include "sys/ExternalProcess.hpp" +#include "sys/AutoRun.hpp" + +#include "ui/ThemeManager.hpp" +#include "ui/edit/dialog_edit_profile.h" +#include "ui/dialog_basic_settings.h" +#include "ui/dialog_manage_groups.h" +#include "ui/dialog_manage_routes.h" +#include "ui/dialog_hotkey.h" + +#include "3rdparty/qrcodegen.hpp" +#include "3rdparty/VT100Parser.hpp" +#include "qv2ray/ui/LogHighlighter.hpp" + +#ifndef NKR_NO_EXTERNAL + +#include "3rdparty/ZxingQtReader.hpp" +#include "qv2ray/components/proxy/QvProxyConfigurator.hpp" + +#endif + +#ifdef Q_OS_WIN + +#include "3rdparty/WinCommander.hpp" + +#else + +#include + +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent), ui(new Ui::MainWindow) { + mainwindow = this; + dialog_message = [=](const QString &a, const QString &b) { + runOnUiThread([=] { + dialog_message_impl(a, b); + }); + }; + + // Load Manager + auto isLoaded = NekoRay::profileManager->Load(); + if (!isLoaded) { + auto defaultGroup = NekoRay::ProfileManager::NewGroup(); + defaultGroup->name = tr("Default"); + NekoRay::profileManager->AddGroup(defaultGroup); + } + + // Setup misc UI + themeManager->ApplyTheme(NekoRay::dataStore->theme); + ui->setupUi(this); + title_base = windowTitle(); + connect(ui->menu_start, &QAction::triggered, this, [=]() { neko_start(); }); + connect(ui->menu_stop, &QAction::triggered, this, [=]() { neko_stop(); }); + connect(ui->tabWidget->tabBar(), &QTabBar::tabMoved, this, [=](int from, int to) { + // use tabData to track tab & gid + NekoRay::profileManager->_groups.clear(); + for (int i = 0; i < ui->tabWidget->tabBar()->count(); i++) { + NekoRay::profileManager->_groups += ui->tabWidget->tabBar()->tabData(i).toInt(); + } + NekoRay::profileManager->Save(); + }); + ui->label_running->installEventFilter(this); + ui->label_inbound->installEventFilter(this); + RegisterHotkey(false); + auto last_size = NekoRay::dataStore->mw_size.split("x"); + if (last_size.length() == 2) { + auto w = last_size[0].toInt(); + auto h = last_size[1].toInt(); + if (w > 0 && h > 0) { + resize(w, h); + } + } + + // top bar + ui->toolButton_program->setMenu(ui->menu_program); + ui->toolButton_preferences->setMenu(ui->menu_preferences); + ui->toolButton_server->setMenu(ui->menu_server); + ui->menubar->setVisible(false); + connect(ui->toolButton_document, &QToolButton::clicked, this, + [=] { QDesktopServices::openUrl(QUrl("https://matsuridayo.github.io/")); }); + connect(ui->toolButton_ads, &QToolButton::clicked, this, + [=] { QDesktopServices::openUrl(QUrl("https://matsuricom.github.io/")); }); + connect(ui->toolButton_update, &QToolButton::clicked, this, [=] { CheckUpdate(); }); + + // Setup log UI + new SyntaxHighlighter(false, qvLogDocument); + ui->masterLogBrowser->setDocument(qvLogDocument); + ui->masterLogBrowser->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + { + auto font = ui->masterLogBrowser->font(); + font.setPointSize(9); + ui->masterLogBrowser->setFont(font); + qvLogDocument->setDefaultFont(font); + } + connect(ui->masterLogBrowser->verticalScrollBar(), &QSlider::valueChanged, this, [=](int value) { + if (ui->masterLogBrowser->verticalScrollBar()->maximum() == value) + qvLogAutoScoll = true; + else + qvLogAutoScoll = false; + }); + connect(ui->masterLogBrowser, &QTextBrowser::textChanged, this, [=]() { + if (!qvLogAutoScoll) + return; + auto bar = ui->masterLogBrowser->verticalScrollBar(); + bar->setValue(bar->maximum()); + }); + showLog = [=](const QString &log) { + runOnUiThread([=] { + show_log_impl(log); + }); + }; + showLog_ext = [=](const QString &tag, const QString &log) { + runOnUiThread([=] { + show_log_impl("[" + tag + "] " + log); + }); + }; + showLog_ext_vt100 = [=](const QString &log) { + runOnUiThread([=] { + show_log_impl(cleanVT100String(log)); + }); + }; + + // table UI + ui->proxyListTable->callback_save_order = [=] { + auto group = NekoRay::profileManager->CurrentGroup(); + group->order = ui->proxyListTable->order; + group->Save(); + }; + connect(ui->proxyListTable->horizontalHeader(), &QHeaderView::sectionClicked, this, + [=](int logicalIndex) { + NekoRay::GroupSortAction action; + // 不正确的descending实现 + if (proxy_last_order == logicalIndex) { + action.descending = true; + proxy_last_order = -1; + } else { + proxy_last_order = logicalIndex; + } + action.save_sort = true; + // 表头 + if (logicalIndex == 1) { + action.method = NekoRay::GroupSortMethod::ByType; + } else if (logicalIndex == 2) { + action.method = NekoRay::GroupSortMethod::ByAddress; + } else if (logicalIndex == 3) { + action.method = NekoRay::GroupSortMethod::ByName; + } else if (logicalIndex == 4) { + action.method = NekoRay::GroupSortMethod::ByLatency; + } else if (logicalIndex == 0) { + action.method = NekoRay::GroupSortMethod::ById; + } else { + return; + } + refresh_proxy_list_impl(-1, action); + }); + ui->proxyListTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + ui->proxyListTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + ui->proxyListTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); + ui->proxyListTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); + ui->proxyListTable->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents); + ui->proxyListTable->horizontalHeader()->setSectionResizeMode(5, QHeaderView::ResizeToContents); + ui->tableWidget_conn->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + ui->tableWidget_conn->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + ui->tableWidget_conn->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); + + // search box + ui->search->setVisible(false); + connect(shortcut_ctrl_f, &QShortcut::activated, this, [=] { + ui->search->setVisible(true); + ui->search->setFocus(); + }); + connect(shortcut_esc, &QShortcut::activated, this, [=] { + if (ui->search->isVisible()) { + ui->search->setText(""); + ui->search->textChanged(""); + ui->search->setVisible(false); + } + if (select_mode) { + emit profile_selected(-1); + select_mode = false; + refresh_status(); + } + }); + connect(ui->search, &QLineEdit::textChanged, this, [=](const QString &text) { + if (text.isEmpty()) { + for (int i = 0; i < ui->proxyListTable->rowCount(); i++) { + ui->proxyListTable->setRowHidden(i, false); + } + } else { + QList findItem = ui->proxyListTable->findItems(text, Qt::MatchContains); + for (int i = 0; i < ui->proxyListTable->rowCount(); i++) { + ui->proxyListTable->setRowHidden(i, true); + } + for (auto item: findItem) { + if (item != nullptr) ui->proxyListTable->setRowHidden(item->row(), false); + } + } + }); + + // refresh + this->refresh_groups(); + + // Setup Tray + auto icon = QIcon::fromTheme("nekoray"); + auto pixmap = QPixmap("../nekoray.png"); + if (!pixmap.isNull()) icon = QIcon(pixmap); + pixmap = QPixmap("./nekoray.png"); + if (!pixmap.isNull()) icon = QIcon(pixmap); + setWindowIcon(icon); + + tray = new QSystemTrayIcon(this);//初始化托盘对象tray + tray->setIcon(icon);//设定托盘图标,引号内是自定义的png图片路径 + tray->setContextMenu(ui->menu_program);//创建托盘菜单 + tray->show();//让托盘图标显示在系统托盘上 + connect(tray, &QSystemTrayIcon::activated, this, + [=](QSystemTrayIcon::ActivationReason reason) { + switch (reason) { + case QSystemTrayIcon::Trigger: + if (this->isVisible()) { + hide(); + } else { + this->showNormal(); + this->raise(); + this->activateWindow(); + } + break; + default: + break; + } + }); + + // + ui->menu_program_preference->addActions(ui->menu_preferences->actions()); + connect(ui->menu_add_from_clipboard2, &QAction::triggered, ui->menu_add_from_clipboard, &QAction::trigger); + connect(shortcut_ctrl_v, &QShortcut::activated, ui->menu_add_from_clipboard, &QAction::trigger); + // + connect(ui->menu_program, &QMenu::aboutToShow, this, [=]() { + ui->actionRemember_last_proxy->setChecked(NekoRay::dataStore->remember_enable); + ui->actionStart_with_system->setChecked(GetProcessAutoRunSelf()); + ui->actionStart_minimal->setChecked(NekoRay::dataStore->start_minimal); + // active server + for (const auto &old: ui->menuActive_Server->actions()) { + ui->menuActive_Server->removeAction(old); + old->deleteLater(); + } + for (const auto &pf: NekoRay::profileManager->CurrentGroup()->ProfilesWithOrder()) { + auto a = new QAction(pf->bean->DisplayTypeAndName()); + a->setProperty("id", pf->id); + a->setCheckable(true); + if (NekoRay::dataStore->started_id == pf->id) a->setChecked(true); + ui->menuActive_Server->addAction(a); + } + // active routing + for (const auto &old: ui->menuActive_Routing->actions()) { + ui->menuActive_Routing->removeAction(old); + old->deleteLater(); + } + for (const auto &name: NekoRay::Routing::List()) { + auto a = new QAction(name); + a->setCheckable(true); + a->setChecked(name == NekoRay::dataStore->active_routing); + ui->menuActive_Routing->addAction(a); + } + }); + connect(ui->menuActive_Server, &QMenu::triggered, this, [=](QAction *a) { + bool ok; + auto id = a->property("id").toInt(&ok); + if (!ok) return; + if (NekoRay::dataStore->started_id == id) { + neko_stop(); + } else { + neko_start(id); + } + }); + connect(ui->menuActive_Routing, &QMenu::triggered, this, [=](QAction *a) { + auto fn = a->text(); + if (!fn.isEmpty()) { + NekoRay::Routing r; + r.load_control_force = true; + r.fn = "routes/" + fn; + if (r.Load()) { + auto btn = QMessageBox::question(nullptr, "NekoRay", + tr("Load routing and apply: %1").arg(fn) + "\n" + r.toString()); + if (btn == QMessageBox::Yes) { + NekoRay::Routing::SetToActive(fn); + if (NekoRay::dataStore->started_id >= 0) { + neko_start(NekoRay::dataStore->started_id); + } else { + refresh_status(); + } + } + } + } + }); + connect(ui->actionRemember_last_proxy, &QAction::triggered, this, [=](bool checked) { + NekoRay::dataStore->remember_enable = checked; + NekoRay::dataStore->Save(); + }); + connect(ui->actionStart_with_system, &QAction::triggered, this, [=](bool checked) { + SetProcessAutoRunSelf(checked); + }); + connect(ui->actionStart_minimal, &QAction::triggered, this, [=](bool checked) { + NekoRay::dataStore->start_minimal = checked; + NekoRay::dataStore->Save(); + }); + // + connect(ui->checkBox_VPN, &QCheckBox::clicked, this, [=](bool checked) { + neko_set_spmode(checked ? NekoRay::SystemProxyMode::VPN : NekoRay::SystemProxyMode::DISABLE); + }); + connect(ui->menu_spmode, &QMenu::aboutToShow, this, [=]() { + ui->menu_spmode_disabled->setChecked(title_spmode == NekoRay::SystemProxyMode::DISABLE); + ui->menu_spmode_system_proxy->setChecked(title_spmode == NekoRay::SystemProxyMode::SYSTEM_PROXY); + ui->menu_spmode_vpn->setChecked(title_spmode == NekoRay::SystemProxyMode::VPN); + }); + connect(ui->menu_spmode_system_proxy, &QAction::triggered, this, + [=]() { neko_set_spmode(NekoRay::SystemProxyMode::SYSTEM_PROXY); }); + connect(ui->menu_spmode_vpn, &QAction::triggered, this, + [=]() { neko_set_spmode(NekoRay::SystemProxyMode::VPN); }); + connect(ui->menu_spmode_disabled, &QAction::triggered, this, + [=]() { neko_set_spmode(NekoRay::SystemProxyMode::DISABLE); }); + connect(ui->menu_qr, &QAction::triggered, this, [=]() { display_qr_link(false); }); + connect(ui->menu_qr_nkr, &QAction::triggered, this, [=]() { display_qr_link(true); }); + connect(ui->menu_tcp_ping, &QAction::triggered, this, [=]() { speedtest_current_group(0); }); + connect(ui->menu_url_test, &QAction::triggered, this, [=]() { speedtest_current_group(1); }); + connect(ui->menu_full_test, &QAction::triggered, this, [=]() { speedtest_current_group(2); }); + refresh_status(); + + // Start Core + NekoRay::dataStore->core_token = GetRandomString(32); + NekoRay::dataStore->core_port = MkPort(); + if (NekoRay::dataStore->core_port <= 0) NekoRay::dataStore->core_port = 19810; + + runOnNewThread([=]() { + QString starting_info; + + auto core_path = NekoRay::dataStore->core_path; +#ifdef Q_OS_WIN + if (!core_path.endsWith(".exe")) core_path += ".exe"; +#endif + if (!QFile(core_path).exists()) { + starting_info = "(not found)"; + core_path = ""; + } + + QStringList args; + args.push_back("nekoray"); + args.push_back("-port"); + args.push_back(Int2String(NekoRay::dataStore->core_port)); +#ifdef NKR_DEBUG + args.push_back("-debug"); +#endif + + for (int retry = 0; retry < 10; retry++) { + showLog("Starting nekoray core " + starting_info + "\n"); + core_process = new QProcess; + core_process_show_stderr = false; + connect(core_process, &QProcess::readyReadStandardOutput, this, + [&]() { + showLog(core_process->readAllStandardOutput().trimmed()); + }); + connect(core_process, &QProcess::readyReadStandardError, this, + [&]() { + auto log = core_process->readAllStandardError().trimmed(); + if (core_process_show_stderr) { + showLog(log); + return; + } + if (log.contains("token is set")) { + core_process_show_stderr = true; + } + }); + if (core_path.isEmpty()) break; + if (!NekoRay::dataStore->v2ray_asset_dir.isEmpty()) { + core_process->setEnvironment(QStringList{ + "V2RAY_LOCATION_ASSET=" + NekoRay::dataStore->v2ray_asset_dir + }); + } + core_process->start(core_path, args); + core_process->write((NekoRay::dataStore->core_token + "\n").toUtf8()); + core_process->waitForFinished(-1); + if (core_process_killed) return; + runOnUiThread([=] { neko_stop(true); }); + QThread::sleep(2); + core_process->deleteLater(); + } + }); + + setup_grpc(); + + // Start last + if (NekoRay::dataStore->remember_enable) { + if (NekoRay::dataStore->system_proxy_mode == NekoRay::SystemProxyMode::SYSTEM_PROXY) { + neko_set_spmode(NekoRay::dataStore->system_proxy_mode, false); + } + if (NekoRay::dataStore->remember_id >= 0) { + runOnUiThread([=] { neko_start(NekoRay::dataStore->remember_id); }); + } + } + + if (!NekoRay::dataStore->start_minimal) show(); +} + +void MainWindow::closeEvent(QCloseEvent *event) { + if (tray->isVisible()) { + hide(); //隐藏窗口 + event->ignore(); //忽略事件 + } +} + +MainWindow::~MainWindow() { + delete ui; +} + +// Group tab manage + +inline int tabIndex2GroupId(int index) { + if (NekoRay::profileManager->_groups.length() <= index) return -1; + return NekoRay::profileManager->_groups[index]; +} + +inline int groupId2TabIndex(int gid) { + for (int key = 0; key < NekoRay::profileManager->_groups.count(); key++) { + if (NekoRay::profileManager->_groups[key] == gid) return key; + } + return 0; +} + +void MainWindow::on_tabWidget_currentChanged(int index) { + if (NekoRay::dataStore->refreshing_group_list) return; + if (tabIndex2GroupId(index) == NekoRay::dataStore->current_group) return; + show_group(tabIndex2GroupId(index)); +} + +void MainWindow::show_group(int gid) { + auto group = NekoRay::profileManager->GetGroup(gid); + if (group == nullptr) { + MessageBoxWarning(tr("Error"), QString("No such group: %1").arg(gid)); + return; + } + if (NekoRay::dataStore->current_group != gid) { + NekoRay::dataStore->current_group = gid; + NekoRay::dataStore->Save(); + } + ui->tabWidget->widget(groupId2TabIndex(gid))->layout()->addWidget(ui->proxyListTable); + refresh_proxy_list(); +} + +// callback + +void MainWindow::dialog_message_impl(const QString &sender, const QString &info) { + if (info.contains("UpdateDataStore")) { + auto changed = NekoRay::dataStore->Save(); + refresh_proxy_list(); + if (changed && NekoRay::dataStore->started_id >= 0 && + QMessageBox::question(this, tr("Confirmation"), tr("Settings changed, restart proxy?") + ) == QMessageBox::StandardButton::Yes) { + neko_start(NekoRay::dataStore->started_id); + } + refresh_status(); + } + if (sender == Dialog_DialogEditProfile) { + if (info == "accept") { + refresh_proxy_list(); + } + } else if (sender == Dialog_DialogManageGroups) { + if (info.startsWith("refresh")) { + this->refresh_groups(); + } + } else if (sender == "SubUpdater") { + // 订阅完毕 + refresh_proxy_list(); + if (!info.contains("dingyue")) { + QMessageBox::information(this, "NekoRay", + tr("Imported %1 profile(s)").arg(NekoRay::dataStore->imported_count)); + } + } else if (sender == "ExternalProcess") { + if (info == "Crashed") { + neko_stop(); + } + } +} + +// top bar & tray menu + +inline bool dialog_is_using = false; + +#define USE_DIALOG(a) if (dialog_is_using) return; \ +dialog_is_using = true; \ +auto dialog = new a(this); \ +dialog->exec(); \ +dialog->deleteLater(); \ +dialog_is_using = false; + +void MainWindow::on_menu_basic_settings_triggered() { + USE_DIALOG(DialogBasicSettings) +} + +void MainWindow::on_menu_manage_groups_triggered() { + USE_DIALOG(DialogManageGroups) +} + +void MainWindow::on_menu_routing_settings_triggered() { + USE_DIALOG(DialogManageRoutes) +} + +void MainWindow::on_menu_hotkey_settings_triggered() { + USE_DIALOG(DialogHotkey) +} + +void MainWindow::on_menu_exit_triggered() { + neko_set_spmode(NekoRay::SystemProxyMode::DISABLE, false); + if (title_spmode == NekoRay::SystemProxyMode::VPN) return; + RegisterHotkey(true); + + if (!isMaximized()) { + auto olds = NekoRay::dataStore->mw_size; + auto news = QString("%1x%2").arg(size().width()).arg(size().height()); + if (olds != news) { + NekoRay::dataStore->mw_size = news; + NekoRay::dataStore->Save(); + } + } + + auto last_id = NekoRay::dataStore->started_id; + neko_stop(); + if (NekoRay::dataStore->remember_enable && last_id >= 0) { + NekoRay::dataStore->UpdateStartedId(last_id); + } + + core_process_killed = true; + hide(); + ExitNekorayCore(); + + if (exit_update) { + QDir::setCurrent(QApplication::applicationDirPath()); + QProcess::startDetached("./updater", QStringList{}); + } + qApp->quit(); +} + +void MainWindow::neko_set_spmode(int mode, bool save) { + if (mode != title_spmode) { + // DISABLE + + if (title_spmode == NekoRay::SystemProxyMode::SYSTEM_PROXY) { +#ifndef NKR_NO_EXTERNAL + ClearSystemProxy(); +#endif + } else if (title_spmode == NekoRay::SystemProxyMode::VPN) { + if (!StopVPNProcess()) { + refresh_status(); + return; + } + } + + // ENABLE + + if (mode == NekoRay::SystemProxyMode::SYSTEM_PROXY) { +#ifndef NKR_NO_EXTERNAL +#ifdef Q_OS_WIN + if (mode == NekoRay::SystemProxyMode::SYSTEM_PROXY && + !InRange(NekoRay::dataStore->inbound_http_port, 0, 65535)) { + auto btn = QMessageBox::warning(this, "NekoRay", tr("Http inbound is not enabled, can't set system proxy."), + "OK", tr("Settings"), "", 0, 0); + if (btn == 1) { + on_menu_basic_settings_triggered(); + } + return; + } +#endif + SetSystemProxy("127.0.0.1", + NekoRay::dataStore->inbound_http_port, + NekoRay::dataStore->inbound_socks_port); +#endif + } else if (mode == NekoRay::SystemProxyMode::VPN) { + if (!StartVPNProcess()) { + refresh_status(); + return; + } + } + } + + if (save) { + NekoRay::dataStore->system_proxy_mode = mode; + NekoRay::dataStore->Save(); + } + + title_spmode = mode; + refresh_status(); +} + +void MainWindow::refresh_status(const QString &traffic_update) { + // From TrafficLooper + if (!traffic_update.isEmpty()) { + traffic_update_cache = traffic_update; + if (traffic_update == "STOP") { + traffic_update_cache = ""; + } else { + ui->label_speed->setText(traffic_update); + return; + } + } + ui->label_speed->setText(traffic_update_cache); + + // From UI + if (last_test_time.addSecs(2) < QTime::currentTime()) { + ui->label_running->setText(tr("Running: %1").arg(running.isNull() + ? tr("None") + : running->bean->DisplayName().left(50))); + } + // + auto display_http = tr("None"); + if (InRange(NekoRay::dataStore->inbound_http_port, 0, 65535)) { + display_http = DisplayAddress(NekoRay::dataStore->inbound_address, NekoRay::dataStore->inbound_http_port); + } + auto inbound_txt = QString("Socks: %1\nHTTP: %2").arg( + DisplayAddress(NekoRay::dataStore->inbound_address, NekoRay::dataStore->inbound_socks_port), + display_http + ); + ui->label_inbound->setText(inbound_txt); + // + ui->checkBox_VPN->setChecked(title_spmode == NekoRay::SystemProxyMode::VPN); + if (select_mode) ui->label_running->setText("[" + tr("Select") + "]"); + + auto make_title = [=](bool isTray) { + QStringList tt; + if (select_mode) tt << "[" + tr("Select") + "]"; + if (!title_error.isEmpty()) tt << "[" + title_error + "]"; + if (title_spmode == NekoRay::SystemProxyMode::SYSTEM_PROXY) { + tt << "[" + tr("System Proxy") + "]"; + } else if (title_spmode == NekoRay::SystemProxyMode::VPN) { + tt << "[" + tr("VPN Mode") + "]"; + } + tt << title_base; + if (!isTray) tt << "(" + QString(NKR_VERSION) + ")"; + if (!NekoRay::dataStore->active_routing.isEmpty() && NekoRay::dataStore->active_routing != "Default") { + tt << "[" + NekoRay::dataStore->active_routing + "]"; + } + if (!running.isNull()) tt << running->bean->DisplayTypeAndName(); + return tt.join(isTray ? "\n" : " "); + }; + + setWindowTitle(make_title(false)); + if (tray != nullptr) tray->setToolTip(make_title(true)); +} + +// table显示 + +// refresh_groups -> show_group -> refresh_proxy_list +void MainWindow::refresh_groups() { + NekoRay::dataStore->refreshing_group_list = true; + + // refresh group? + for (int i = ui->tabWidget->count() - 1; i > 0; i--) { + ui->tabWidget->removeTab(i); + } + + int index = 0; + for (const auto &gid: NekoRay::profileManager->_groups) { + auto group = NekoRay::profileManager->GetGroup(gid); + if (index == 0) { + ui->tabWidget->setTabText(0, group->name); + } else { + auto widget2 = new QWidget(); + auto layout2 = new QVBoxLayout(); + layout2->setContentsMargins(QMargins()); + layout2->setSpacing(0); + widget2->setLayout(layout2); + ui->tabWidget->addTab(widget2, group->name); + } + ui->tabWidget->tabBar()->setTabData(index, gid); + index++; + } + + // show after group changed + if (NekoRay::profileManager->CurrentGroup() == nullptr) { + NekoRay::dataStore->current_group = -1; + ui->tabWidget->setCurrentIndex(groupId2TabIndex(0)); + show_group(NekoRay::profileManager->_groups.count() > 0 ? NekoRay::profileManager->_groups.first() : 0); + } else { + ui->tabWidget->setCurrentIndex(groupId2TabIndex(NekoRay::dataStore->current_group)); + show_group(NekoRay::dataStore->current_group); + } + + NekoRay::dataStore->refreshing_group_list = false; +} + + +void MainWindow::refresh_proxy_list_impl(const int &id, NekoRay::GroupSortAction groupSortAction) { + if (id < 0) { + //这样才能清空数据 + ui->proxyListTable->setRowCount(0); + } + + // 绘制或更新item(s) + int row = -1; + for (const auto &profile: NekoRay::profileManager->profiles) { + if (NekoRay::dataStore->current_group != profile->gid) continue; + + row++; + if (id >= 0 && profile->id != id) continue; // update only one item + if (id < 0) { + ui->proxyListTable->insertRow(row); + } else { + // 排序过的,要找 + row = ui->proxyListTable->id2Row[id]; + } + + auto f0 = std::make_unique(); +// auto font = f0->font(); +// font.setPointSize(9); +// f0->setFont(font); + f0->setData(114514, profile->id); + + // C0: is Running + auto f = f0->clone(); + if (profile->id == NekoRay::dataStore->started_id) { + f->setText("✓"); + } else { + f->setText(" "); + } + ui->proxyListTable->setItem(row, 0, f); + + // C1: Type + f = f0->clone(); + f->setText(profile->bean->DisplayType()); + auto insecure_hint = DisplayInsecureHint(profile->bean); + if (!insecure_hint.isEmpty()) { + f->setBackground(Qt::red); + f->setToolTip(insecure_hint); + } + ui->proxyListTable->setItem(row, 1, f); + + // C2: Address+Port + f = f0->clone(); + f->setText(profile->bean->DisplayAddress()); + ui->proxyListTable->setItem(row, 2, f); + + // C3: Name + f = f0->clone(); + f->setText(profile->bean->name); + ui->proxyListTable->setItem(row, 3, f); + + // C4: Test Result + f = f0->clone(); + f->setText(profile->DisplayLatency()); + if (!profile->full_test_report.isEmpty()) f->setText(profile->full_test_report); + ui->proxyListTable->setItem(row, 4, f); + + // C5: Traffic + f = f0->clone(); + f->setText(profile->traffic_data->DisplayTraffic()); + ui->proxyListTable->setItem(row, 5, f); + } + + // 显示排序 + if (id < 0) { + switch (groupSortAction.method) { + case NekoRay::GroupSortMethod::Raw: { + auto group = NekoRay::profileManager->CurrentGroup(); + if (group == nullptr) return; + ui->proxyListTable->order = group->order; + break; + } + case NekoRay::GroupSortMethod::ById: { + std::sort(ui->proxyListTable->order.begin(), ui->proxyListTable->order.end(), + [=](int a, int b) { + if (groupSortAction.descending) { + return a > b; + } else { + return a < b; + } + }); + break; + } + case NekoRay::GroupSortMethod::ByAddress: + case NekoRay::GroupSortMethod::ByName: + case NekoRay::GroupSortMethod::ByType: { + std::sort(ui->proxyListTable->order.begin(), ui->proxyListTable->order.end(), + [=](int a, int b) { + QString ms_a; + QString ms_b; + if (groupSortAction.method == NekoRay::GroupSortMethod::ByType) { + ms_a = NekoRay::profileManager->GetProfile(a)->bean->DisplayType(); + ms_b = NekoRay::profileManager->GetProfile(b)->bean->DisplayType(); + } else if (groupSortAction.method == NekoRay::GroupSortMethod::ByName) { + ms_a = NekoRay::profileManager->GetProfile(a)->bean->name; + ms_b = NekoRay::profileManager->GetProfile(b)->bean->name; + } else if (groupSortAction.method == NekoRay::GroupSortMethod::ByAddress) { + ms_a = NekoRay::profileManager->GetProfile(a)->bean->DisplayAddress(); + ms_b = NekoRay::profileManager->GetProfile(b)->bean->DisplayAddress(); + } + if (groupSortAction.descending) { + return ms_a > ms_b; + } else { + return ms_a < ms_b; + } + }); + break; + } + case NekoRay::GroupSortMethod::ByLatency: { + std::sort(ui->proxyListTable->order.begin(), ui->proxyListTable->order.end(), + [=](int a, int b) { + auto ms_a = NekoRay::profileManager->GetProfile(a)->latency; + auto ms_b = NekoRay::profileManager->GetProfile(b)->latency; + if (ms_a <= 0) ms_a = 114514; + if (ms_b <= 0) ms_b = 114514; + if (groupSortAction.descending) { + return ms_a > ms_b; + } else { + return ms_a < ms_b; + } + }); + break; + } + } + ui->proxyListTable->update_order(groupSortAction.save_sort); + } +} + +// table菜单相关 + +void MainWindow::on_proxyListTable_itemDoubleClicked(QTableWidgetItem *item) { + auto id = item->data(114514).toInt(); + if (select_mode) { + emit profile_selected(id); + select_mode = false; + refresh_status(); + return; + } + auto dialog = new DialogEditProfile("", id, this); + connect(dialog, &QDialog::finished, dialog, &QDialog::deleteLater); +} + +void MainWindow::on_menu_add_from_input_triggered() { + if (!NekoRay::profileManager->CurrentGroup()->url.isEmpty()) return; + auto dialog = new DialogEditProfile("socks", NekoRay::dataStore->current_group, this); + connect(dialog, &QDialog::finished, dialog, &QDialog::deleteLater); +} + +void MainWindow::on_menu_add_from_clipboard_triggered() { + if (!NekoRay::profileManager->CurrentGroup()->url.isEmpty()) return; + auto clipboard = QApplication::clipboard()->text(); + NekoRay::sub::groupUpdater->AsyncUpdate(clipboard); +} + +void MainWindow::on_menu_move_triggered() { + auto ents = get_now_selected(); + if (ents.isEmpty()) return; + + auto items = QStringList{}; + for (auto &&group: NekoRay::profileManager->groups) { + items += Int2String(group->id) + " " + group->name; + } + + bool ok; + auto a = QInputDialog::getItem(nullptr, + tr("Move"), + tr("Move %1 item(s)").arg(ents.count()), + items, 0, false, &ok); + if (!ok) return; + auto gid = SubStrBefore(a, " ").toInt(); + for (const auto &ent: ents) { + NekoRay::profileManager->MoveProfile(ent, gid); + } + refresh_proxy_list(); +} + +void MainWindow::on_menu_delete_triggered() { + auto ents = get_now_selected(); + if (ents.count() == 0) return; + if (QMessageBox::question(this, tr("Confirmation"), QString(tr("Remove %1 item(s) ?")).arg(ents.count())) == + QMessageBox::StandardButton::Yes) { + for (const auto &ent: ents) { + NekoRay::profileManager->DeleteProfile(ent->id); + } + refresh_proxy_list(); + } +} + +void MainWindow::on_menu_reset_traffic_triggered() { + auto ents = get_now_selected(); + if (ents.count() == 0) return; + if (QMessageBox::question(this, + tr("Confirmation"), + QString(tr("Reset traffic of %1 item(s) ?")).arg(ents.count())) + == QMessageBox::StandardButton::Yes) { + for (const auto &ent: ents) { + ent->traffic_data->Reset(); + ent->Save(); + refresh_proxy_list(ent->id); + } + } +} + +void MainWindow::on_menu_profile_debug_info_triggered() { + auto ents = get_now_selected(); + if (ents.count() != 1) return; + auto btn = QMessageBox::information(nullptr, "NekoRay", ents.first()->ToJsonBytes(), "OK", "Edit", "Reload", 0, 0); + if (btn == 1) { + QDesktopServices::openUrl(QUrl::fromLocalFile( + QFileInfo(QString("profiles/%1.json").arg(ents.first()->id)).absoluteFilePath() + )); + } else if (btn == 2) { + ents.first()->Load(); + refresh_proxy_list(); + } +} + +void MainWindow::on_menu_copy_links_triggered() { + auto ents = get_now_selected(); + QStringList links; + for (const auto &ent: ents) { + links += ent->bean->ToShareLink(); + } + QApplication::clipboard()->setText(links.join("\n")); + MessageBoxInfo("NekoRay", tr("Copied %1 item(s)").arg(links.length())); +} + +void MainWindow::on_menu_export_config_triggered() { + auto ents = get_now_selected(); + if (ents.count() != 1) return; + auto ent = ents.first(); + auto result = NekoRay::BuildConfig(ent, false); + auto config_core = QJsonObject2QString(result->coreConfig, true); + QApplication::clipboard()->setText(config_core); + MessageBoxWarning(tr("Config copied"), config_core); +} + +void MainWindow::display_qr_link(bool nkrFormat) { + auto ents = get_now_selected(); + if (ents.count() != 1) return; + + auto link = ents.first()->bean->ToShareLink(); + if (nkrFormat) { + link = ents.first()->bean->ToNekorayShareLink(ents.first()->type); + } + + qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(link.toUtf8().data(), + qrcodegen::QrCode::Ecc::MEDIUM); + qint32 sz = qr.getSize(); + QImage im(sz, sz, QImage::Format_RGB32); + QRgb black = qRgb(0, 0, 0); + QRgb white = qRgb(255, 255, 255); + for (int y = 0; y < sz; y++) + for (int x = 0; x < sz; x++) + im.setPixel(x, y, qr.getModule(x, y) ? black : white); + + class W : public QDialog { + public: + QLabel *l = nullptr; + QPlainTextEdit *l2 = nullptr; + QImage im; + + void set(QLabel *qLabel, QPlainTextEdit *pl, QImage qImage) { + this->l = qLabel; + this->l2 = pl; + this->im = std::move(qImage); + } + + void resizeEvent(QResizeEvent *resizeEvent) override { + auto size = resizeEvent->size(); + auto side = size.height() - 20 - l2->size().height(); + l->setPixmap(QPixmap::fromImage(im.scaled(side, side, Qt::KeepAspectRatio, Qt::FastTransformation), + Qt::MonoOnly)); + l->resize(side, side); + } + }; + + auto w = new W(); + auto l = new QLabel(w); + w->setLayout(new QVBoxLayout); + w->setMinimumSize(256, 256); + l->setMinimumSize(256, 256); + l->setMargin(6); + l->setAlignment(Qt::AlignmentFlag::AlignCenter); + l->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + l->setScaledContents(true); + w->layout()->addWidget(l); + auto l2 = new QPlainTextEdit(w); + l2->setPlainText(link); + l2->setReadOnly(true); + w->layout()->addWidget(l2); + w->set(l, l2, im); + w->setWindowTitle(ents.first()->bean->DisplayTypeAndName()); + QSizePolicy sizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + sizePolicy.setHeightForWidth(true); + w->setSizePolicy(sizePolicy); + w->exec(); + w->deleteLater(); +} + +void MainWindow::on_menu_scan_qr_triggered() { + if (!NekoRay::profileManager->CurrentGroup()->url.isEmpty()) return; +#ifndef NKR_NO_EXTERNAL + using namespace ZXingQt; + + hide(); + QThread::sleep(1); + + auto screen = QGuiApplication::primaryScreen(); + auto geom = screen->geometry(); + auto qpx = screen->grabWindow(0, geom.x(), geom.y(), geom.width(), geom.height()); + + show(); + + auto hints = DecodeHints() + .setFormats(BarcodeFormat::QRCode) + .setTryRotate(false) + .setBinarizer(Binarizer::FixedThreshold); + + auto result = ReadBarcode(qpx.toImage(), hints); + const auto &text = result.text(); + if (text.isEmpty()) { + MessageBoxInfo("NekoRay", tr("QR Code not found")); + } else { + NekoRay::sub::groupUpdater->AsyncUpdate(text); + } +#endif +} + +void MainWindow::on_menu_clear_test_result_triggered() { + for (const auto &profile: NekoRay::profileManager->profiles) { + if (NekoRay::dataStore->current_group != profile->gid) continue; + profile->latency = 0; + profile->full_test_report = ""; + } + refresh_proxy_list(); +} + +void MainWindow::on_menu_select_all_triggered() { + ui->proxyListTable->selectAll(); +} + +void MainWindow::on_menu_delete_repeat_triggered() { + QList> out; + QList> out_del; + + NekoRay::ProfileFilter::Uniq(NekoRay::profileManager->CurrentGroup()->Profiles(), out, true, false); + NekoRay::ProfileFilter::OnlyInSrc_ByPointer(NekoRay::profileManager->CurrentGroup()->Profiles(), out, out_del); + + QString remove_display; + for (const auto &ent: out_del) { + remove_display += ent->bean->DisplayTypeAndName() + "\n"; + } + if (out_del.length() > 0 && + QMessageBox::question(this, + tr("Confirmation"), + tr("Remove %1 item(s) ?").arg(out_del.length()) + "\n" + remove_display + ) == QMessageBox::StandardButton::Yes) { + + for (const auto &ent: out_del) { + NekoRay::profileManager->DeleteProfile(ent->id); + } + refresh_proxy_list(); + } +} + +void MainWindow::on_menu_remove_unavailable_triggered() { + QList> out_del; + + for (const auto &profile: NekoRay::profileManager->profiles) { + if (NekoRay::dataStore->current_group != profile->gid) continue; + if (profile->latency < 0) out_del += profile; + } + + QString remove_display; + for (const auto &ent: out_del) { + remove_display += ent->bean->DisplayTypeAndName() + "\n"; + } + if (out_del.length() > 0 && + QMessageBox::question(this, + tr("Confirmation"), + tr("Remove %1 item(s) ?").arg(out_del.length()) + "\n" + remove_display + ) == QMessageBox::StandardButton::Yes) { + + for (const auto &ent: out_del) { + NekoRay::profileManager->DeleteProfile(ent->id); + } + refresh_proxy_list(); + } +} + +void MainWindow::on_proxyListTable_customContextMenuRequested(const QPoint &pos) { + ui->menu_server->popup(ui->proxyListTable->viewport()->mapToGlobal(pos)); //弹出菜单 +} + +QMap> MainWindow::get_now_selected() { + auto items = ui->proxyListTable->selectedItems(); + QMap> map; + for (auto item: items) { + auto id = item->data(114514).toInt(); + auto ent = NekoRay::profileManager->GetProfile(id); + if (ent != nullptr) map[id] = ent; + } + return map; +} + +void MainWindow::keyPressEvent(QKeyEvent *event) { + switch (event->key()) { + case Qt::Key_Escape: + // take over by shortcut_esc + break; + case Qt::Key_Return: { + neko_start(); + break; + } + case Qt::Key_Delete: { + on_menu_delete_triggered(); + break; + } + default: + QMainWindow::keyPressEvent(event); + } +} + +// Log + +inline void FastAppendTextDocument(const QString &message, QTextDocument *doc) { + QTextCursor cursor(doc); + cursor.movePosition(QTextCursor::End); + cursor.beginEditBlock(); + cursor.insertBlock(); + cursor.insertText(message); + cursor.endEditBlock(); +} + +void MainWindow::show_log_impl(const QString &log) { + if (log.trimmed().isEmpty()) return; + + FastAppendTextDocument(log, qvLogDocument); + // qvLogDocument->setPlainText(qvLogDocument->toPlainText() + log); + // From https://gist.github.com/jemyzhang/7130092 + auto maxLines = 200; + auto block = qvLogDocument->begin(); + + while (block.isValid()) { + if (qvLogDocument->blockCount() > maxLines) { + QTextCursor cursor(block); + block = block.next(); + cursor.select(QTextCursor::BlockUnderCursor); + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); + cursor.removeSelectedText(); + continue; + } + break; + } +} + +void MainWindow::on_masterLogBrowser_customContextMenuRequested(const QPoint &pos) { + QMenu *menu = ui->masterLogBrowser->createStandardContextMenu(); + + auto sep = new QAction; + sep->setSeparator(true); + menu->addAction(sep); + + auto action_clear = new QAction; + action_clear->setText(tr("Clear")); + action_clear->setData(-1); + connect(action_clear, &QAction::triggered, this, [=] { + qvLogDocument->clear(); + }); + menu->addAction(action_clear); + + menu->exec(ui->masterLogBrowser->viewport()->mapToGlobal(pos)); //弹出菜单 +} + +// eventFilter + +bool MainWindow::eventFilter(QObject *obj, QEvent *event) { + if (event->type() == QEvent::MouseButtonPress) { + auto mouseEvent = dynamic_cast(event); + + if (obj == ui->label_running && mouseEvent->button() == Qt::LeftButton && running != nullptr) { + test_current(); + return true; + } else if (obj == ui->label_inbound && mouseEvent->button() == Qt::LeftButton) { + on_menu_basic_settings_triggered(); + return true; + } + } + return QMainWindow::eventFilter(obj, event); +} + +// profile selector + +void MainWindow::start_select_mode(QObject *context, const std::function &callback) { + select_mode = true; + connectOnce(this, &MainWindow::profile_selected, context, callback); + refresh_status(); +} + +// 连接列表 + +inline QJsonArray last_arr; + +void MainWindow::refresh_connection_list(const QJsonArray &arr) { + if (last_arr == arr) { + return; + } + last_arr = arr; + + ui->tableWidget_conn->setRowCount(0); + + int row = -1; + for (const auto &_item: arr) { + auto item = _item.toObject(); + + row++; + ui->tableWidget_conn->insertRow(row); + + // C0: Status + auto *f = new QTableWidgetItem(""); + f->setData(114514, item["ID"].toInt()); + auto start_t = item["Start"].toInt(); + auto end_t = item["End"].toInt(); + if (end_t > 0) { + f->setText(tr("End")); + } else { + f->setText(tr("Active")); + } + f->setToolTip(tr("Start: %1\nEnd: %2").arg( + DisplayTime(start_t), + end_t > 0 ? DisplayTime(end_t) : "" + )); + ui->tableWidget_conn->setItem(row, 0, f); + + // C1: Outbound + f = f->clone(); + f->setToolTip(""); + f->setText(item["Tag"].toString()); + ui->tableWidget_conn->setItem(row, 1, f); + + // C2: Destination + f = f->clone(); + QString target1 = item["Dest"].toString(); + QString target2 = item["RDest"].toString(); + if (target2.isEmpty() || target1 == target2) { + target2 = ""; + } + f->setText("[" + target1 + "] " + target2); + ui->tableWidget_conn->setItem(row, 2, f); + } +} + + +// Hotkey + +#ifndef NKR_NO_EXTERNAL + +#include + +inline QList> RegisteredHotkey; + +void MainWindow::RegisterHotkey(bool unregister) { + while (!RegisteredHotkey.isEmpty()) { + auto hk = RegisteredHotkey.takeFirst(); + hk->deleteLater(); + } + if (unregister) return; + + QStringList regstr; + regstr += NekoRay::dataStore->hotkey_mainwindow; + regstr += NekoRay::dataStore->hotkey_group; + regstr += NekoRay::dataStore->hotkey_route; + + for (const auto &key: regstr) { + if (key.isEmpty()) continue; + if (regstr.count(key) > 1) return; // Conflict hotkey + } + for (const auto &key: regstr) { + QKeySequence k(key); + if (k.isEmpty()) continue; + auto hk = QSharedPointer(new QHotkey(k, true)); + if (hk->isRegistered()) { + RegisteredHotkey += hk; + connect(hk.get(), &QHotkey::activated, this, [=] { HotkeyEvent(key); }); + } else { + hk->deleteLater(); + } + } +} + +void MainWindow::HotkeyEvent(const QString &key) { + if (key.isEmpty()) return; + runOnUiThread([=] { + if (key == NekoRay::dataStore->hotkey_mainwindow) { + tray->activated(QSystemTrayIcon::ActivationReason::Trigger); + } else if (key == NekoRay::dataStore->hotkey_group) { + on_menu_manage_groups_triggered(); + } else if (key == NekoRay::dataStore->hotkey_route) { + on_menu_routing_settings_triggered(); + } + }); +} + +#else + +void MainWindow::RegisterHotkey(bool unregister) {} + +void MainWindow::HotkeyEvent(const QString &key) {} + +#endif + +// VPN Launcher + +//inline QString mshta_exec_uac = R"(vbscript:CreateObject("Shell.Application").ShellExecute("%1","%2","","runas",1)(window.close))"; + +bool MainWindow::StartVPNProcess() { + // + if (vpn_pid != 0) { + return true; + } +#ifdef Q_OS_WIN + auto configFn = ":/nekoray/vpn/sing-box-vpn.json"; + if (QFile::exists("vpn/sing-box-vpn.json")) configFn = "vpn/sing-box-vpn.json"; + auto config = ReadFileText(configFn).replace("%PORT%", Int2String(NekoRay::dataStore->inbound_socks_port)); +#else + auto protectPath = QDir::currentPath() + "/protect"; + auto configFn = ":/nekoray/vpn/vpn-run-root.sh"; + if (QFile::exists("vpn/vpn-run-root.sh")) configFn = "vpn/vpn-run-root.sh"; + auto config = ReadFileText(configFn) + .replace("$PORT", Int2String(NekoRay::dataStore->inbound_socks_port)) + .replace("$USE_NEKORAY", "1") + .replace("./nekoray_core", QApplication::applicationDirPath() + "/nekoray_core") + .replace("./tun2socks", QApplication::applicationDirPath() + "/tun2socks") + .replace("$PROTECT_LISTEN_PATH", protectPath) + .replace("$TUN_NAME", "nekoray-tun") + .replace("$USER_ID", Int2String((int) getuid())) + .replace("$TABLE_FWMARK", "514"); +#endif + // + QFile file; + file.setFileName(QFileInfo(configFn).fileName()); + file.open(QIODevice::ReadWrite | QIODevice::Truncate); + file.write(config.toUtf8()); + file.close(); + auto configPath = QFileInfo(file).absoluteFilePath(); + // +#ifdef Q_OS_WIN + runOnNewThread([=] { + vpn_pid = 1; //TODO get pid? + WinCommander::runProcessElevated(QApplication::applicationDirPath() + "/sing-box.exe", + {"run", "-c", configPath}); // blocking + vpn_pid = 0; + runOnUiThread([=] { + neko_set_spmode(NekoRay::SystemProxyMode::DISABLE); + }); + }); +#else + QFile::remove(protectPath); + if (QFile::exists(protectPath)) { + MessageBoxWarning("Error", "protect cannot be removed"); + return false; + } + // + auto vpn_process = new QProcess; + QProcess::connect(vpn_process, &QProcess::stateChanged, mainwindow, [=](QProcess::ProcessState state) { + if (state == QProcess::NotRunning) { + if (watcher != nullptr) { + watcher->deleteLater(); + watcher = nullptr; + } + vpn_pid = 0; + vpn_process->deleteLater(); + GetMainWindow()->neko_set_spmode(NekoRay::SystemProxyMode::DISABLE); + } + }); + if (watcher != nullptr) { + watcher->deleteLater(); + watcher = nullptr; + } + watcher = new QFileSystemWatcher({QDir::currentPath()}); + connect(watcher, &QFileSystemWatcher::directoryChanged, this, [=] { + if (!QFile::exists(protectPath)) return; + if (!Tun2rayStartStop(true)) { + neko_set_spmode(NekoRay::SystemProxyMode::DISABLE); + } + if (watcher != nullptr) { + watcher->deleteLater(); + watcher = nullptr; + } + }); + // + vpn_process->setProcessChannelMode(QProcess::ForwardedChannels); + vpn_process->start("pkexec", {"bash", configPath}); + vpn_process->waitForStarted(); + vpn_pid = vpn_process->processId(); // actually it's pkexec or bash PID +#endif + return true; +} + +bool MainWindow::StopVPNProcess() { + if (vpn_pid != 0) { + bool ok; +#ifdef Q_OS_WIN + auto ret = WinCommander::runProcessElevated("taskkill", {"/IM", "sing-box.exe"}); + ok = ret == 0; +#else + QProcess p; + p.start("pkexec", {"pkill", "-2", "-P", Int2String(vpn_pid)}); + p.waitForFinished(); + ok = p.exitCode() == 0; +#endif + if (ok) { + vpn_pid = 0; + Tun2rayStartStop(false); + } else { + MessageBoxWarning(tr("Error"), tr("Failed to stop VPN process")); + } + return ok; + } + return true; +} diff --git a/ui/mainwindow.h b/ui/mainwindow.h new file mode 100644 index 0000000..f6c3a7d --- /dev/null +++ b/ui/mainwindow.h @@ -0,0 +1,175 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "GroupSort.hpp" + +#include "db/ProxyEntity.hpp" +#include "db/Group.hpp" +#include "main/GuiUtils.hpp" + +class QFileSystemWatcher; + +QT_BEGIN_NAMESPACE +namespace Ui { + class MainWindow; +} +QT_END_NAMESPACE + +class MainWindow : public QMainWindow { +Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + + ~MainWindow(); + + void refresh_proxy_list(const int &id = -1) { refresh_proxy_list_impl(id, {}); }; + + void show_group(int gid); + + void refresh_groups(); + + void refresh_status(const QString &traffic_update = ""); + + void neko_start(int _id = -1); + + void neko_stop(bool crash = false); + + void neko_set_spmode(int mode, bool save = true); + + void show_log_impl(const QString &log); + + void start_select_mode(QObject *context, const std::function &callback); + + void refresh_connection_list(const QJsonArray &arr); + + void RegisterHotkey(bool unregister); + +signals: + + void profile_selected(int id); + +public slots: + + void on_menu_exit_triggered(); + +private slots: + + void on_masterLogBrowser_customContextMenuRequested(const QPoint &pos); + + void on_menu_basic_settings_triggered(); + + void on_menu_routing_settings_triggered(); + + void on_menu_hotkey_settings_triggered(); + + void on_menu_add_from_input_triggered(); + + void on_menu_add_from_clipboard_triggered(); + + void on_menu_move_triggered(); + + void on_menu_delete_triggered(); + + void on_menu_reset_traffic_triggered(); + + void on_menu_profile_debug_info_triggered(); + + void on_menu_copy_links_triggered(); + + void on_menu_export_config_triggered(); + + void display_qr_link(bool nkrFormat = false); + + void on_menu_scan_qr_triggered(); + + void on_menu_clear_test_result_triggered(); + + void on_menu_manage_groups_triggered(); + + void on_menu_select_all_triggered(); + + void on_menu_delete_repeat_triggered(); + + void on_menu_remove_unavailable_triggered(); + + void on_proxyListTable_itemDoubleClicked(QTableWidgetItem *item); + + void on_proxyListTable_customContextMenuRequested(const QPoint &pos); + + void on_tabWidget_currentChanged(int index); + +private: + Ui::MainWindow *ui; + QSystemTrayIcon *tray; + QShortcut *shortcut_ctrl_f = new QShortcut(QKeySequence("Ctrl+F"), this); + QShortcut *shortcut_ctrl_v = new QShortcut(QKeySequence("Ctrl+V"), this); + QShortcut *shortcut_esc = new QShortcut(QKeySequence("Esc"), this); + // + bool core_process_killed = false; + bool core_process_show_stderr = false; + QProcess *core_process = nullptr; + qint64 vpn_pid = 0; + QFileSystemWatcher *watcher = nullptr; + // + bool qvLogAutoScoll = true; + QTextDocument *qvLogDocument = new QTextDocument(this); + // + QString title_base; + QString title_error; + int title_spmode = NekoRay::SystemProxyMode::DISABLE; + QSharedPointer running; + QString traffic_update_cache; + QTime last_test_time; + // + int proxy_last_order = -1; + bool select_mode = false; + bool exit_update = false; + + QMap> get_now_selected(); + + void dialog_message_impl(const QString &sender, const QString &info); + + void refresh_proxy_list_impl(const int &id = -1, NekoRay::GroupSortAction groupSortAction = {}); + + void keyPressEvent(QKeyEvent *event) override; + + void closeEvent(QCloseEvent *event) override; + + // + + void HotkeyEvent(const QString &key); + + bool StartVPNProcess(); + + bool StopVPNProcess(); + + + // grpc and ... + + static void ExitNekorayCore(); + + void speedtest_current_group(int mode); + + void test_current(); + + void setup_grpc(); + + void CheckUpdate(); + + bool Tun2rayStartStop(bool start); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; +}; + +inline MainWindow *GetMainWindow() { + return (MainWindow *) mainwindow; +} diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui new file mode 100644 index 0000000..6cebde4 --- /dev/null +++ b/ui/mainwindow.ui @@ -0,0 +1,770 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + + 800 + 600 + + + + NekoRay + + + + + + + + + Program + + + + .. + + + + 24 + 24 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextUnderIcon + + + + + + + Preferences + + + + .. + + + + 24 + 24 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextUnderIcon + + + + + + + Server + + + + .. + + + + 24 + 24 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextUnderIcon + + + + + + + Ads + + + + .. + + + + 24 + 24 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextUnderIcon + + + + + + + Document + + + + .. + + + + 24 + 24 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextUnderIcon + + + + + + + Update + + + + .. + + + + 24 + 24 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonTextUnderIcon + + + + + + + + 0 + 0 + + + + VPN Mode + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + true + + + + + + + + + Qt::Vertical + + + + 0 + + + true + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::CustomContextMenu + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + 16 + + + false + + + false + + + 26 + + + + + + + + + Type + + + + + Address + + + + + Name + + + + + Test Result + + + + + Traffic + + + + + + + + + + + + + 0 + + + + Log + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::CustomContextMenu + + + false + + + + + + + + Connection + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::CustomContextMenu + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + false + + + + Status + + + + + Outbound + + + + + Destination + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 0 + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 800 + 32 + + + + + Program + + + + System Proxy + + + + + + + + Preferences + + + + + + Active Server + + + + + + Active Routing + + + + + + + + + + + + + + + + + + + Preferences + + + + + + + + + Server + + + + Share + + + + + + + + + + Current Group + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Exit + + + + + Basic Settings + + + + + New profile + + + + + Groups + + + + + Start [ Enter ] + + + + + Stop + + + + + Routes + + + + + New profile from clipboard + + + + + Delete [ Delete ] + + + + + Add profile from clipboard + + + + + Debug Info + + + + + QR Code and link + + + + + Copy Link + + + + + Tcp Ping + + + + + Url Test + + + + + Clear Test Result + + + + + Export V2ray config + + + + + Reset Traffic + + + + + Scan QR Code + + + + + true + + + Enable System Proxy + + + + + true + + + Disable + + + + + Delete Repeat + + + + + fake + + + false + + + + + Move + + + + + true + + + Start with system + + + + + true + + + Remember last profile + + + + + true + + + Start minimal + + + + + Remove Unavailable + + + + + Full Test + + + + + Hot key + + + + + Select All + + + + + QR Code and link (Nekoray) + + + + + fake + + + false + + + + + fake + + + false + + + + + Copy links of selected + + + + + true + + + Enable VPN + + + + + + MyTableWidget + QTableWidget +
ui/widget/MyTableWidget.h
+
+
+ + +
diff --git a/ui/mainwindow_grpc.cpp b/ui/mainwindow_grpc.cpp new file mode 100644 index 0000000..f57d3f1 --- /dev/null +++ b/ui/mainwindow_grpc.cpp @@ -0,0 +1,384 @@ +#include "./ui_mainwindow.h" +#include "mainwindow.h" + +#include "db/Database.hpp" +#include "db/ConfigBuilder.hpp" +#include "db/TrafficLooper.hpp" +#include "rpc/gRPC.h" + +#include +#include +#include +#include +#include +#include + +#ifndef NKR_NO_GRPC +using namespace NekoRay::rpc; +#endif + +void MainWindow::setup_grpc() { +#ifndef NKR_NO_GRPC + // Setup Connection + defaultClient = new Client([=](const QString &errStr) { + showLog("gRPC Error: " + errStr); + }, "127.0.0.1:" + Int2String(NekoRay::dataStore->core_port), NekoRay::dataStore->core_token); + auto t = new QTimer(); + connect(t, &QTimer::timeout, this, [=]() { + bool ok = defaultClient->KeepAlive(); + runOnUiThread([=]() { + if (!ok) { + title_error = tr("Error"); + } else { + title_error = ""; + } + refresh_status(); + }); + }); + auto tt = new QThread; + tt->start(); + t->moveToThread(tt); + runOnUiThread([=] { + t->start(2000); + }, t); + + // Looper + runOnNewThread([=] { NekoRay::traffic::trafficLooper->loop(); }); +#endif +} + +// 测速 + +inline bool speedtesting = false; + +void MainWindow::speedtest_current_group(int mode) { +#ifndef NKR_NO_GRPC + if (speedtesting) return; + + QStringList full_test_flags; + if (mode == libcore::FullTest) { + bool ok; + auto s = QInputDialog::getText(nullptr, tr("Input"), + tr("Please enter the items to be tested, separated by commas\n" + "1. Latency\n" + "2. Download speed\n" + "3. In and Out IP\n" + "4. NAT type"), + QLineEdit::Normal, "1,2,3,4", &ok); + full_test_flags = s.trimmed().split(","); + if (!ok) return; + } + + speedtesting = true; + + runOnNewThread([=]() { + auto group = NekoRay::profileManager->CurrentGroup(); + if (group->archive) return; + auto order = ui->proxyListTable->order;//copy + + QList> profiles; + QMutex lock; + QMutex lock2; + int threadN = mode == libcore::FullTest ? 1 : NekoRay::dataStore->test_concurrent; + int threadN_finished = 0; + + // 这个是按照显示的顺序 + for (auto id: order) { + auto profile = NekoRay::profileManager->GetProfile(id); + if (profile != nullptr) profiles += profile; + } + + // Threads + for (int i = 0; i < threadN; i++) { + runOnNewThread([&] { + forever { + // + lock.lock(); + if (profiles.isEmpty()) { + threadN_finished++; + if (threadN == threadN_finished) lock2.unlock(); + lock.unlock(); + return; + } + auto profile = profiles.takeFirst(); + lock.unlock(); + + // + libcore::TestReq req; + req.set_mode((libcore::TestMode) mode); + req.set_timeout(3000); + req.set_url(NekoRay::dataStore->test_url.toStdString()); + + // + QList ext; + + if (mode == libcore::TestMode::UrlTest || mode == libcore::FullTest) { + auto c = NekoRay::BuildConfig(profile, true); + // external test ??? + if (!c->ext.isEmpty()) { + ext = c->ext; + for (auto extC: ext) { + extC->Start(); + } + QThread::msleep(500); + } + // + auto config = new libcore::LoadConfigReq; + config->set_coreconfig(QJsonObject2QString(c->coreConfig, true).toStdString()); + req.set_allocated_config(config); + req.set_in_address(profile->bean->serverAddress.toStdString()); + + req.set_full_latency(full_test_flags.contains("1")); + req.set_full_speed(full_test_flags.contains("2")); + req.set_full_in_out(full_test_flags.contains("3")); + req.set_full_nat(full_test_flags.contains("4")); + } else if (mode == libcore::TcpPing) { + req.set_address(profile->bean->DisplayAddress().toStdString()); + } + + bool rpcOK; + auto result = defaultClient->Test(&rpcOK, req); + for (auto extC: ext) { + extC->Kill(); + extC->deleteLater(); + } + if (!rpcOK) return; + + profile->latency = result.ms(); + if (profile->latency == 0) profile->latency = -1; // sn + profile->full_test_report = result.full_report().c_str(); + + runOnUiThread([=] { + if (!result.error().empty()) { + show_log_impl( + tr("[%1] test error: %2").arg(profile->bean->DisplayTypeAndName(), + result.error().c_str())); + } + refresh_proxy_list(profile->id); + }); + } + }); + } + + // Control + lock2.lock(); + lock2.lock(); + lock2.unlock(); + speedtesting = false; + }); +#endif +} + +void MainWindow::test_current() { +#ifndef NKR_NO_GRPC + last_test_time = QTime::currentTime(); + ui->label_running->setText(tr("Testing")); + + runOnNewThread([=] { + libcore::TestReq req; + req.set_mode(libcore::UrlTest); + req.set_timeout(3000); + req.set_url(NekoRay::dataStore->test_url.toStdString()); + + bool rpcOK; + auto result = defaultClient->Test(&rpcOK, req); + if (!rpcOK) return; + + auto latency = result.ms(); + last_test_time = QTime::currentTime(); + + runOnUiThread([=] { + if (latency <= 0) { + ui->label_running->setText(tr("Test Result") + ": " + tr("Unavailable")); + } else if (latency > 0) { + ui->label_running->setText(tr("Test Result") + ": " + QString("%1 ms").arg(latency)); + } + }); + }); +#endif +} + +void MainWindow::ExitNekorayCore() { +#ifndef NKR_NO_GRPC + NekoRay::rpc::defaultClient->Exit(); +#endif +} + +void MainWindow::neko_start(int _id) { + auto ents = get_now_selected(); + auto ent = (_id < 0 && !ents.isEmpty()) ? ents.first() : NekoRay::profileManager->GetProfile(_id); + if (ent == nullptr) return; + + if (select_mode) { + emit profile_selected(ent->id); + select_mode = false; + refresh_status(); + return; + } + + if (NekoRay::profileManager->GetGroup(ent->gid)->archive) return; + + auto result = NekoRay::BuildConfig(ent, false); + if (!result->error.isEmpty()) { + MessageBoxWarning("BuildConfig return error", result->error); + return; + } + + if (NekoRay::dataStore->started_id >= 0) neko_stop(); + show_log_impl(">>>>>>>> " + tr("Starting profile %1").arg(ent->bean->DisplayTypeAndName())); + auto insecure_hint = DisplayInsecureHint(ent->bean); + if (!insecure_hint.isEmpty()) show_log_impl(">>>>>>>> " + tr("Profile is insecure: %1").arg(insecure_hint)); + +#ifndef NKR_NO_GRPC + libcore::LoadConfigReq req; + req.set_coreconfig(QJsonObject2QString(result->coreConfig, true).toStdString()); + req.set_trydomains(result->tryDomains.join(",").toStdString()); + // + bool rpcOK; + QString error = defaultClient->Start(&rpcOK, req); + if (rpcOK && !error.isEmpty()) { + MessageBoxWarning("LoadConfig return error", error); + return; + } + // + NekoRay::traffic::trafficLooper->proxy = result->outboundStat.get(); + NekoRay::traffic::trafficLooper->items = result->outboundStats; + NekoRay::traffic::trafficLooper->loop_enabled = true; +#endif + + for (auto extC: result->ext) { + extC->Start(); + } + + NekoRay::dataStore->UpdateStartedId(ent->id); + running = ent; + refresh_status(); + refresh_proxy_list(ent->id); +} + +void MainWindow::neko_stop(bool crash) { + auto id = NekoRay::dataStore->started_id; + if (id < 0) return; + show_log_impl(">>>>>>>> " + tr("Stopping profile %1").arg(running->bean->DisplayTypeAndName())); + + while (!NekoRay::sys::running_ext.isEmpty()) { + auto extC = NekoRay::sys::running_ext.takeFirst(); + extC->Kill(); + extC->deleteLater(); + } + +#ifndef NKR_NO_GRPC + NekoRay::traffic::trafficLooper->loop_enabled = false; + NekoRay::traffic::trafficLooper->loop_mutex.lock(); + if (NekoRay::dataStore->traffic_loop_interval != 0) { + NekoRay::traffic::trafficLooper->update_all(); + for (const auto &item: NekoRay::traffic::trafficLooper->items) { + NekoRay::profileManager->GetProfile(item->id)->Save(); + refresh_proxy_list(item->id); + } + } + NekoRay::traffic::trafficLooper->loop_mutex.unlock(); + + if (!crash) { + bool rpcOK; + QString error = defaultClient->Stop(&rpcOK); + if (rpcOK && !error.isEmpty()) { + MessageBoxWarning("Stop return error", error); + return; + } + } +#endif + + NekoRay::dataStore->UpdateStartedId(-1919); + running = nullptr; + refresh_status(); + refresh_proxy_list(id); +} + +void MainWindow::CheckUpdate() { +#ifndef NKR_NO_GRPC + bool ok; + libcore::UpdateReq request; + request.set_action(libcore::UpdateAction::Check); + auto response = NekoRay::rpc::defaultClient->Update(&ok, request); + if (!ok) return; + + auto err = response.error(); + if (!err.empty()) { + runOnUiThread([=] { + MessageBoxWarning(QObject::tr("Update"), err.c_str()); + }); + return; + } + + if (response.release_download_url() == nullptr) { + runOnUiThread([=] { + MessageBoxInfo(QObject::tr("Update"), QObject::tr("No update")); + }); + return; + } + + runOnUiThread([=] { + QMessageBox box(QMessageBox::Question, QObject::tr("Update"), + QObject::tr("Update found: %1\nRelease note:\n%2") + .arg(response.assets_name().c_str(), response.release_note().c_str())); + QAbstractButton *btn1; + if (!NekoRay::dataStore->flag_use_appdata) { + btn1 = box.addButton(QObject::tr("Update"), QMessageBox::AcceptRole); + } + QAbstractButton *btn2 = box.addButton(QObject::tr("Open in browser"), QMessageBox::AcceptRole); + box.addButton(QObject::tr("Close"), QMessageBox::RejectRole); + box.exec(); + + if (btn1 == box.clickedButton()) { + // Download Update + runOnNewThread([=] { + bool ok2; + libcore::UpdateReq request2; + request2.set_action(libcore::UpdateAction::Download); + auto response2 = NekoRay::rpc::defaultClient->Update(&ok2, request2); + runOnUiThread([=] { + if (response2.error().empty()) { + auto q = QMessageBox::question(nullptr, QObject::tr("Update"), + QObject::tr("Update is ready, restart to install?")); + if (q == QMessageBox::StandardButton::Yes) { + this->exit_update = true; + on_menu_exit_triggered(); + } + } else { + MessageBoxWarning(QObject::tr("Update"), response2.error().c_str()); + } + }); + }); + } else if (btn2 == box.clickedButton()) { + QDesktopServices::openUrl(QUrl(response.release_url().c_str())); + } + }); +#endif +} + +bool MainWindow::Tun2rayStartStop(bool start) { +#ifndef NKR_NO_GRPC + // For Linux only currently (check in go) + bool ok; + if (start) { + libcore::SetTunReq req; + req.set_name("nekoray-tun"); + req.set_mtu(1500); + req.set_implementation(0); + req.set_fakedns(NekoRay::dataStore->fake_dns); + auto error = defaultClient->SetTun(&ok, req); + if (!ok) return false; + if (!error.isEmpty()) { + MessageBoxWarning("Error", "Failed to start Tun2ray: " + error); + return false; + } + } else { + libcore::SetTunReq req; + req.set_implementation(-1); + defaultClient->SetTun(&ok, req); + if (!ok) return false; + } +#endif + return true; +} diff --git a/ui/widget/GroupItem.cpp b/ui/widget/GroupItem.cpp new file mode 100644 index 0000000..8feca6f --- /dev/null +++ b/ui/widget/GroupItem.cpp @@ -0,0 +1,120 @@ +#include "GroupItem.h" +#include "ui_GroupItem.h" + +#include "ui/edit/dialog_edit_group.h" +#include "main/GuiUtils.hpp" +#include "sub/GroupUpdater.hpp" + +#include + +QString ParseSubInfo(const QString &info) { + if (info.trimmed().isEmpty()) return ""; + + QString result; + + long long used = 0; + long long total = 0; + long long expire = 0; + + + auto re0m = QRegularExpression("total=([0-9]+)").match(info); + if (re0m.lastCapturedIndex() >= 1) { + total = re0m.captured(1).toLongLong(); + } else { + return ""; + } + auto re1m = QRegularExpression("upload=([0-9]+)").match(info); + if (re1m.lastCapturedIndex() >= 1) { + used += re1m.captured(1).toLongLong(); + } + auto re2m = QRegularExpression("download=([0-9]+)").match(info); + if (re2m.lastCapturedIndex() >= 1) { + used += re2m.captured(1).toLongLong(); + } + auto re3m = QRegularExpression("expire=([0-9]+)").match(info); + if (re3m.lastCapturedIndex() >= 1) { + expire = re3m.captured(1).toLongLong(); + } + + result = QObject::tr("Used: %1 Remain: %2 Expire: %3") + .arg(ReadableSize(used), ReadableSize(total - used), DisplayTime(expire, QLocale::ShortFormat)); + + return result; +} + +GroupItem::GroupItem(QWidget *parent, const QSharedPointer &ent, QListWidgetItem *item) : + QWidget(parent), ui(new Ui::GroupItem) { + ui->setupUi(this); + + this->ent = ent; + this->item = item; + if (ent == nullptr) return; + + connect(this, &GroupItem::edit_clicked, this, &GroupItem::on_edit_clicked); + + refresh_data(); +} + +GroupItem::~GroupItem() { + delete ui; +} + +void GroupItem::refresh_data() { + ui->name->setText(ent->name); + + auto type = ent->url.isEmpty() ? tr("Basic") : tr("Subscription"); + if (ent->archive) type = tr("Archive") + " " + type; + type += " (" + Int2String(ent->Profiles().length()) + ")"; + ui->type->setText(type); + + if (ent->url.isEmpty()) { + ui->url->hide(); + ui->subinfo->hide(); + ui->update_sub->hide(); + } else { + ui->url->setText(ent->url); + auto subinfo = ParseSubInfo(ent->info); + if (subinfo.isEmpty()) { + ui->subinfo->hide(); + } else { + ui->subinfo->show(); + ui->subinfo->setText(subinfo); + } + } + runOnUiThread([=] { + adjustSize(); + item->setSizeHint(sizeHint()); + dynamic_cast(parent())->adjustSize(); + }, this); +} + +void GroupItem::on_update_sub_clicked() { + if (QMessageBox::question(this, tr("Confirmation"), tr("Update %1?").arg(ent->name)) + == QMessageBox::StandardButton::Yes) { + NekoRay::sub::groupUpdater->AsyncUpdate(ent->url, ent->id, this, [=] { + refresh_data(); + }); + } +} + +void GroupItem::on_edit_clicked() { + auto dialog = new DialogEditGroup(ent, this); + int ret = dialog->exec(); + dialog->deleteLater(); + + if (ret == QDialog::Accepted) { + ent->Save(); + refresh_data(); + dialog_message(Dialog_DialogManageGroups, "refresh" + Int2String(ent->id)); + } +} + +void GroupItem::on_remove_clicked() { + if (NekoRay::profileManager->groups.count() == 1) return; + if (QMessageBox::question(this, tr("Confirmation"), tr("Remove %1?").arg(ent->name)) == + QMessageBox::StandardButton::Yes) { + NekoRay::profileManager->DeleteGroup(ent->id); + dialog_message(Dialog_DialogManageGroups, "refresh-1"); + delete item; + } +} diff --git a/ui/widget/GroupItem.h b/ui/widget/GroupItem.h new file mode 100644 index 0000000..8178c4b --- /dev/null +++ b/ui/widget/GroupItem.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "db/Database.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class GroupItem; } +QT_END_NAMESPACE + +class GroupItem : public QWidget { +Q_OBJECT + +public: + explicit GroupItem(QWidget *parent, const QSharedPointer &ent, QListWidgetItem *item); + + ~GroupItem() override; + + void refresh_data(); + + QSharedPointer ent; + QListWidgetItem *item; + +private: + Ui::GroupItem *ui; + +signals: + + void edit_clicked(); + +private slots: + + void on_update_sub_clicked(); + + void on_edit_clicked(); + + void on_remove_clicked(); +}; diff --git a/ui/widget/GroupItem.ui b/ui/widget/GroupItem.ui new file mode 100644 index 0000000..fd2f77f --- /dev/null +++ b/ui/widget/GroupItem.ui @@ -0,0 +1,128 @@ + + + GroupItem + + + + 0 + 0 + 403 + 300 + + + + + 0 + 0 + + + + + + + + + + + + + 0 + 0 + + + + color: rgb(251, 114, 153); + + + Type + + + + + + + + 0 + 0 + + + + Name + + + + + + + + 0 + 0 + + + + Update Subscription + + + + + + + + 0 + 0 + + + + Edit + + + + + + + + 0 + 0 + + + + Remove + + + + + + + + + + 0 + 0 + + + + color: rgb(102, 102, 102); + + + Url + + + + + + + + 0 + 0 + + + + 订阅流量信息 + + + + + + + + diff --git a/ui/widget/MyLineEdit.h b/ui/widget/MyLineEdit.h new file mode 100644 index 0000000..90dee9e --- /dev/null +++ b/ui/widget/MyLineEdit.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +class MyLineEdit : public QLineEdit { +public slots: + + explicit MyLineEdit(QWidget *parent = nullptr) : QLineEdit(parent) { + } + + void setText(const QString &s) { + QLineEdit::setText(s); + QLineEdit::home(false); + } +}; diff --git a/ui/widget/MyTableWidget.h b/ui/widget/MyTableWidget.h new file mode 100644 index 0000000..ea7d014 --- /dev/null +++ b/ui/widget/MyTableWidget.h @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include +#include +#include + +class MyTableWidget : public QTableWidget { +public: + explicit MyTableWidget(QWidget *parent = nullptr) : QTableWidget(parent) { + // 拖拽设置 + this->setDragDropMode(QAbstractItemView::InternalMove); // 内部移动 + this->setDropIndicatorShown(true); // drop位置 提示 + this->setSelectionBehavior(QAbstractItemView::SelectRows); + }; + +// takes and returns the whole row + QList takeRow(int row) { + QList rowItems; + for (int col = 0; col < columnCount(); ++col) { + rowItems << takeItem(row, col); + } + return rowItems; + } + +// sets the whole row + void setRow(int row, const QList &rowItems) { + for (int col = 0; col < columnCount(); ++col) { + setItem(row, col, rowItems.at(col)); + } + } + + QList order; // id sorted + std::function callback_save_order; + std::map id2Row; + + void _save_order(bool saveToFile) { + order.clear(); + id2Row.clear(); + for (int i = 0; i < this->rowCount(); i++) { + auto id = this->item(i, 0)->data(114514).toInt(); + order += id; + id2Row[id] = i; + } + if (callback_save_order != nullptr && saveToFile) + callback_save_order(); + } + + void update_order(bool saveToFile) { + if (order.isEmpty()) { + _save_order(false); + return; + } + + // 纠错: order 里面含有不在当前表格控件的 id + bool needSave = false; + auto deleted_profiles = order; + for (int i = 0; i < this->rowCount(); i++) { + auto id = this->item(i, 0)->data(114514).toInt(); + deleted_profiles.removeAll(id); + } + for (auto deleted_profile: deleted_profiles) { + needSave = true; + order.removeAll(deleted_profile); + } + + QMap> newRows; + for (int i = 0; i < this->rowCount(); i++) { + auto id = this->item(i, 0)->data(114514).toInt(); + auto dst = order.indexOf(id); + if (dst == i) continue; + if (dst == -1) { + // 纠错: 新的profile不需要移动 + needSave = true; + continue; + } + newRows[dst] = takeRow(i); + } + + for (int i = 0; i < this->rowCount(); i++) { + if (!newRows.contains(i)) continue; + setRow(i, newRows[i]); + } + + // Then save the order + _save_order(needSave || saveToFile); + }; + +protected: + +/* + * 2021.7.6 by gy + * 拖拽 继承QTableWidget overwrite dropEvent事件 + * 功能:拖动一行到鼠标落下的位置 + * 注意:DragDropMode相关参数的设置 +*/ + void dropEvent(QDropEvent *event) override { + // 原行号与目标行号的确定 + int row_src, row_dst; + row_src = this->currentRow();// 原行号 可加if + QTableWidgetItem *item = this->itemAt(event->pos());// 获取落点的item + if (item != nullptr) { + // 判断是否为空 + row_dst = item->row();// 不为空 获取其行号 + // 保证鼠标落下的位置 就是拖拽的一行最后所移动到的位置(考虑插入新行 移除原行的上下变化) + row_src = (row_src > row_dst ? row_src + 1 : row_src);// 如果src在dst的下方(行号大),后续插入dst会影响src的行号 + row_dst = (row_src < row_dst ? row_dst + 1 : row_dst);// 如果src在dst的上方(行号小),后续移除src会影响dst的行号 + this->insertRow(row_dst);// 插入一行 + } else { + // 落点没有item 说明拖动到了最下面 + row_dst = this->rowCount();// 获取行总数 + this->insertRow(row_dst);// 在最后新增一行 + } + // 执行移动 并移除原行 + for (int i = 0; i < this->columnCount(); i++) { + // 遍历列 + this->setItem(row_dst, i, this->takeItem(row_src, i));// 每一列item的移动 + } + this->removeRow(row_src);// 删除原行 + + // Then save the order + _save_order(true); + }; +}; diff --git a/ui/widget/ProxyItem.cpp b/ui/widget/ProxyItem.cpp new file mode 100644 index 0000000..b64234a --- /dev/null +++ b/ui/widget/ProxyItem.cpp @@ -0,0 +1,41 @@ +#include "ProxyItem.h" +#include "ui_ProxyItem.h" + +#include + +ProxyItem::ProxyItem(QWidget *parent, const QSharedPointer &ent, QListWidgetItem *item) : + QWidget(parent), ui(new Ui::ProxyItem) { + ui->setupUi(this); + this->item = item; + this->ent = ent; + if (ent == nullptr) return; + + refresh_data(); +} + +ProxyItem::~ProxyItem() { + delete ui; +} + +void ProxyItem::refresh_data() { + ui->type->setText(ent->bean->DisplayType()); + ui->name->setText(ent->bean->DisplayName()); + ui->address->setText(ent->bean->DisplayAddress()); + ui->traffic->setText(ent->traffic_data->DisplayTraffic()); + ui->test_result->setText(ent->DisplayLatency()); + + runOnUiThread([=] { + adjustSize(); + item->setSizeHint(sizeHint()); + dynamic_cast(parent())->adjustSize(); + }, this); +} + +void ProxyItem::on_remove_clicked() { + if (!this->remove_confirm || + QMessageBox::question(this, tr("Confirmation"), tr("Remove %1?").arg(ent->bean->DisplayName())) + == QMessageBox::StandardButton::Yes) { + // TODO do remove (or not) -> callback + delete item; + } +} diff --git a/ui/widget/ProxyItem.h b/ui/widget/ProxyItem.h new file mode 100644 index 0000000..fb3533f --- /dev/null +++ b/ui/widget/ProxyItem.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "db/ProxyEntity.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class ProxyItem; } +QT_END_NAMESPACE + +class ProxyItem : public QWidget { +Q_OBJECT + +public: + explicit ProxyItem(QWidget *parent, const QSharedPointer &ent, QListWidgetItem *item); + + ~ProxyItem() override; + + void refresh_data(); + + QSharedPointer ent; + QListWidgetItem *item; + bool remove_confirm = false; + +private: + Ui::ProxyItem *ui; + +private slots: + + void on_remove_clicked(); +}; diff --git a/ui/widget/ProxyItem.ui b/ui/widget/ProxyItem.ui new file mode 100644 index 0000000..ce7915e --- /dev/null +++ b/ui/widget/ProxyItem.ui @@ -0,0 +1,99 @@ + + + ProxyItem + + + + 0 + 0 + 400 + 300 + + + + + 0 + 0 + + + + + + + + + + + + 名称 + + + + + + + + 0 + 0 + + + + Remove + + + + + + + + + + + color: rgb(102, 102, 102); + + + 地址 + + + + + + + 测试结果 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + color: rgb(251, 114, 153); + + + 类型 + + + + + + + 流量 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + diff --git a/updater/.gitignore b/updater/.gitignore new file mode 100644 index 0000000..7b3c510 --- /dev/null +++ b/updater/.gitignore @@ -0,0 +1,2 @@ +/updater +/launcher diff --git a/updater/go.mod b/updater/go.mod new file mode 100644 index 0000000..8a8b280 --- /dev/null +++ b/updater/go.mod @@ -0,0 +1,11 @@ +module updater + +go 1.18 + +require github.com/codeclysm/extract v2.2.0+incompatible + +require ( + github.com/h2non/filetype v1.1.3 // indirect + github.com/juju/errors v0.0.0-20220331221717-b38fca44723b // indirect + github.com/stretchr/testify v1.7.1 // indirect +) diff --git a/updater/go.sum b/updater/go.sum new file mode 100644 index 0000000..4949ed5 --- /dev/null +++ b/updater/go.sum @@ -0,0 +1,19 @@ +github.com/codeclysm/extract v2.2.0+incompatible h1:q3wyckoA30bhUSiwdQezMqVhwd8+WGE64/GL//LtUhI= +github.com/codeclysm/extract v2.2.0+incompatible/go.mod h1:2nhFMPHiU9At61hz+12bfrlpXSUrOnK+wR+KlGO4Uks= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/juju/errors v0.0.0-20220331221717-b38fca44723b h1:AxFeSQJfcm2O3ov1wqAkTKYFsnMw2g1B4PkYujfAdkY= +github.com/juju/errors v0.0.0-20220331221717-b38fca44723b/go.mod h1:jMGj9DWF/qbo91ODcfJq6z/RYc3FX3taCBZMCcpI4Ls= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/updater/launcher_linux.go b/updater/launcher_linux.go new file mode 100644 index 0000000..0593d8d --- /dev/null +++ b/updater/launcher_linux.go @@ -0,0 +1,77 @@ +package main + +import ( + "flag" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" +) + +var local_qt_theme bool + +func Launcher() { + log.Println("Running as launcher") + wd, _ := filepath.Abs(".") + + _debug := flag.Bool("debug", false, "Debug mode") + flag.BoolVar(&local_qt_theme, "theme", false, "Use local QT theme (unstable)") + flag.Parse() + + // Find & symlink some Qt Plugin to enable system theme + tryLinkQtPlugin("styles", !local_qt_theme) + tryLinkQtPlugin("platformthemes", !local_qt_theme) + + cmd := exec.Command("./nekoray", flag.Args()...) + cmd.Env = os.Environ() + ld_env := "LD_LIBRARY_PATH=" + filepath.Join(wd, "./usr/lib") + cmd.Env = append(cmd.Env, ld_env) + log.Println(ld_env, cmd) + + if *_debug { + cmd.Env = append(cmd.Env, "QT_DEBUG_PLUGINS=1") + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + cmd.Run() + } else { + cmd.Start() + } +} + +func tryLinkQtPlugin(sub string, remove bool) { + wd_plugins_sub := filepath.Join("./usr/plugins", sub) + + if Exist(wd_plugins_sub) { + if remove { + os.RemoveAll(wd_plugins_sub) + } + } else { + if remove { + return + } + + arch := "x86_64" + if runtime.GOARCH == "arm64" { + arch = "aarch64" + } + + paths := []string{ + filepath.Join("/usr/lib/qt5/plugins", sub), + filepath.Join("/usr/lib64/qt5/plugins", sub), + filepath.Join("/usr/lib/"+arch+"-linux-gnu/qt5/plugins", sub), + filepath.Join("/usr/lib/qt/plugins", sub), + } + path := FindExist(paths) + if path == "" { + log.Println("warning:", sub, "not found") + return + } + + err := os.Symlink(path, wd_plugins_sub) + if err != nil { + log.Println("symlink failed:", err.Error()) + } + } +} diff --git a/updater/launcher_windows.go b/updater/launcher_windows.go new file mode 100644 index 0000000..ec63e6b --- /dev/null +++ b/updater/launcher_windows.go @@ -0,0 +1,10 @@ +package main + +import ( + "log" + "runtime" +) + +func Launcher() { + log.Fatalf("launcher is not for your platform", runtime.GOOS) +} diff --git a/updater/main.go b/updater/main.go new file mode 100644 index 0000000..c104cb9 --- /dev/null +++ b/updater/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" +) + +func main() { + // update & launcher + exe, err := os.Executable() + if err != nil { + panic(err.Error()) + } + + wd := filepath.Dir(exe) + os.Chdir(wd) + exe = filepath.Base(os.Args[0]) + log.Println("exe:", exe, "exe dir:", wd) + + if strings.HasPrefix(strings.ToLower(exe), "updater") { + if runtime.GOOS == "windows" { + if strings.HasPrefix(strings.ToLower(exe), "updater.old") { + // 2. "updater.old" update files + time.Sleep(time.Second) + Updater() + // 3. start + exec.Command("./nekoray.exe").Start() + } else { + // 1. nekoray stop it self and run "updater.exe" + Copy("./updater.exe", "./updater.old") + exec.Command("./updater.old").Start() + } + } else { + // 1. update files + Updater() + // 2. start + Launcher() + } + return + } else if strings.HasPrefix(strings.ToLower(exe), "launcher") { + Launcher() + return + } + log.Fatalf("wrong name") +} + +func Copy(src string, dst string) { + // Read all content of src to data + data, _ := ioutil.ReadFile(src) + // Write data to dst + ioutil.WriteFile(dst, data, 0644) +} diff --git a/updater/updater.go b/updater/updater.go new file mode 100644 index 0000000..f65615a --- /dev/null +++ b/updater/updater.go @@ -0,0 +1,149 @@ +package main + +import ( + "context" + "log" + "os" + "path/filepath" + "runtime" + + "github.com/codeclysm/extract" +) + +func Updater() { + pre_cleanup := func() { + if runtime.GOOS == "linux" { + os.RemoveAll("./usr") + } + os.RemoveAll("./nekoray_update") + } + + // update extract + if Exist("./nekoray.zip") { + pre_cleanup() + log.Println("updating from zip") + + f, err := os.Open("./nekoray.zip") + if err != nil { + log.Fatalln(err.Error()) + } + err = extract.Zip(context.Background(), f, "./nekoray_update", nil) + if err != nil { + log.Fatalln(err.Error()) + } + f.Close() + } else if Exist("./nekoray.tar.gz") { + pre_cleanup() + log.Println("updating from tar.gz") + + f, err := os.Open("./nekoray.tar.gz") + if err != nil { + log.Fatalln(err.Error()) + } + err = extract.Gz(context.Background(), f, "./nekoray_update", nil) + if err != nil { + log.Fatalln(err.Error()) + } + f.Close() + } else { + log.Fatalln("no update") + } + + // remove old file + removeAll("./*.dll") + removeAll("./*.dmp") + + // nekoray_list_old := make([]string, 0) + // nekoray_list_new := make([]string, 0) + + // // delete old file from list + // if f, _ := os.Open("./files.json"); f != nil { + // err := json.NewDecoder(f).Decode(&nekoray_list_old) + // if err == nil { + // for _, fn := range nekoray_list_old { + // log.Println("del", fn, os.RemoveAll(fn)) + // } + // } + // f.Close() + // } + + // // walk the new file list + // if os.Chdir("./nekoray_update/nekoray/") == nil { + // filepath.Walk(".", + // func(path string, info os.FileInfo, err error) error { + // if path != "." { + // nekoray_list_new = append([]string{path}, nekoray_list_new...) + // } + // return nil + // }) + // os.Chdir("../../") + + // // store new file list + // if f, _ := os.OpenFile("./files.json", os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644); f != nil { + // json.NewEncoder(f).Encode(&nekoray_list_new) + // f.Close() + // } + // } + + // update move + err := Mv("./nekoray_update/nekoray", "./") + if err != nil { + log.Fatalln(err.Error()) + } + + os.RemoveAll("./nekoray_update") + os.RemoveAll("./nekoray.zip") + os.RemoveAll("./nekoray.tar.gz") +} + +func Exist(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func FindExist(paths []string) string { + for _, path := range paths { + if Exist(path) { + return path + } + } + return "" +} + +func Mv(src, dst string) error { + s, err := os.Stat(src) + if err != nil { + return err + } + if s.IsDir() { + es, err := os.ReadDir(src) + if err != nil { + return err + } + for _, e := range es { + err = Mv(filepath.Join(src, e.Name()), filepath.Join(dst, e.Name())) + if err != nil { + return err + } + } + } else { + err = os.MkdirAll(filepath.Dir(dst), 0755) + if err != nil { + return err + } + err = os.Rename(src, dst) + if err != nil { + return err + } + } + return nil +} + +func removeAll(glob string) { + files, _ := filepath.Glob(glob) + if files != nil { + for _, f := range files { + os.Remove(f) + } + } +}