├── .clang-format ├── .github ├── ISSUE_TEMPLATE │ ├── Bug-Report.md │ └── Feature-Request.md └── workflows │ ├── format.yml │ ├── linux-release.yml │ ├── macos-release.yml │ └── windows-release.yml ├── .gitignore ├── .gitmodules ├── .lgtm.yml ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml └── src ├── V2Ray-Desktop.pro ├── appproxy.cpp ├── appproxy.h ├── appproxyworker.cpp ├── appproxyworker.h ├── configurator.cpp ├── configurator.h ├── constants.h ├── images ├── icon-about.svg ├── icon-dashboard.svg ├── icon-logs.svg ├── icon-rules.svg ├── icon-servers.svg ├── icon-settings.svg ├── logo.png ├── v2ray.gray.png ├── v2ray.icns ├── v2ray.ico └── v2ray.png ├── locales └── zh-CN.ts ├── main.cpp ├── misc ├── tpl-linux-autostart.desktop └── tpl-macos-autostart.plist ├── networkproxy.cpp ├── networkproxy.h ├── networkrequest.cpp ├── networkrequest.h ├── qml.qrc ├── qrcodehelper.cpp ├── qrcodehelper.h ├── runguard.cpp ├── runguard.h ├── serverconfighelper.cpp ├── serverconfighelper.h ├── ui ├── about.qml ├── dashboard.qml ├── logs.qml ├── main.qml ├── rules.qml ├── servers.qml └── settings.qml ├── utility.cpp ├── utility.h ├── v2raycore.cpp └── v2raycore.h /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: Google 4 | AccessModifierOffset: -1 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: true 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlinesLeft: true 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: true 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: true 14 | AllowShortFunctionsOnASingleLine: All 15 | AllowShortIfStatementsOnASingleLine: true 16 | AllowShortLoopsOnASingleLine: true 17 | AlwaysBreakAfterDefinitionReturnType: None 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: true 20 | AlwaysBreakTemplateDeclarations: true 21 | BinPackArguments: true 22 | BinPackParameters: false 23 | BraceWrapping: 24 | AfterClass: false 25 | AfterControlStatement: false 26 | AfterEnum: false 27 | AfterFunction: false 28 | AfterNamespace: false 29 | AfterObjCDeclaration: false 30 | AfterStruct: false 31 | AfterUnion: false 32 | BeforeCatch: false 33 | BeforeElse: false 34 | IndentBraces: false 35 | BreakBeforeBinaryOperators: None 36 | BreakBeforeBraces: Attach 37 | BreakBeforeTernaryOperators: true 38 | BreakConstructorInitializersBeforeComma: false 39 | BreakAfterJavaFieldAnnotations: false 40 | BreakStringLiterals: true 41 | ColumnLimit: 80 42 | CommentPragmas: '^ IWYU pragma:' 43 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 44 | ConstructorInitializerIndentWidth: 2 45 | ContinuationIndentWidth: 2 46 | Cpp11BracedListStyle: true 47 | DerivePointerAlignment: true 48 | DisableFormat: false 49 | ExperimentalAutoDetectBinPacking: false 50 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] 51 | IncludeCategories: 52 | - Regex: '^<.*\.h>' 53 | Priority: 1 54 | - Regex: '^<.*' 55 | Priority: 2 56 | - Regex: '.*' 57 | Priority: 3 58 | IncludeIsMainRegex: '([-_](test|unittest))?$' 59 | IndentCaseLabels: true 60 | IndentWidth: 2 61 | IndentWrappedFunctionNames: false 62 | JavaScriptQuotes: Leave 63 | JavaScriptWrapImports: true 64 | KeepEmptyLinesAtTheStartOfBlocks: false 65 | MacroBlockBegin: '' 66 | MacroBlockEnd: '' 67 | MaxEmptyLinesToKeep: 1 68 | NamespaceIndentation: None 69 | ObjCBlockIndentWidth: 2 70 | ObjCSpaceAfterProperty: false 71 | ObjCSpaceBeforeProtocolList: false 72 | PenaltyBreakBeforeFirstCallParameter: 1 73 | PenaltyBreakComment: 300 74 | PenaltyBreakFirstLessLess: 120 75 | PenaltyBreakString: 1000 76 | PenaltyExcessCharacter: 1000000 77 | PenaltyReturnTypeOnItsOwnLine: 200 78 | PointerAlignment: Right 79 | ReflowComments: true 80 | SortIncludes: true 81 | SpaceAfterCStyleCast: false 82 | SpaceBeforeAssignmentOperators: true 83 | SpaceBeforeParens: ControlStatements 84 | SpaceInEmptyParentheses: false 85 | SpacesBeforeTrailingComments: 2 86 | SpacesInAngles: false 87 | SpacesInContainerLiterals: true 88 | SpacesInCStyleCastParentheses: false 89 | SpacesInParentheses: false 90 | SpacesInSquareBrackets: false 91 | Standard: Auto 92 | TabWidth: 2 93 | UseTab: Never 94 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug-Report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] ..." 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 感谢你向 V2Ray Desktop 提交 issue! 12 | 在提交之前,请确认: 13 | 14 | - [ ] 我已经在 [Issue Tracker](https://github.com/Dr-Incognito/V2Ray-Desktop/issues) 中找过我要提出的问题 15 | - [ ] 如果你可以自己 Debug 并解决的话,提交 PR 吧! 16 | 17 | 请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。 18 | 19 | 28 | 29 | 我都确认过了,我要继续提交。 30 | 31 | ------------------------------------------------------------------ 32 | 33 | 请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *details needed* 标记并在收到更多资讯之前关闭 issue。 34 | 35 | 36 | ### V2Ray Desktop Config 37 | 38 | 45 | ``` 46 | ... 47 | ``` 48 | 49 | ### V2Ray Desktop Log 50 | 54 | ``` 55 | ... 56 | ``` 57 | 58 | ### 环境 Environment 59 | 60 | * 操作系统 (the OS for running the client) 61 | 62 | ... 63 | 64 | ### 说明 Description 65 | 66 | 69 | 70 | ### 重现问题的具体布骤 Steps to Reproduce 71 | 72 | 1. [First Step] 73 | 2. [Second Step] 74 | 3. ... 75 | 76 | **我预期会发生……?** 77 | 78 | 79 | **实际上发生了什麽?** 80 | 81 | 82 | ### 更多信息 More Information 83 | 84 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature-Request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request(新功能建议) 3 | about: Suggest a new idea for V2Ray Desktop 4 | title: "[New Feature] ..." 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 感谢你向 V2Ray Desktop 提交 Feature Request! 12 | 在提交之前,请确认: 13 | 14 | - [ ] 我已经在 [Issue Tracker](https://github.com/Dr-Incognito/V2Ray-Desktop/issues) 中找过我要提出的请求 15 | 16 | 请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。 17 | 18 | 26 | 27 | 我都确认过了,我要继续提交。 28 | 29 | ------------------------------------------------------------------ 30 | 31 | 请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *details needed* 标记并在收到更多资讯之前关闭 issue。 32 | 33 | 34 | ### 说明 Description 35 | 36 | 39 | 40 | ### 可能的解决方案 Possible Solution 41 | 42 | 43 | 44 | 45 | ### 更多信息 More Information 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format Source Code with clang-format 2 | on: [push] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: DoozyX/clang-format-lint-action@v0.13 10 | with: 11 | source: 'src' 12 | exclude: 'src/3rdparty' 13 | extensions: 'h,cpp' 14 | clangFormatVersion: 12 15 | inplace: True 16 | - uses: EndBug/add-and-commit@v4 17 | with: 18 | author_name: github-actions 19 | author_email: noreply@github.com 20 | message: 'Format code with clang-format.' 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/linux-release.yml: -------------------------------------------------------------------------------- 1 | name: Release V2Ray-Desktop for Linux 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-18.04 9 | steps: 10 | # Install dependencies 11 | - name: Install Linux dependencies 12 | run: | 13 | sudo apt update 14 | sudo apt install -y zlib1g-dev libgl1-mesa-dev libxcb-xinerama0 libgtk2.0-dev patchelf 15 | # Install Qt (Qt 6 requires Ubuntu 20.04; Use Qt 5 here for better compatibility) 16 | - name: Install Qt 17 | uses: jurplel/install-qt-action@v2 18 | with: 19 | version: '5.15.2' 20 | target: desktop 21 | # Clone the source code 22 | - name: Get the source code 23 | uses: actions/checkout@v2 24 | with: 25 | fetch-depth: 1 26 | submodules: true 27 | # Install linuxdeployqt 28 | - name: Install linuxdeployqt 29 | run: | 30 | wget https://github.com/probonopd/linuxdeployqt/releases/download/8/linuxdeployqt-continuous-x86_64.AppImage -O /usr/local/bin/linuxdeployqt 31 | chmod a+x /usr/local/bin/linuxdeployqt 32 | # Build 33 | - name: Build 34 | run: | 35 | sed -i "s/QtQuick.Dialogs/QtQuick.Dialogs 1.3/" src/ui/servers.qml 36 | sed -i "/topPadding: 7/d" src/ui/servers.qml 37 | sed -i "/bottomPadding: 7/d" src/ui/servers.qml 38 | mkdir build 39 | cd build 40 | qmake ../src/V2Ray-Desktop.pro 41 | make -j$(nproc) 42 | # Integrate with clash 1.9.0 43 | - name: Get clash-1.9.0 44 | run: | 45 | wget https://github.com/Dreamacro/clash/releases/download/v1.9.0/clash-linux-amd64-v1.9.0.gz 46 | wget https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb 47 | mkdir -p dist/clash-core 48 | gzip -d clash-linux-amd64-v1.9.0.gz 49 | mv clash-linux-amd64-v1.9.0 dist/clash-core/clash 50 | chmod a+x dist/clash-core/clash 51 | mv Country.mmdb dist/clash-core/Country.mmdb 52 | # Localization 53 | - name: Localization 54 | run: | 55 | lrelease src/V2Ray-Desktop.pro 56 | mkdir -p dist/locales 57 | cp src/locales/*.qm dist/locales 58 | # Generate AppImage 59 | - name: Package 60 | run: | 61 | cp build/V2Ray-Desktop dist 62 | cp src/images/v2ray.png dist/v2ray-desktop.png 63 | cp src/misc/tpl-linux-autostart.desktop dist/default.desktop 64 | sed -i "s/%1/AppRun %F/" dist/default.desktop 65 | cd dist 66 | linuxdeployqt V2Ray-Desktop -appimage -qmldir=../src/ui 67 | mv *.AppImage ../V2Ray-Desktop-linux-x86_64.AppImage 68 | # Upload binaries to release 69 | - name: Get release 70 | id: get_release 71 | uses: bruceadams/get-release@v1.2.3 72 | env: 73 | GITHUB_TOKEN: ${{ github.token }} 74 | - name: Upload release binary 75 | uses: actions/upload-release-asset@v1.0.2 76 | env: 77 | GITHUB_TOKEN: ${{ github.token }} 78 | with: 79 | upload_url: ${{ steps.get_release.outputs.upload_url }} 80 | asset_path: V2Ray-Desktop-linux-x86_64.AppImage 81 | asset_name: V2Ray-Desktop-v${{ steps.get_release.outputs.tag_name }}-linux-x86_64.AppImage 82 | asset_content_type: application/octet-stream 83 | -------------------------------------------------------------------------------- /.github/workflows/macos-release.yml: -------------------------------------------------------------------------------- 1 | name: Release V2Ray-Desktop for macOS 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | build: 8 | runs-on: macos-latest 9 | steps: 10 | # Install dependencies 11 | - name: Install macOS dependencies 12 | run: | 13 | brew install wget 14 | # Install Qt 15 | - name: Install Qt 16 | uses: jurplel/install-qt-action@v2 17 | with: 18 | version: '6.2.3' 19 | target: desktop 20 | # Clone the source code 21 | - name: Get the source code 22 | uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 1 25 | submodules: true 26 | # Build 27 | - name: Build 28 | run: | 29 | mkdir build 30 | cd build 31 | qmake ../src/V2Ray-Desktop.pro 32 | make -j16 33 | # Integrate with clash 1.9.0 34 | - name: Get clash-1.9.0 35 | run: | 36 | wget https://github.com/Dreamacro/clash/releases/download/v1.9.0/clash-darwin-amd64-v1.9.0.gz 37 | wget https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb 38 | mkdir build/V2Ray-Desktop.app/Contents/MacOS/clash-core 39 | gzip -d clash-darwin-amd64-v1.9.0.gz 40 | mv clash-darwin-amd64-v1.9.0 build/V2Ray-Desktop.app/Contents/MacOS/clash-core/clash 41 | chmod a+x build/V2Ray-Desktop.app/Contents/MacOS/clash-core/clash 42 | mv Country.mmdb build/V2Ray-Desktop.app/Contents/MacOS/clash-core/Country.mmdb 43 | # Localization 44 | - name: Localization 45 | run: | 46 | lrelease src/V2Ray-Desktop.pro 47 | mkdir build/V2Ray-Desktop.app/Contents/MacOS/locales 48 | cp src/locales/*.qm build/V2Ray-Desktop.app/Contents/MacOS/locales 49 | # Generate dmg 50 | - name: Package 51 | run: | 52 | macdeployqt build/V2Ray-Desktop.app -dmg -qmldir=src/ui 53 | mv build/V2Ray-Desktop.dmg . 54 | # Upload binaries to release 55 | - name: Get release 56 | id: get_release 57 | uses: bruceadams/get-release@v1.2.3 58 | env: 59 | GITHUB_TOKEN: ${{ github.token }} 60 | - name: Upload release binary 61 | uses: actions/upload-release-asset@v1.0.2 62 | env: 63 | GITHUB_TOKEN: ${{ github.token }} 64 | with: 65 | upload_url: ${{ steps.get_release.outputs.upload_url }} 66 | asset_path: V2Ray-Desktop.dmg 67 | asset_name: V2Ray-Desktop-v${{ steps.get_release.outputs.tag_name }}-macOS-x86_64.dmg 68 | asset_content_type: application/octet-stream 69 | -------------------------------------------------------------------------------- /.github/workflows/windows-release.yml: -------------------------------------------------------------------------------- 1 | name: Release V2Ray-Desktop for Windows 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | build: 8 | runs-on: windows-latest 9 | steps: 10 | # Install dependencies (MinGW) 11 | - name: Install MinGW compiler 12 | uses: egor-tensin/setup-mingw@v2 13 | with: 14 | platform: x64 15 | # Install Qt 16 | - name: Install Qt 17 | uses: jurplel/install-qt-action@v2 18 | with: 19 | version: '6.2.1' 20 | arch: 'win64_mingw81' 21 | install-deps: true 22 | target: desktop 23 | # Clone the source code 24 | - name: Get the source code 25 | uses: actions/checkout@v2 26 | with: 27 | fetch-depth: 1 28 | submodules: true 29 | # Build 30 | - name: Build Binary 31 | run: | 32 | mkdir build 33 | cd build 34 | qmake ../src/V2Ray-Desktop.pro 35 | make -j16 36 | # Generate dmg 37 | - name: Package 38 | uses: carlosperate/download-file-action@v1 39 | with: 40 | file-url: "https://github.com/Dr-Incognito/V2Ray-Desktop/releases/download/2.2.1/V2Ray-Desktop-v2.2.1-win64.zip" 41 | - run: | 42 | windeployqt build/release/V2Ray-Desktop.exe --dir dist --release --qmldir src/ui 43 | # unzip V2Ray-Desktop-v2.2.1-win64.zip -d dist 44 | copy build/release/V2Ray-Desktop.exe dist 45 | # Localization 46 | - name: Localization 47 | run: | 48 | lrelease src/V2Ray-Desktop.pro 49 | mkdir dist/locales 50 | copy src/locales/*.qm dist/locales 51 | # Integrate with clash 1.9.0 52 | - name: Get clash-1.9.0 53 | uses: carlosperate/download-file-action@v1 54 | with: 55 | file-url: "https://github.com/Dreamacro/clash/releases/download/v1.9.0/clash-windows-amd64-v1.9.0.zip" 56 | - uses: carlosperate/download-file-action@v1 57 | with: 58 | file-url: "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb" 59 | - run: | 60 | mkdir dist/clash-core 61 | # del dist/clash-core/clash.exe 62 | # del dist/clash-core/Country.mmdb 63 | unzip clash-windows-amd64-v1.9.0.zip 64 | move clash-windows-amd64.exe dist/clash-core/clash.exe 65 | move Country.mmdb dist/clash-core/Country.mmdb 66 | # Copy OpenSSL Dynamic Link Libraries 67 | - name: Fix OpenSSL 68 | uses: carlosperate/download-file-action@v1 69 | with: 70 | file-url: "https://github.com/Dr-Incognito/V2Ray-Desktop/files/7991307/libssl.dll%2Blibcrypto.dll.zip" 71 | file-name: "libssl.dll+libcrypto.dll.zip" 72 | - run: | 73 | unzip libssl.dll+libcrypto.dll.zip 74 | move libcrypto-1_1-x64.dll dist 75 | move libssl-1_1-x64.dll dist 76 | # Upload binaries to release 77 | - name: Create zip release 78 | uses: vimtor/action-zip@v1 79 | with: 80 | files: dist/ 81 | dest: V2Ray-Desktop.zip 82 | - name: Get release 83 | id: get_release 84 | uses: bruceadams/get-release@v1.2.3 85 | env: 86 | GITHUB_TOKEN: ${{ github.token }} 87 | - name: Upload release binary 88 | uses: actions/upload-release-asset@v1.0.2 89 | env: 90 | GITHUB_TOKEN: ${{ github.token }} 91 | with: 92 | upload_url: ${{ steps.get_release.outputs.upload_url }} 93 | asset_path: V2Ray-Desktop.zip 94 | asset_name: V2Ray-Desktop-v${{ steps.get_release.outputs.tag_name }}-win64.zip 95 | asset_content_type: application/octet-stream 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | 4 | *~ 5 | *.autosave 6 | *.a 7 | *.core 8 | *.moc 9 | *.o 10 | *.obj 11 | *.orig 12 | *.rej 13 | *.so 14 | *.so.* 15 | *_pch.h.cpp 16 | *_resource.rc 17 | *.qm 18 | .#* 19 | *.*# 20 | core 21 | !core/ 22 | tags 23 | .DS_Store 24 | .directory 25 | *.debug 26 | Makefile* 27 | *.prl 28 | *.app 29 | moc_*.cpp 30 | ui_*.h 31 | qrc_*.cpp 32 | Thumbs.db 33 | *.res 34 | *.rc 35 | /.qmake.cache 36 | /.qmake.stash 37 | 38 | # qtcreator generated files 39 | *.pro.user* 40 | 41 | # xemacs temporary files 42 | *.flc 43 | 44 | # Vim temporary files 45 | .*.swp 46 | 47 | # Visual Studio generated files 48 | *.ib_pdb_index 49 | *.idb 50 | *.ilk 51 | *.pdb 52 | *.sln 53 | *.suo 54 | *.vcproj 55 | *vcproj.*.*.user 56 | *.ncb 57 | *.sdf 58 | *.opensdf 59 | *.vcxproj 60 | *vcxproj.* 61 | 62 | # MinGW generated files 63 | *.Debug 64 | *.Release 65 | 66 | # Python byte code 67 | *.pyc 68 | 69 | # Build Directory 70 | build/ 71 | dist/ 72 | 73 | # Binaries 74 | # -------- 75 | *.dll 76 | *.exe 77 | 78 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/3rdparty/qzxing"] 2 | path = src/3rdparty/qzxing 3 | url = https://github.com/ftylitak/qzxing.git 4 | -------------------------------------------------------------------------------- /.lgtm.yml: -------------------------------------------------------------------------------- 1 | extraction: 2 | cpp: 3 | prepare: 4 | packages: 5 | - "build-essential" 6 | - "libqt5svg5-dev" 7 | - "libssl-dev" 8 | - "pkg-config" 9 | - "qt5-default" 10 | - "qtbase5-dev" 11 | - "qtdeclarative5-dev" 12 | index: 13 | build_command: 14 | - "qmake src/V2Ray-Desktop.pro" 15 | - "make -j4" 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | dist: focal 3 | osx_image: xcode12.2 4 | 5 | os: 6 | - linux 7 | - osx 8 | 9 | branches: 10 | only: 11 | - 'master' 12 | 13 | before_install: 14 | - if [ "$TRAVIS_OS_NAME" == "linux" ]; then 15 | sudo apt-get install -y build-essential libz-dev qt5-default libqt5svg5-dev qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev; 16 | sudo apt-get install clang-format; 17 | sudo rm /usr/local/clang-7.0.0/bin/clang-format; 18 | fi 19 | 20 | # Replace Qt::endl to endl because it is not supported in Qt 5.12 (Ubuntu 20.04) 21 | # QByteArray::AbortOnBase64DecodingErrors is not supported until Qt 5.15 22 | - if [ "$TRAVIS_OS_NAME" == "linux" ]; then 23 | sed -i "s/Qt::endl/endl/g" src/main.cpp; 24 | sed -i "s/QByteArray::AbortOnBase64DecodingErrors/QByteArray::Base64Encoding/g" src/serverconfighelper.cpp; 25 | fi 26 | 27 | - if [ "$TRAVIS_OS_NAME" == "osx" ]; then 28 | brew install qt5 clang-format; 29 | brew link qt5 --force; 30 | fi 31 | 32 | - git submodule update --init --recursive 33 | 34 | script: 35 | - clang-format --version 36 | - qmake src/V2Ray-Desktop.pro 37 | - make -j4 38 | 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # V2Ray Desktop 2 | 3 | [![Build Status](https://travis-ci.com/Dr-Incognito/V2Ray-Desktop.svg?branch=master)](https://travis-ci.com/Dr-Incognito/V2Ray-Desktop) 4 | [![Build status](https://ci.appveyor.com/api/projects/status/0t07jpv22tf7xpn9?svg=true)](https://ci.appveyor.com/project/Dr-Incognito/V2Ray-Desktop) 5 | [![LGTM grade: C/C++](https://img.shields.io/lgtm/grade/cpp/g/Dr-Incognito/V2Ray-Desktop.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Dr-Incognito/V2Ray-Desktop/context:cpp) 6 | [![LGTM alerts](https://img.shields.io/lgtm/alerts/g/Dr-Incognito/V2Ray-Desktop.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Dr-Incognito/V2Ray-Desktop/alerts/) 7 | 8 | ## What's New in 2.0? 9 | 10 | We use [Clash](https://github.com/Dreamacro/clash) as the backend proxy, which supports **Shadowsocks(R)**, **V2Ray**, and **Trojan** protocols. 11 | 12 | ## Introduction 13 | 14 | V2Ray Desktop is a cross-platform GUI client that supports **Shadowsocks(R)**, **V2Ray**, and **Trojan** protocols, running on Windows, Linux, and macOS. 15 | It is built with Qt 5 and QML 2. 16 | 17 | Compared to [V2Ray](http://v2ray.com/), V2Ray Desktop provides more advanced features such as server subscription and latency test. You can easily migrate to V2Ray Desktop from [Shadowsocks-Qt5](https://github.com/shadowsocks/shadowsocks-qt5/) and [V2Ray Core](http://v2ray.com/) by importing their config files. 18 | 19 | You can get the latest release at [Releases Page](https://github.com/Dr-Incognito/V2Ray-Desktop/releases). 20 | If you are using Arch Linux, you can install from [archlinuxcn](https://github.com/archlinuxcn/mirrorlist-repo) or build from [AUR](https://aur.archlinux.org/packages/v2ray-desktop/). 21 | 22 | **Notes:** 23 | - Some functions (*e.g.,* server subscription) in the prebuilt binary packages require **OpenSSL >= 1.1.0**. If you are using Ubuntu<=18.04, please install OpenSSL manually. You can refer to [Installation Guide](https://github.com/Dr-Incognito/V2Ray-Desktop/wiki/Installation) ([安装指南](https://github.com/Dr-Incognito/V2Ray-Desktop/wiki/安装指南)) in the Wiki page for the detailed information. 24 | - The AppImage for Linux is built in Ubuntu 16.04. Linux with GLIBC< 2.23 (*e.g.,* Ubuntu<=16.04) may have problems using this AppImage. Please consider building it from source with Qt >= 5.15. 25 | 26 | For more information, please visit the [project's Wiki page](https://github.com/Dr-Incognito/V2Ray-Desktop/wiki). 27 | 28 | ## Features 29 | 30 | - Support Windows, Linux, and macOS. 31 | - Support **Shadowsocks(R)**, **V2Ray**, and **Trojan** servers. 32 | - Support connecting to multiple servers. 33 | - Support adding/updating servers from subscription URLs. 34 | - Support adding servers by importing [Shadowsocks-Qt5](https://github.com/shadowsocks/shadowsocks-qt5/) and [V2Ray Core](http://v2ray.com/) configuration. 35 | - Support adding servers by scanning QR codes. 36 | - Support PAC proxy mode, Global proxy mode, and Manual proxy mode. 37 | - Support getting and setting system proxies for Windows, Linux (GNOME/KDE), and macOS. 38 | - Support automatically updating GFWList. 39 | - Support automatically starts up when logged in. 40 | 41 | ## Screenshot 42 | 43 | Dashboard 44 | 45 | Servers 46 | 47 | ## License 48 | 49 | This project is licensed under version 3 of the GNU General Public License. 50 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{branch}-{build}" 2 | os: 3 | - Visual Studio 2017 4 | 5 | environment: 6 | appveyor_build_worker_cloud: gce 7 | appveyor_rdp_password: jS^xg8tx5hkEB&VX 8 | matrix: 9 | - QT_HOME: C:\Qt\5.13.2\mingw73_64 10 | - QT_HOME: C:\Qt\5.12.6\mingw73_64 11 | 12 | branches: 13 | only: 14 | - 'master' 15 | 16 | init: 17 | - ps: iex ((new-object net.WebClient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 18 | 19 | install: 20 | - set MINGW_HOME=C:\mingw-w64\x86_64-7.2.0-posix-seh-rt_v5-rev1\mingw64 21 | - set PATH=%QT_HOME%\bin;%MINGW_HOME%\bin;%PATH% 22 | - git submodule update --init --recursive 23 | 24 | build_script: 25 | # Qt::endl and QByteArray::AbortOnBase64DecodingErrors are not supported until Qt 5.14 and 5.15, respectively. 26 | - sed -i "s/Qt::endl/endl/g" src/main.cpp 27 | - sed -i "s/QByteArray::AbortOnBase64DecodingErrors/QByteArray::Base64Encoding/g" src/serverconfighelper.cpp 28 | - qmake src\V2Ray-Desktop.pro 29 | - mingw32-make -j4 30 | 31 | on_finish: 32 | - ps: $blockRdp = $false; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 33 | 34 | matrix: 35 | fast_finish: false 36 | -------------------------------------------------------------------------------- /src/V2Ray-Desktop.pro: -------------------------------------------------------------------------------- 1 | QT += quick quickcontrols2 widgets svg 2 | 3 | CONFIG += c++11 4 | 5 | # The following define makes your compiler emit warnings if you use 6 | # any Qt feature that has been marked deprecated (the exact warnings 7 | # depend on your compiler). Refer to the documentation for the 8 | # deprecated API to know how to port your code away from it. 9 | DEFINES += QT_DEPRECATED_WARNINGS QUAZIP_STATIC 10 | 11 | # You can also make your code fail to compile if it uses deprecated APIs. 12 | # In order to do so, uncomment the following line. 13 | # You can also select to disable deprecated APIs only up to a certain version of Qt. 14 | DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 15 | 16 | HEADERS += \ 17 | appproxy.h \ 18 | appproxyworker.h \ 19 | configurator.h \ 20 | constants.h \ 21 | networkproxy.h \ 22 | networkrequest.h \ 23 | qrcodehelper.h \ 24 | runguard.h \ 25 | serverconfighelper.h \ 26 | utility.h \ 27 | v2raycore.h 28 | 29 | SOURCES += \ 30 | appproxy.cpp \ 31 | appproxyworker.cpp \ 32 | configurator.cpp \ 33 | main.cpp \ 34 | networkproxy.cpp \ 35 | networkrequest.cpp \ 36 | qrcodehelper.cpp \ 37 | runguard.cpp \ 38 | serverconfighelper.cpp \ 39 | utility.cpp \ 40 | v2raycore.cpp 41 | 42 | include(3rdparty/qzxing/src/QZXing.pri) 43 | LIBS += -lz 44 | 45 | RESOURCES += qml.qrc 46 | 47 | TRANSLATIONS += locales/zh-CN.ts 48 | 49 | # Additional import path used to resolve QML modules in Qt Creator's code model 50 | QML_IMPORT_PATH = 51 | 52 | # Additional import path used to resolve QML modules just for Qt Quick Designer 53 | QML_DESIGNER_IMPORT_PATH = 54 | 55 | # Default rules for deployment. 56 | qnx: target.path = /tmp/$${TARGET}/bin 57 | else: unix:!android: target.path = /opt/$${TARGET}/bin 58 | !isEmpty(target.path): INSTALLS += target 59 | 60 | win32 { 61 | RC_ICONS = images/v2ray.ico 62 | } 63 | unix { 64 | RC_ICONS = images/v2ray.ico 65 | } 66 | osx { 67 | ICON = images/v2ray.icns 68 | } 69 | -------------------------------------------------------------------------------- /src/appproxy.cpp: -------------------------------------------------------------------------------- 1 | #include "appproxy.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include "constants.h" 21 | #include "networkproxy.h" 22 | #include "networkrequest.h" 23 | #include "qrcodehelper.h" 24 | #include "serverconfighelper.h" 25 | #include "utility.h" 26 | 27 | AppProxy::AppProxy(QObject* parent) 28 | : QObject(parent), 29 | v2ray(V2RayCore::getInstance()), 30 | configurator(Configurator::getInstance()) { 31 | // Setup Worker 32 | worker->moveToThread(&workerThread); 33 | connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater); 34 | 35 | // Setup Worker -> getServerLatency 36 | connect(this, &AppProxy::getServerLatencyStarted, worker, 37 | &AppProxyWorker::getServerLatency); 38 | connect(worker, &AppProxyWorker::serverLatencyReady, this, 39 | &AppProxy::returnServerLatency); 40 | 41 | // Setup Worker -> getGfwList 42 | connect(this, &AppProxy::getGfwListStarted, worker, 43 | &AppProxyWorker::getGfwList); 44 | connect(worker, &AppProxyWorker::gfwListReady, this, 45 | &AppProxy::returnGfwList); 46 | 47 | // Setup Worker -> getNetworkStatus 48 | connect(this, &AppProxy::getNetworkStatusStarted, worker, 49 | &AppProxyWorker::getUrlAccessibility); 50 | connect(worker, &AppProxyWorker::urlAccessibilityReady, this, 51 | &AppProxy::returnNetworkAccessiblity); 52 | 53 | // Setup Worker -> getSubscriptionServers 54 | connect(this, &AppProxy::getSubscriptionServersStarted, worker, 55 | &AppProxyWorker::getSubscriptionServers); 56 | connect(worker, &AppProxyWorker::subscriptionServersReady, this, 57 | &AppProxy::addSubscriptionServers); 58 | 59 | // Setup Worker -> getLogs 60 | connect(this, &AppProxy::getLogsStarted, worker, &AppProxyWorker::getLogs); 61 | connect(worker, &AppProxyWorker::logsReady, this, &AppProxy::returnLogs); 62 | 63 | // Setup Worker -> getLatestRelease 64 | connect(this, &AppProxy::getLatestReleaseStarted, worker, 65 | &AppProxyWorker::getLatestRelease); 66 | connect(worker, &AppProxyWorker::latestReleaseReady, this, 67 | &AppProxy::returnLatestRelease); 68 | 69 | workerThread.start(); 70 | } 71 | 72 | AppProxy::~AppProxy() { 73 | NetworkProxyHelper::resetSystemProxy(); 74 | workerThread.quit(); 75 | workerThread.wait(); 76 | } 77 | 78 | QString AppProxy::getAppVersion() { 79 | QString appVersion = QString("v%1.%2.%3") 80 | .arg(QString::number(APP_VERSION_MAJOR), 81 | QString::number(APP_VERSION_MINOR), 82 | QString::number(APP_VERSION_PATCH)); 83 | emit appVersionReady(appVersion); 84 | return appVersion; 85 | } 86 | 87 | void AppProxy::getV2RayCoreVersion() { 88 | QString v2rayVersion = v2ray.getVersion(); 89 | QRegularExpression regx("^[0-9]"); 90 | if (regx.match(v2rayVersion).hasMatch()) { 91 | v2rayVersion = QString("v%1").arg(v2rayVersion); 92 | } 93 | emit v2RayCoreVersionReady(v2rayVersion); 94 | } 95 | 96 | void AppProxy::getOperatingSystem() { 97 | QString operatingSystem = QSysInfo::prettyProductName(); 98 | emit operatingSystemReady(operatingSystem); 99 | } 100 | 101 | void AppProxy::getV2RayCoreStatus() { 102 | emit v2RayCoreStatusReady(v2ray.isRunning()); 103 | } 104 | 105 | void AppProxy::setV2RayCoreRunning(bool expectedRunning) { 106 | bool isSuccessful = false; 107 | if (expectedRunning) { 108 | isSuccessful = v2ray.start(); 109 | qInfo() 110 | << QString("Start Clash ... %1").arg(isSuccessful ? "success" : "failed"); 111 | } else { 112 | isSuccessful = v2ray.stop(); 113 | qInfo() 114 | << QString("Stop Clash ... %1").arg(isSuccessful ? "success" : "failed"); 115 | } 116 | if (isSuccessful) { 117 | emit v2RayCoreStatusReady(expectedRunning); 118 | } else { 119 | emit v2RayCoreStatusReady(!expectedRunning); 120 | } 121 | } 122 | 123 | void AppProxy::getNetworkStatus() { 124 | qRegisterMetaType>("QMap"); 125 | qRegisterMetaType("QNetworkProxy"); 126 | emit getNetworkStatusStarted({{"google.com", true}, {"baidu.com", false}}, 127 | getQProxy()); 128 | } 129 | 130 | QNetworkProxy AppProxy::getQProxy() { 131 | QStringList connectedServerNames = configurator.getConnectedServerNames(); 132 | if (connectedServerNames.size() == 0 || !v2ray.isRunning()) { 133 | return QNetworkProxy::NoProxy; 134 | } 135 | 136 | QJsonObject appConfig = configurator.getAppConfig(); 137 | QNetworkProxy::ProxyType proxyType = QNetworkProxy::Socks5Proxy; 138 | int socksPort = appConfig["socksPort"].toInt(); 139 | QNetworkProxy proxy; 140 | proxy.setType(proxyType); 141 | proxy.setHostName("127.0.0.1"); 142 | proxy.setPort(socksPort); 143 | return proxy; 144 | } 145 | 146 | void AppProxy::returnNetworkAccessiblity(QMap accessible) { 147 | bool isGoogleAccessible = 148 | accessible.contains("google.com") ? accessible["google.com"] : false, 149 | isBaiduAccessible = 150 | accessible.contains("baidu.com") ? accessible["baidu.com"] : false; 151 | 152 | emit networkStatusReady( 153 | QJsonDocument(QJsonObject{ 154 | {"isGoogleAccessible", isGoogleAccessible}, 155 | {"isBaiduAccessible", isBaiduAccessible}, 156 | }) 157 | .toJson()); 158 | } 159 | 160 | void AppProxy::getAppConfig() { 161 | QJsonObject appConfig = configurator.getAppConfig(); 162 | emit appConfigReady(QJsonDocument(appConfig).toJson()); 163 | } 164 | 165 | void AppProxy::setAppConfig(QString configString) { 166 | QJsonDocument configDoc = QJsonDocument::fromJson(configString.toUtf8()); 167 | QJsonObject appConfig = configDoc.object(); 168 | // Check if app config contains errors 169 | QStringList appConfigErrors = getAppConfigErrors(appConfig); 170 | if (appConfigErrors.size() > 0) { 171 | emit appConfigError(appConfigErrors.join('\n')); 172 | return; 173 | } 174 | // Set auto start and update UI language 175 | setAutoStart(appConfig["autoStart"].toBool()); 176 | retranslate(appConfig["language"].toString()); 177 | // Save app config 178 | appConfig["httpPort"] = appConfig["httpPort"].toString().toInt(); 179 | appConfig["socksPort"] = appConfig["socksPort"].toString().toInt(); 180 | configurator.setAppConfig(appConfig); 181 | qInfo() << "Application config updated. Restarting V2Ray ..."; 182 | // Restart V2Ray Core 183 | v2ray.restart(); 184 | // Notify that the app config has changed 185 | emit appConfigChanged(); 186 | } 187 | 188 | QStringList AppProxy::getAppConfigErrors(const QJsonObject& appConfig) { 189 | QStringList errors; 190 | errors.append( 191 | Utility::getStringConfigError(appConfig, "language", tr("Language"))); 192 | errors.append(Utility::getStringConfigError( 193 | appConfig, "serverIp", tr("Listening IP Address"), 194 | { 195 | std::bind(&Utility::isIpAddrValid, std::placeholders::_1), 196 | })); 197 | errors.append(Utility::getStringConfigError( 198 | appConfig, "dns", tr("DNS Server"), 199 | { 200 | std::bind(&Utility::isIpAddrListValid, std::placeholders::_1), 201 | })); 202 | errors.append(Utility::getNumericConfigError(appConfig, "httpPort", 203 | tr("HTTP Port"), 1, 65535)); 204 | errors.append(Utility::getNumericConfigError(appConfig, "socksPort", 205 | tr("SOCKS Port"), 1, 65535)); 206 | if (appConfig["httpPort"].toString() == appConfig["socksPort"].toString()) { 207 | errors.append(tr("'HTTP Port' and 'SOCKS Port' can not be the same.")); 208 | } 209 | errors.append(Utility::getStringConfigError( 210 | appConfig, "gfwListUrl", tr("GFW List URL"), 211 | {std::bind(&Utility::isUrlValid, std::placeholders::_1)})); 212 | 213 | // Remove empty error messages generated by getNumericConfigError() and 214 | // getStringConfigError() 215 | errors.removeAll(""); 216 | return errors; 217 | } 218 | 219 | bool AppProxy::retranslate(QString language) { 220 | if (language.isEmpty()) { 221 | Configurator& configurator(Configurator::getInstance()); 222 | language = configurator.getLanguage(); 223 | } 224 | QCoreApplication* app = QGuiApplication::instance(); 225 | app->removeTranslator(&translator); 226 | bool isTrLoaded = translator.load( 227 | QString("%1/%2.qm").arg(Configurator::getLocaleDirPath(), language)); 228 | 229 | app->installTranslator(&translator); 230 | QQmlEngine::contextForObject(this)->engine()->retranslate(); 231 | return isTrLoaded; 232 | } 233 | 234 | void AppProxy::setAutoStart(bool autoStart) { 235 | const QString APP_NAME = "V2Ray Desktop"; 236 | const QString APP_PATH = 237 | QDir::toNativeSeparators(Configurator::getAppFilePath()); 238 | #if defined(Q_OS_WIN) 239 | QSettings settings( 240 | "HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", 241 | QSettings::NativeFormat); 242 | #elif defined(Q_OS_LINUX) 243 | QFile srcFile(":/misc/tpl-linux-autostart.desktop"); 244 | QFile dstFile(QString("%1/.config/autostart/v2ray-dekstop.desktop") 245 | .arg(QDir::homePath())); 246 | #elif defined(Q_OS_MAC) 247 | QFile srcFile(":/misc/tpl-macos-autostart.plist"); 248 | QFile dstFile( 249 | QString("%1/Library/LaunchAgents/com.v2ray.desktop.launcher.plist") 250 | .arg(QDir::homePath())); 251 | #endif 252 | 253 | #if defined(Q_OS_WIN) 254 | if (autoStart) { 255 | settings.setValue(APP_NAME, APP_PATH); 256 | } else { 257 | settings.remove(APP_NAME); 258 | } 259 | #elif defined(Q_OS_LINUX) or defined(Q_OS_MAC) 260 | if (autoStart) { 261 | QString fileContent; 262 | if (srcFile.exists() && 263 | srcFile.open(QIODevice::ReadOnly | QIODevice::Text)) { 264 | fileContent = srcFile.readAll(); 265 | srcFile.close(); 266 | } 267 | QFileInfo dstFileInfo(dstFile); 268 | if (!dstFileInfo.dir().exists()) { 269 | dstFileInfo.dir().mkpath("."); 270 | } 271 | if (dstFile.open(QIODevice::WriteOnly)) { 272 | dstFile.write(fileContent.arg(APP_PATH).toUtf8()); 273 | dstFile.close(); 274 | } 275 | } else { 276 | if (dstFile.exists()) { 277 | dstFile.remove(); 278 | } 279 | } 280 | #endif 281 | } 282 | 283 | void AppProxy::getLogs() { 284 | emit getLogsStarted(Configurator::getAppLogFilePath(), 285 | Configurator::getV2RayLogFilePath()); 286 | } 287 | 288 | void AppProxy::returnLogs(QString logs) { emit logsReady(logs); } 289 | 290 | void AppProxy::clearLogs() { 291 | QFile appLogFile(Configurator::getAppLogFilePath()); 292 | QFile v2RayLogFile(Configurator::getV2RayLogFilePath()); 293 | if (appLogFile.exists()) { 294 | appLogFile.resize(0); 295 | } 296 | if (v2RayLogFile.exists()) { 297 | v2RayLogFile.resize(0); 298 | } 299 | } 300 | 301 | void AppProxy::getProxySettings() { 302 | bool isV2RayRunning = v2ray.isRunning(); 303 | QJsonObject appConfig = configurator.getAppConfig(); 304 | QString systemProxy = NetworkProxyHelper::getSystemProxy().toString(); 305 | QJsonArray connectedServers; 306 | for (QString cs : configurator.getConnectedServerNames()) { 307 | connectedServers.append(cs); 308 | } 309 | 310 | emit proxySettingsReady( 311 | QJsonDocument(QJsonObject{{"isV2RayRunning", isV2RayRunning}, 312 | {"systemProxy", systemProxy}, 313 | {"proxyMode", appConfig["proxyMode"].toString()}, 314 | {"connectedServers", connectedServers}}) 315 | .toJson()); 316 | } 317 | 318 | void AppProxy::setProxyMode(QString proxyMode) { 319 | QJsonObject appConfig = configurator.getAppConfig(); 320 | QString _proxyMode = appConfig["proxyMode"].toString(); 321 | // Automatically set system proxy according to app config 322 | if (!proxyMode.size()) { 323 | proxyMode = _proxyMode; 324 | } 325 | 326 | // Update app config 327 | configurator.setAppConfig({{"proxyMode", proxyMode}}); 328 | // Detect whether the proxy mode is changed 329 | if (proxyMode != _proxyMode) { 330 | v2ray.restart(); 331 | } 332 | emit proxyModeChanged(proxyMode); 333 | } 334 | 335 | void AppProxy::setSystemProxy(bool enableProxy, QString protocol) { 336 | // Allow values for 'protocol': http; socks 337 | QJsonObject appConfig = configurator.getAppConfig(); 338 | if (!protocol.size()) { 339 | protocol = appConfig["defaultSysProxyProtocol"].toString(); 340 | } 341 | 342 | // Set system proxy 343 | NetworkProxyHelper::resetSystemProxy(); 344 | if (enableProxy && v2ray.isRunning()) { 345 | NetworkProxy proxy(protocol, "127.0.0.1", 346 | appConfig[QString("%1Port").arg(protocol)].toInt(), 347 | NetworkProxyMode::GLOBAL_MODE); 348 | NetworkProxyHelper::setSystemProxy(proxy); 349 | } 350 | configurator.setAppConfig({ 351 | {"enableSysProxy", enableProxy}, 352 | {"defaultSysProxyProtocol", protocol}, 353 | }); 354 | } 355 | 356 | void AppProxy::updateGfwList() { 357 | QJsonObject appConfig = configurator.getAppConfig(); 358 | emit getGfwListStarted(appConfig["gfwListUrl"].toString(), getQProxy()); 359 | } 360 | 361 | void AppProxy::returnGfwList(QString gfwList) { 362 | if (gfwList.size()) { 363 | QFile gfwListFile(Configurator::getGfwListFilePath()); 364 | gfwListFile.open(QFile::WriteOnly); 365 | gfwListFile.write(gfwList.toUtf8()); 366 | gfwListFile.flush(); 367 | // Update app config 368 | QString updatedTime = QDateTime::currentDateTime().toString(); 369 | configurator.setAppConfig(QJsonObject{ 370 | {"gfwListLastUpdated", QDateTime::currentDateTime().toString()}}); 371 | qInfo() << "GFW List updated successfully."; 372 | emit gfwListUpdated(updatedTime); 373 | // Restart V2Ray 374 | v2ray.restart(); 375 | } else { 376 | emit gfwListUpdated(tr("Failed to update GFW List.")); 377 | } 378 | } 379 | 380 | void AppProxy::getServers() { 381 | QJsonArray servers = configurator.getServers(); 382 | QStringList connectedServerNames = configurator.getConnectedServerNames(); 383 | 384 | for (auto itr = servers.begin(); itr != servers.end(); ++itr) { 385 | QJsonObject server = (*itr).toObject(); 386 | QString serverName = 387 | server.contains("name") ? server["name"].toString() : ""; 388 | server["connected"] = connectedServerNames.contains(serverName); 389 | if (serverLatency.contains(serverName)) { 390 | server["latency"] = serverLatency[serverName].toInt(); 391 | } 392 | *itr = server; 393 | } 394 | emit serversReady(QJsonDocument(servers).toJson()); 395 | } 396 | 397 | void AppProxy::getServer(QString serverName, bool forDuplicate) { 398 | QJsonObject server = configurator.getServer(serverName); 399 | if (forDuplicate) { 400 | server.remove("name"); 401 | } 402 | emit serverDInfoReady(QJsonDocument(server).toJson()); 403 | } 404 | 405 | void AppProxy::getServerLatency(QString serverName) { 406 | QJsonObject _serverLatency; 407 | QJsonArray servers; 408 | if (serverName.size()) { 409 | servers.append(configurator.getServer(serverName)); 410 | } else { 411 | servers = configurator.getServers(); 412 | } 413 | qRegisterMetaType("QJsonArray"); 414 | emit getServerLatencyStarted(servers); 415 | } 416 | 417 | void AppProxy::returnServerLatency(QMap latency) { 418 | // Ref: 419 | // https://stackoverflow.com/questions/8517853/iterating-over-a-qmap-with-for 420 | // Note: Convert to StdMap for better performance 421 | for (auto l : latency.toStdMap()) { 422 | serverLatency[l.first] = l.second.toInt(); 423 | } 424 | emit serverLatencyReady(QJsonDocument::fromVariant(latency).toJson()); 425 | } 426 | 427 | void AppProxy::setServerConnection(QString serverName, bool connected) { 428 | configurator.setServerConnection(serverName, connected); 429 | bool isV2RayRunning = v2ray.restart(); 430 | qInfo() << (connected ? "Connected to " : "Disconnected from ") << serverName; 431 | emit v2RayCoreStatusReady(isV2RayRunning); 432 | emit serverConnectivityChanged(serverName, connected); 433 | } 434 | 435 | void AppProxy::addServer(QString protocol, QString configString) { 436 | ServerConfigHelper::Protocol _protocol = 437 | ServerConfigHelper::getProtocol(protocol); 438 | QJsonDocument configDoc = QJsonDocument::fromJson(configString.toUtf8()); 439 | QJsonObject serverConfig = configDoc.object(); 440 | // Check server config before saving 441 | QStringList serverConfigErrors = 442 | ServerConfigHelper::getServerConfigErrors(_protocol, serverConfig); 443 | if (serverConfigErrors.size() > 0) { 444 | emit serverConfigError(serverConfigErrors.join('\n')); 445 | return; 446 | } 447 | // Save server config 448 | configurator.addServer( 449 | ServerConfigHelper::getPrettyServerConfig(_protocol, serverConfig)); 450 | emit serversChanged(); 451 | qInfo() << QString("Add new %1 server [Name=%2, Addr=%3].") 452 | .arg(protocol, serverConfig["serverName"].toString(), 453 | serverConfig["serverAddr"].toString()); 454 | } 455 | 456 | void AppProxy::addServerUrl(QString serverUrl) { 457 | if (serverUrl.startsWith("vmess://") || serverUrl.startsWith("ss://") || 458 | serverUrl.startsWith("ssr://") || serverUrl.startsWith("trojan://")) { 459 | addSubscriptionServers(serverUrl); 460 | } else { 461 | addSubscriptionUrl(serverUrl); 462 | } 463 | } 464 | 465 | void AppProxy::addSubscriptionUrl(QString subsriptionUrl) { 466 | QString error = Utility::getStringConfigError( 467 | {{"subsriptionUrl", subsriptionUrl}}, "subsriptionUrl", 468 | tr("Subscription URL"), 469 | {std::bind(&Utility::isUrlValid, std::placeholders::_1)}); 470 | if (!error.isEmpty()) { 471 | emit serverConfigError(error); 472 | return; 473 | } 474 | updateSubscriptionServers(subsriptionUrl); 475 | } 476 | 477 | void AppProxy::updateSubscriptionServers(QString subsriptionUrl) { 478 | QStringList subscriptionUrls; 479 | if (subsriptionUrl.isEmpty()) { 480 | // Sync servers from subscription 481 | subscriptionUrls = configurator.getSubscriptionUrls(); 482 | } else { 483 | // Add a new server subscription 484 | subscriptionUrls.append(subsriptionUrl); 485 | } 486 | for (QString su : subscriptionUrls) { 487 | emit getSubscriptionServersStarted(su, getQProxy()); 488 | } 489 | } 490 | 491 | void AppProxy::addSubscriptionServers(QString subsriptionServers, 492 | QString subsriptionUrl) { 493 | if (!subsriptionServers.size()) { 494 | emit serverConfigError(tr("Failed to get subscription servers from URLs.")); 495 | return; 496 | } 497 | // Remove servers from the subscription if exists 498 | QMap removedServers; 499 | if (!subsriptionUrl.isEmpty()) { 500 | configurator.removeSubscriptionServers(subsriptionUrl); 501 | } 502 | // Add new servers 503 | int nImportedServers = 0; 504 | QStringList servers = subsriptionServers.split('\n'); 505 | for (QString server : servers) { 506 | ServerConfigHelper::Protocol protocol = 507 | ServerConfigHelper::Protocol::UNKNOWN; 508 | if (server.startsWith("ss://") || server.startsWith("ssr://")) { 509 | protocol = ServerConfigHelper::Protocol::SHADOWSOCKS; 510 | } else if (server.startsWith("vmess://")) { 511 | protocol = ServerConfigHelper::Protocol::VMESS; 512 | } else if (server.startsWith("trojan://")) { 513 | protocol = ServerConfigHelper::Protocol::TROJAN; 514 | } 515 | QJsonObject serverConfig = ServerConfigHelper::getServerConfigFromUrl( 516 | protocol, server, subsriptionUrl); 517 | QStringList serverConfigErrors = 518 | ServerConfigHelper::getServerConfigErrors(protocol, serverConfig); 519 | serverConfig = 520 | ServerConfigHelper::getPrettyServerConfig(protocol, serverConfig); 521 | if (!serverConfigErrors.empty()) { 522 | qWarning() << QString("Error occurred for the server URL: %1. Errors: %2") 523 | .arg(server, serverConfigErrors.join(" ")); 524 | continue; 525 | } 526 | // Recover auto connect option for the server 527 | QString serverName = 528 | serverConfig.contains("name") ? serverConfig["name"].toString() : ""; 529 | serverConfig["autoConnect"] = 530 | removedServers.contains(serverName) 531 | ? removedServers[serverName]["autoConnect"].toBool() 532 | : false; 533 | // Save the server 534 | configurator.addServer(serverConfig); 535 | qInfo() << QString("Add a new server[Name=%1] from URI: %2") 536 | .arg(serverName, server); 537 | ++nImportedServers; 538 | } 539 | if (nImportedServers) { 540 | emit serversChanged(); 541 | } else { 542 | emit serverConfigError(tr("No supported servers added from the URL.")); 543 | } 544 | } 545 | 546 | void AppProxy::addServerConfigFile(QString configFilePath, 547 | QString configFileType) { 548 | QFile configFile(configFilePath); 549 | if (!configFile.exists()) { 550 | emit serverConfigError(tr("The config file does not exist.")); 551 | return; 552 | } 553 | configFile.open(QIODevice::ReadOnly | QIODevice::Text); 554 | QJsonDocument configDoc = QJsonDocument::fromJson(configFile.readAll()); 555 | configFile.close(); 556 | 557 | int nAddServers = 0; 558 | ServerConfigHelper::Protocol protocol = ServerConfigHelper::Protocol::UNKNOWN; 559 | QList servers; 560 | if (configFileType == "v2ray-config") { 561 | protocol = ServerConfigHelper::Protocol::VMESS; 562 | servers = 563 | ServerConfigHelper::getServerConfigFromV2RayConfig(configDoc.object()); 564 | } else if (configFileType == "shadowsocks-qt5-config") { 565 | protocol = ServerConfigHelper::Protocol::SHADOWSOCKS; 566 | servers = ServerConfigHelper::getServerConfigFromShadowsocksQt5Config( 567 | configDoc.object()); 568 | } 569 | for (QJsonObject server : servers) { 570 | QStringList serverConfigErrors = 571 | ServerConfigHelper::getServerConfigErrors(protocol, server); 572 | if (!serverConfigErrors.empty()) { 573 | qWarning() << QString( 574 | "Error occurred for the server[Name=%1]. Errors: %2") 575 | .arg(server["name"].toString(), 576 | serverConfigErrors.join(" ")); 577 | continue; 578 | } else { 579 | configurator.addServer( 580 | ServerConfigHelper::getPrettyServerConfig(protocol, server)); 581 | qInfo() 582 | << QString( 583 | "Add a new server[Name=%1] from Shadowsocks-Qt5 config file.") 584 | .arg(server["name"].toString()); 585 | ++nAddServers; 586 | } 587 | } 588 | if (nAddServers) { 589 | emit serversChanged(); 590 | } else { 591 | emit serverConfigError( 592 | tr("No supported servers added from the config file.")); 593 | } 594 | } 595 | 596 | void AppProxy::editServer(QString serverName, 597 | QString protocol, 598 | QString configString) { 599 | QJsonDocument configDoc = QJsonDocument::fromJson(configString.toUtf8()); 600 | ServerConfigHelper::Protocol _protocol = 601 | ServerConfigHelper::getProtocol(protocol); 602 | QJsonObject serverConfig = configDoc.object(); 603 | QStringList serverConfigErrors = ServerConfigHelper::getServerConfigErrors( 604 | _protocol, serverConfig, &serverName); 605 | if (serverConfigErrors.size() > 0) { 606 | emit serverConfigError(serverConfigErrors.join('\n')); 607 | return; 608 | } 609 | serverConfig = 610 | ServerConfigHelper::getPrettyServerConfig(_protocol, serverConfig); 611 | 612 | if (configurator.editServer(serverName, serverConfig)) { 613 | QString newServerName = serverConfig["name"].toString(); 614 | // Update the information of server connectivity 615 | QStringList connectedServerNames = configurator.getConnectedServerNames(); 616 | serverConfig["connected"] = connectedServerNames.contains(newServerName); 617 | // Update the server latency even if the server name is changed 618 | if (serverLatency.contains(serverName)) { 619 | serverConfig["latency"] = serverLatency[serverName].toInt(); 620 | if (newServerName != serverName) { 621 | serverLatency.insert(newServerName, serverLatency[serverName]); 622 | serverLatency.remove(serverName); 623 | } 624 | } 625 | emit serverChanged(serverName, QJsonDocument(serverConfig).toJson()); 626 | // Restart V2Ray Core 627 | v2ray.restart(); 628 | } 629 | } 630 | 631 | void AppProxy::removeServer(QString serverName) { 632 | configurator.removeServer(serverName); 633 | qInfo() << QString("Server [Name=%1] has been removed.").arg(serverName); 634 | emit serverRemoved(serverName); 635 | // Restart V2Ray Core 636 | v2ray.restart(); 637 | } 638 | 639 | void AppProxy::removeSubscriptionServers(QString subscriptionUrl) { 640 | configurator.removeSubscriptionServers(subscriptionUrl); 641 | qInfo() << QString("Servers from Subscription %1 have been removed.") 642 | .arg(subscriptionUrl); 643 | emit serversChanged(); 644 | } 645 | 646 | void AppProxy::scanQrCodeScreen() { 647 | QStringList servers; 648 | QList screens = QGuiApplication::screens(); 649 | 650 | for (int i = 0; i < screens.size(); ++i) { 651 | QRect r = screens.at(i)->geometry(); 652 | QPixmap screenshot = 653 | screens.at(i)->grabWindow(0, r.x(), r.y(), r.width(), r.height()); 654 | QString serverUrl = QrCodeHelper::decode( 655 | screenshot.toImage().convertToFormat(QImage::Format_Grayscale8)); 656 | if (serverUrl.size()) { 657 | servers.append(serverUrl); 658 | } 659 | } 660 | qInfo() << QString("Add %1 servers from QR code.") 661 | .arg(QString::number(servers.size())); 662 | addSubscriptionServers(servers.join('\n')); 663 | } 664 | 665 | void AppProxy::copyToClipboard(QString text) { 666 | QClipboard* clipboard = QGuiApplication::clipboard(); 667 | clipboard->setText(text, QClipboard::Clipboard); 668 | } 669 | 670 | void AppProxy::getLatestRelease(QString name) { 671 | if (!latestVersion.contains(name)) { 672 | QDateTime initTime = QDateTime::currentDateTime(); 673 | initTime.setSecsSinceEpoch(0); 674 | latestVersion[name] = { 675 | {"currentVersion", ""}, 676 | {"latestVersion", ""}, 677 | {"checkTime", initTime}, 678 | }; 679 | } 680 | 681 | if (latestVersion[name]["checkTime"].toDateTime().secsTo( 682 | QDateTime::currentDateTime()) < RELEASE_CHECK_INTERVAL) { 683 | emit latestReleaseReady(name, 684 | latestVersion[name]["latestVersion"].toString()); 685 | return; 686 | } 687 | if (name == "v2ray-core") { 688 | latestVersion[name]["currentVersion"] = v2ray.getVersion(); 689 | emit getLatestReleaseStarted(name, V2RAY_RELEASES_URL, getQProxy()); 690 | } else if (name == "v2ray-desktop") { 691 | latestVersion[name]["currentVersion"] = getAppVersion(); 692 | emit getLatestReleaseStarted(name, APP_RELEASES_URL, getQProxy()); 693 | } 694 | } 695 | 696 | void AppProxy::returnLatestRelease(QString name, QString version) { 697 | if (version.isEmpty()) { 698 | emit latestReleaseError(name, tr("Failed to check updates")); 699 | return; 700 | } 701 | if (!Utility::isVersionNewer(latestVersion[name]["currentVersion"].toString(), 702 | version)) { 703 | version = ""; 704 | } 705 | latestVersion[name]["checkTime"] = QDateTime::currentDateTime(); 706 | latestVersion[name]["latestVersion"] = version; 707 | emit latestReleaseReady(name, version); 708 | } 709 | 710 | void AppProxy::upgradeDependency(QString name, QString version) { 711 | if (!V2RAY_USE_LOCAL_INSTALL) { 712 | emit upgradeError(name, tr("Please upgrade from the package manager")); 713 | return; 714 | } else if (name == "v2ray-core" && 715 | QProcessEnvironment::systemEnvironment().contains("APPIMAGE")) { 716 | emit upgradeError(name, 717 | tr("The V2Ray Core in AppImage is not upgradable.")); 718 | return; 719 | } 720 | if (name == "v2ray-core") { 721 | #if defined(Q_OS_WIN) 722 | QString operatingSystem = "windows-amd64"; 723 | #elif defined(Q_OS_LINUX) 724 | QString operatingSystem = "linux-amd64"; 725 | #elif defined(Q_OS_MAC) 726 | QString operatingSystem = "darwin-amd64"; 727 | #else 728 | QString operatingSystem = "unknown"; 729 | #endif 730 | // TODO 731 | emit upgradeStarted( 732 | name, V2RAY_ASSETS_URL.arg(version, operatingSystem, version), 733 | Configurator::getAppTempDir().filePath(name), getQProxy()); 734 | } else if (name == "v2ray-desktop") { 735 | #if defined(Q_OS_WIN) 736 | QString operatingSystem = "win64"; 737 | QString fileExtension = "zip"; 738 | #elif defined(Q_OS_LINUX) 739 | QString operatingSystem = "linux-x86_64"; 740 | QString fileExtension = "AppImage"; 741 | #elif defined(Q_OS_MAC) 742 | QString operatingSystem = "macOS"; 743 | QString fileExtension = "zip"; 744 | #else 745 | QString operatingSystem = "unknown"; 746 | QString fileExtension = ""; 747 | #endif 748 | emit upgradeStarted( 749 | name, 750 | APP_ASSETS_URL.arg(version, version, operatingSystem, fileExtension), 751 | Configurator::getAppTempDir().filePath(name), getQProxy()); 752 | } 753 | } 754 | 755 | void AppProxy::replaceDependency(QString name, 756 | QString outputFilePath, 757 | QString errorMsg) { 758 | if (errorMsg.isEmpty()) { 759 | if (name == "v2ray-core") { 760 | v2ray.stop(); 761 | if (!replaceV2RayCoreFiles(Configurator::getV2RayInstallDirPath(), 762 | outputFilePath)) { 763 | errorMsg = tr("Failed to replace V2Ray Core files."); 764 | } 765 | v2ray.start(); 766 | } else if (name == "v2ray-desktop") { 767 | // TODO: Upgrade the app itself with an external upgrader 768 | } 769 | } 770 | if (!errorMsg.isEmpty()) { 771 | emit upgradeError(name, errorMsg); 772 | } else { 773 | emit upgradeCompleted(name); 774 | } 775 | } 776 | 777 | bool AppProxy::replaceV2RayCoreFiles(const QString& srcFolderPath, 778 | const QString& dstFolderPath) { 779 | #if defined(Q_OS_WIN) 780 | QString v2RayExecFilePath = QDir(dstFolderPath).filePath("v2ray.exe"); 781 | QString v2RayCtlExecFilePath = QDir(dstFolderPath).filePath("v2ctl.exe"); 782 | #elif defined(Q_OS_LINUX) or defined(Q_OS_MAC) 783 | QString v2RayExecFilePath = QDir(dstFolderPath).filePath("v2ray"); 784 | QString v2RayCtlExecFilePath = QDir(dstFolderPath).filePath("v2ctl"); 785 | #endif 786 | QFile(v2RayExecFilePath) 787 | .setPermissions(QFileDevice::ReadUser | QFileDevice::WriteOwner | 788 | QFileDevice::ExeUser); 789 | QFile(v2RayCtlExecFilePath) 790 | .setPermissions(QFileDevice::ReadUser | QFileDevice::WriteOwner | 791 | QFileDevice::ExeUser); 792 | 793 | QDir srcFolder(srcFolderPath); 794 | if (srcFolder.exists() && !srcFolder.removeRecursively()) { 795 | // TODO: fallback if errors occurred 796 | return false; 797 | } 798 | return QDir().rename(dstFolderPath, srcFolderPath); 799 | } 800 | -------------------------------------------------------------------------------- /src/appproxy.h: -------------------------------------------------------------------------------- 1 | #ifndef APPPROXY_H 2 | #define APPPROXY_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "appproxyworker.h" 13 | #include "configurator.h" 14 | #include "serverconfighelper.h" 15 | #include "v2raycore.h" 16 | 17 | class AppProxy : public QObject { 18 | Q_OBJECT 19 | Q_DISABLE_COPY(AppProxy) 20 | 21 | public: 22 | AppProxy(QObject* parent = 0); 23 | ~AppProxy(); 24 | 25 | signals: 26 | void getServerLatencyStarted(QJsonArray servers); 27 | void getGfwListStarted(QString gfwListUrl, QNetworkProxy proxy); 28 | void getNetworkStatusStarted(QMap urls, QNetworkProxy proxy); 29 | void getSubscriptionServersStarted(QString url, QNetworkProxy proxy); 30 | void getLogsStarted(QString appLogFilePath, QString v2RayLogFilePath); 31 | void getLatestReleaseStarted(QString name, 32 | QString releaseUrl, 33 | QNetworkProxy proxy); 34 | void upgradeStarted(QString name, 35 | QString assetsUrl, 36 | QString outputFolderPath, 37 | QNetworkProxy proxy); 38 | 39 | void appVersionReady(QString appVersion); 40 | void v2RayCoreVersionReady(QString v2RayCoreVersion); 41 | void operatingSystemReady(QString operatingSystem); 42 | void v2RayCoreStatusReady(bool isRunning); 43 | void networkStatusReady(QString networkStatus); 44 | void proxySettingsReady(QString proxySettings); 45 | void appConfigReady(QString appConfig); 46 | void appConfigError(QString errorMessage); 47 | void appConfigChanged(); 48 | void logsReady(QString logs); 49 | void proxyModeReady(QString proxyMode); 50 | void proxyModeChanged(QString proxyMode); 51 | void gfwListUpdated(QString gfwListUpdateTime); 52 | void serversReady(QString servers); 53 | void serverDInfoReady(QString server); 54 | void serverLatencyReady(QString latency); 55 | void serverConfigError(QString errorMessage); 56 | void serverConnectivityChanged(QString serverName, bool connected); 57 | void serverChanged(QString serverName, QString serverConfig); 58 | void serverRemoved(QString serverName); 59 | void serversChanged(); 60 | void latestReleaseReady(QString name, QString version); 61 | void latestReleaseError(QString name, QString errorMsg); 62 | void upgradeCompleted(QString name); 63 | void upgradeError(QString name, QString errorMsg); 64 | 65 | public slots: 66 | QString getAppVersion(); 67 | void getV2RayCoreVersion(); 68 | void getOperatingSystem(); 69 | void getV2RayCoreStatus(); 70 | void setV2RayCoreRunning(bool expectedRunning); 71 | void getNetworkStatus(); 72 | void getAppConfig(); 73 | void setAppConfig(QString configString); 74 | void setProxyMode(QString proxyMode = ""); 75 | void setSystemProxy(bool enableProxy, QString protocol = ""); 76 | void getProxySettings(); 77 | void updateGfwList(); 78 | void getLogs(); 79 | void clearLogs(); 80 | void getServers(); 81 | void getServer(QString serverName, bool forDuplicate = false); 82 | void getServerLatency(QString serverName = ""); 83 | void setServerConnection(QString serverName, bool connected); 84 | void addServer(QString protocol, QString configString); 85 | void addServerConfigFile(QString configFilePath, QString configFileType); 86 | void editServer(QString serverName, QString protocol, QString configString); 87 | void addServerUrl(QString serverUrl); 88 | void addSubscriptionUrl(QString subsriptionUrl); 89 | void updateSubscriptionServers(QString subsriptionUrl = ""); 90 | void removeServer(QString serverName); 91 | void removeSubscriptionServers(QString subscriptionUrl); 92 | void scanQrCodeScreen(); 93 | void copyToClipboard(QString text); 94 | bool retranslate(QString language = ""); 95 | void getLatestRelease(QString name); 96 | void upgradeDependency(QString name, QString version); 97 | 98 | private slots: 99 | void returnServerLatency(QMap latency); 100 | void returnGfwList(QString gfwList); 101 | void returnNetworkAccessiblity(QMap accessible); 102 | void addSubscriptionServers(QString subsriptionServers, 103 | QString subsriptionUrl = ""); 104 | void returnLogs(QString logs); 105 | void returnLatestRelease(QString name, QString version); 106 | void replaceDependency(QString name, 107 | QString outputFilePath, 108 | QString errorMsg); 109 | 110 | private: 111 | V2RayCore& v2ray; 112 | QJsonObject serverLatency; 113 | Configurator& configurator; 114 | QMap> latestVersion; 115 | 116 | AppProxyWorker* worker = new AppProxyWorker(); 117 | QThread workerThread; 118 | QTranslator translator; 119 | 120 | QNetworkProxy getQProxy(); 121 | void setAutoStart(bool autoStart); 122 | QStringList getAppConfigErrors(const QJsonObject& appConfig); 123 | bool replaceV2RayCoreFiles(const QString& srcFolderPath, 124 | const QString& dstFolderPath); 125 | }; 126 | 127 | #endif // APPPROXY_H 128 | -------------------------------------------------------------------------------- /src/appproxyworker.cpp: -------------------------------------------------------------------------------- 1 | #include "appproxyworker.h" 2 | #include "networkrequest.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "constants.h" 9 | #include "utility.h" 10 | 11 | AppProxyWorker::AppProxyWorker(QObject* parent) : QObject(parent) {} 12 | 13 | void AppProxyWorker::getServerLatency(QJsonArray servers) { 14 | QMap serverLatency; 15 | for (auto itr = servers.begin(); itr != servers.end(); ++itr) { 16 | QJsonObject server = (*itr).toObject(); 17 | QString serverName = server["name"].toString(); 18 | int latency = NetworkRequest::getLatency(server["server"].toString(), 19 | server["port"].toInt()); 20 | serverLatency[serverName] = latency; 21 | } 22 | emit serverLatencyReady(serverLatency); 23 | } 24 | 25 | void AppProxyWorker::getGfwList(QString gfwListUrl, QNetworkProxy proxy) { 26 | QNetworkProxy* p = 27 | proxy.type() == QNetworkProxy::ProxyType::NoProxy ? nullptr : &proxy; 28 | QString gfwList = NetworkRequest::getNetworkResponse(gfwListUrl, p); 29 | emit gfwListReady(gfwList); 30 | } 31 | 32 | void AppProxyWorker::getUrlAccessibility(QMap urls, 33 | QNetworkProxy proxy) { 34 | // Ref: 35 | // https://stackoverflow.com/questions/8517853/iterating-over-a-qmap-with-for 36 | // Note: Convert to StdMap for better performance 37 | QMap accessible; 38 | for (auto l : urls.toStdMap()) { 39 | QString url = l.first; 40 | bool useProxy = l.second; 41 | QNetworkProxy* p = useProxy ? &proxy : nullptr; 42 | QByteArray response = NetworkRequest::getNetworkResponse( 43 | QString("http://www.%1").arg(url), p, HTTP_GET_TIMEOUT); 44 | accessible[url] = response.size() > 0; 45 | } 46 | emit urlAccessibilityReady(accessible); 47 | } 48 | 49 | void AppProxyWorker::getSubscriptionServers(QString subscriptionUrl, 50 | QNetworkProxy proxy) { 51 | QNetworkProxy* p = 52 | proxy.type() == QNetworkProxy::ProxyType::NoProxy ? nullptr : &proxy; 53 | QByteArray response = NetworkRequest::getNetworkResponse(subscriptionUrl, p); 54 | QByteArray subscriptionServers = QByteArray::fromBase64(response); 55 | emit subscriptionServersReady(subscriptionServers, subscriptionUrl); 56 | } 57 | 58 | void AppProxyWorker::getLogs(QString appLogFilePath, QString v2RayLogFilePath) { 59 | QFile appLogFile(appLogFilePath); 60 | QFile v2RayLogFile(v2RayLogFilePath); 61 | QStringList logs; 62 | // Read the app and V2Ray logs 63 | if (appLogFile.open(QIODevice::ReadOnly | QIODevice::Text)) { 64 | QList _logList = appLogFile.readAll().split('\n'); 65 | int cnt = 0; 66 | for (auto itr = _logList.end() - 1; 67 | itr >= _logList.begin() && cnt <= MAX_N_LOGS; --itr, ++cnt) { 68 | logs.append(*itr); 69 | } 70 | appLogFile.close(); 71 | } 72 | if (v2RayLogFile.open(QIODevice::ReadOnly | QIODevice::Text)) { 73 | QList _logList = v2RayLogFile.readAll().split('\n'); 74 | int cnt = 0; 75 | for (auto itr = _logList.end() - 1; 76 | itr >= _logList.begin() && cnt <= MAX_N_LOGS; --itr, ++cnt) { 77 | logs.append(Utility::formatV2RayLog(*itr)); 78 | } 79 | v2RayLogFile.close(); 80 | } 81 | // Sort logs by timestamp 82 | logs.sort(); 83 | std::reverse(logs.begin(), logs.end()); 84 | 85 | emit logsReady(logs.join('\n')); 86 | } 87 | 88 | void AppProxyWorker::getLatestRelease(QString name, 89 | QString releaseUrl, 90 | QNetworkProxy proxy) { 91 | QNetworkProxy* p = 92 | proxy.type() == QNetworkProxy::ProxyType::NoProxy ? nullptr : &proxy; 93 | QString latestRelease = Utility::getLatestRelease(releaseUrl, p); 94 | 95 | emit latestReleaseReady(name, latestRelease); 96 | } 97 | -------------------------------------------------------------------------------- /src/appproxyworker.h: -------------------------------------------------------------------------------- 1 | #ifndef APPPROXYWORKER_H 2 | #define APPPROXYWORKER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | class AppProxyWorker : public QObject { 13 | Q_OBJECT 14 | public: 15 | explicit AppProxyWorker(QObject* parent = nullptr); 16 | 17 | public slots: 18 | void getServerLatency(QJsonArray servers); 19 | void getGfwList(QString gfwListUrl, QNetworkProxy proxy); 20 | void getUrlAccessibility(QMap urls, QNetworkProxy proxy); 21 | void getSubscriptionServers(QString url, QNetworkProxy proxy); 22 | void getLogs(QString appLogFilePath, QString v2RayLogFilePath); 23 | void getLatestRelease(QString name, QString releaseUrl, QNetworkProxy proxy); 24 | 25 | signals: 26 | void serverLatencyReady(QMap latency); 27 | void gfwListReady(QString gfwList); 28 | void urlAccessibilityReady(QMap accessible); 29 | void subscriptionServersReady(QString subscriptionServers, 30 | QString subscriptionUrl); 31 | void logsReady(QString logs); 32 | void latestReleaseReady(QString name, QString version); 33 | }; 34 | 35 | #endif // APPPROXYWORKER_H 36 | -------------------------------------------------------------------------------- /src/configurator.cpp: -------------------------------------------------------------------------------- 1 | #include "configurator.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 13 | #include 14 | #endif 15 | 16 | #include "constants.h" 17 | 18 | QJsonObject Configurator::DEFAULT_APP_CONFIG = { 19 | {"autoStart", DEFAULT_AUTO_START}, 20 | {"hideWindow", DEFAULT_HIDE_WINDOW}, 21 | {"enableSysProxy", AUTO_ENABLE_SYS_PROXY}, 22 | {"defaultSysProxyProtocol", DEFAULT_SYS_PROXY_PROTOCOL}, 23 | {"language", DEFAULT_LANGUAGE}, 24 | {"serverIp", DEFAULT_SERVER_IP}, 25 | {"httpPort", DEFAULT_HTTP_PORT}, 26 | {"socksPort", DEFAULT_SOCKS_PORT}, 27 | {"dns", DEFAULT_DNS_SERVER}, 28 | {"proxyMode", DEFAULT_PROXY_MODE}, 29 | {"gfwListUrl", DEFAULT_GFW_LIST_URL}, 30 | {"gfwListLastUpdated", "Never"}}; 31 | 32 | QString Configurator::getDefaultLanguage() { 33 | const static QMap LANGUAGES{ 34 | {QLocale::Chinese, "zh-CN"}}; 35 | QLocale::Language systemLocale = QLocale::system().language(); 36 | return LANGUAGES.contains(systemLocale) ? LANGUAGES[systemLocale] : "en-US"; 37 | } 38 | 39 | Configurator::Configurator() { connectedServerNames = getAutoConnectServers(); } 40 | 41 | QStringList Configurator::getAutoConnectServers() { 42 | QStringList autoConnectedServers; 43 | QJsonArray servers = getServers(); 44 | for (auto itr = servers.begin(); itr != servers.end(); ++itr) { 45 | QJsonObject server = (*itr).toObject(); 46 | if (server["autoConnect"].toBool() && server.contains("name")) { 47 | autoConnectedServers.append(server["name"].toString()); 48 | } 49 | } 50 | return autoConnectedServers; 51 | } 52 | 53 | Configurator &Configurator::getInstance() { 54 | static Configurator configuratorInstance; 55 | return configuratorInstance; 56 | } 57 | 58 | QDir Configurator::getAppConfigDir() { 59 | QDir appConfigDir = 60 | QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)) 61 | .filePath(QCoreApplication::applicationName()); 62 | if (!appConfigDir.exists()) { 63 | appConfigDir.mkpath("."); 64 | } 65 | return appConfigDir; 66 | } 67 | 68 | QDir Configurator::getAppTempDir() { 69 | QDir tempDir = 70 | QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)) 71 | .filePath(QCoreApplication::applicationName()); 72 | if (!tempDir.exists()) { 73 | tempDir.mkpath("."); 74 | } 75 | return tempDir; 76 | } 77 | 78 | QString Configurator::getV2RayInstallDirPath() { 79 | if (!V2RAY_USE_LOCAL_INSTALL) { 80 | return V2RAY_SYS_INSTALL_DIR; 81 | } 82 | return QDir(QCoreApplication::applicationDirPath()) 83 | .filePath(V2RAY_LOCAL_INSTALL_DIR); 84 | } 85 | 86 | QString Configurator::getLocaleDirPath() { 87 | return QDir(QCoreApplication::applicationDirPath()).filePath(LOCALE_DIR); 88 | } 89 | 90 | QString Configurator::getAppFilePath() { 91 | if (QProcessEnvironment::systemEnvironment().contains("APPIMAGE")) { 92 | return QProcessEnvironment::systemEnvironment().value("APPIMAGE"); 93 | } 94 | return QGuiApplication::applicationFilePath(); 95 | } 96 | 97 | QString Configurator::getAppWorkingDirPath() { 98 | if (QProcessEnvironment::systemEnvironment().contains("OWD")) { 99 | return QProcessEnvironment::systemEnvironment().value("OWD"); 100 | } 101 | return QCoreApplication::applicationDirPath(); 102 | } 103 | 104 | QString Configurator::getAppLogFilePath() { 105 | return getAppConfigDir().filePath(APP_LOG_FILE_NAME); 106 | } 107 | 108 | QString Configurator::getAppConfigFilePath() { 109 | return getAppConfigDir().filePath(APP_CFG_FILE_NAME); 110 | } 111 | 112 | QString Configurator::getV2RayLogFilePath() { 113 | return getAppConfigDir().filePath(V2RAY_CORE_LOG_FILE_NAME); 114 | } 115 | 116 | QString Configurator::getV2RayConfigFilePath() { 117 | return getAppConfigDir().filePath(V2RAY_CORE_CFG_FILE_NAME); 118 | } 119 | 120 | QString Configurator::getGfwListFilePath() { 121 | return getAppConfigDir().filePath(GFW_LIST_FILE_NAME); 122 | } 123 | 124 | QJsonObject Configurator::getAppConfig() { 125 | QJsonObject config = DEFAULT_APP_CONFIG; 126 | QFile appCfgFile(getAppConfigFilePath()); 127 | if (appCfgFile.exists() && 128 | appCfgFile.open(QIODevice::ReadOnly | QIODevice::Text)) { 129 | QJsonDocument configDoc = QJsonDocument::fromJson(appCfgFile.readAll()); 130 | config = configDoc.object(); 131 | if (config.empty()) { 132 | qWarning() << "Failed to parse the app config."; 133 | config = DEFAULT_APP_CONFIG; 134 | } 135 | appCfgFile.close(); 136 | } 137 | // Replace old proxy mode with a newer value 138 | QString proxyMode = config["proxyMode"].toString(); 139 | if (proxyMode == "pac" || proxyMode == "global" || proxyMode == "manual") { 140 | config["proxyMode"] = "rule"; 141 | } 142 | // Replace old GFW List URL with a newer value 143 | QString _glu = 144 | "https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt"; 145 | QString gfwListUrl = config["gfwListUrl"].toString(); 146 | if (gfwListUrl == _glu) { 147 | config["gfwListUrl"] = DEFAULT_GFW_LIST_URL; 148 | config["gfwListLastUpdated"] = "Never"; 149 | } 150 | return config; 151 | } 152 | 153 | QString Configurator::getLanguage() { 154 | QJsonObject appConfig = getAppConfig(); 155 | return appConfig.contains("language") ? appConfig["language"].toString() 156 | : getDefaultLanguage(); 157 | } 158 | 159 | void Configurator::setAppConfig(QJsonObject config) { 160 | // Load current configuration 161 | QJsonObject _config = getAppConfig(); 162 | // Overwrite new values to current configuration 163 | for (auto itr = config.begin(); itr != config.end(); ++itr) { 164 | QString configName = itr.key(); 165 | QVariant configValue = itr.value().toVariant(); 166 | #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 167 | if (configValue.metaType().id() == QMetaType::Bool) { 168 | _config[configName] = configValue.toBool(); 169 | } else if (configValue.metaType().id() == QMetaType::Double || 170 | configValue.metaType().id() == QMetaType::Int || 171 | configValue.metaType().id() == QMetaType::LongLong) { 172 | _config[configName] = configValue.toInt(); 173 | } else if (configValue.metaType().id() == QMetaType::QString) { 174 | _config[configName] = configValue.toString(); 175 | } else if (configValue.metaType().id() == QMetaType::QJsonArray) { 176 | _config[configName] = configValue.toJsonArray(); 177 | } else { 178 | qWarning() << "Ignore unknown config item [Name=" << configName 179 | << ", Type=" << configValue.metaType().name() << "]"; 180 | } 181 | #else 182 | switch (configValue.type()) { 183 | case QVariant::Bool: _config[configName] = configValue.toBool(); break; 184 | case QVariant::Double: 185 | case QVariant::Int: 186 | case QVariant::LongLong: _config[configName] = configValue.toInt(); break; 187 | case QVariant::String: 188 | _config[configName] = configValue.toString(); 189 | break; 190 | case QVariant::List: 191 | _config[configName] = configValue.toJsonArray(); 192 | break; 193 | default: 194 | qWarning() << "Ignore unknown config item [Name=" << configName 195 | << ", Type=" << configValue.type() << "]"; 196 | break; 197 | } 198 | #endif 199 | } 200 | // Save config to file 201 | QFile configFile(getAppConfigFilePath()); 202 | configFile.open(QFile::WriteOnly); 203 | configFile.write(QJsonDocument(_config).toJson()); 204 | } 205 | 206 | QJsonObject Configurator::getV2RayConfig() { 207 | QJsonObject appConfig = getAppConfig(); 208 | QJsonArray connectedServerNames; 209 | for (QString csn : getConnectedServerNames()) { 210 | connectedServerNames.append(csn); 211 | } 212 | 213 | QJsonObject v2RayConfig{ 214 | {"port", appConfig["httpPort"].toInt()}, 215 | {"socks-port", appConfig["socksPort"].toInt()}, 216 | {"allow-lan", true}, 217 | {"bind-address", appConfig["serverIp"].toString()}, 218 | {"mode", "rule"}, 219 | {"log-level", "info"}, 220 | {"dns", QJsonObject{{"enable", false}, 221 | {"listen", "0.0.0.0:53"}, 222 | {"nameserver", 223 | getPrettyDnsServers(appConfig["dns"].toString())}}}, 224 | {"proxies", getConnectedServers()}, 225 | {"proxy-groups", 226 | QJsonArray{{QJsonObject{{"name", "PROXY"}, 227 | {"type", "load-balance"}, 228 | {"proxies", connectedServerNames}, 229 | {"url", "http://www.gstatic.com/generate_204"}, 230 | {"interval", 300}}}}}, 231 | {"rules", getRules()}}; 232 | 233 | return v2RayConfig; 234 | } 235 | 236 | QJsonArray Configurator::getPrettyDnsServers(QString dnsString) { 237 | QJsonArray dnsServers; 238 | QStringList _dnsServers = dnsString.split(';'); 239 | for (QString ds : _dnsServers) { 240 | dnsServers.append(ds.trimmed()); 241 | } 242 | return dnsServers; 243 | } 244 | 245 | QJsonArray Configurator::getServers() { 246 | QJsonObject appConfig = getAppConfig(); 247 | QJsonArray servers = appConfig.contains("servers") 248 | ? appConfig["servers"].toArray() 249 | : QJsonArray(); 250 | return servers; 251 | } 252 | 253 | QJsonObject Configurator::getServer(QString serverName) { 254 | QJsonArray servers = getServers(); 255 | QJsonObject server; 256 | for (auto itr = servers.begin(); itr != servers.end(); ++itr) { 257 | QJsonObject _server = (*itr).toObject(); 258 | if (_server.contains("name") && _server["name"].toString() == serverName) { 259 | server = (*itr).toObject(); 260 | break; 261 | } 262 | } 263 | return server; 264 | } 265 | 266 | QStringList Configurator::getSubscriptionUrls() { 267 | QStringList subscriptionUrls; 268 | QJsonArray servers = getServers(); 269 | for (auto itr = servers.begin(); itr != servers.end(); ++itr) { 270 | QJsonObject server = (*itr).toObject(); 271 | if (server.contains("subscription")) { 272 | QString subscriptionUrl = server["subscription"].toString(); 273 | if (!subscriptionUrls.contains(subscriptionUrl)) { 274 | subscriptionUrls.append(subscriptionUrl); 275 | } 276 | } 277 | } 278 | return subscriptionUrls; 279 | } 280 | 281 | int Configurator::addServer(QJsonObject serverConfig) { 282 | QJsonArray servers = getServers(); 283 | servers.append(serverConfig); 284 | setAppConfig(QJsonObject{{"servers", servers}}); 285 | return 1; 286 | } 287 | 288 | int Configurator::editServer(QString serverName, QJsonObject serverConfig) { 289 | QJsonArray servers = getServers(); 290 | bool isEdited = false; 291 | for (auto itr = servers.begin(); itr != servers.end(); ++itr) { 292 | QJsonObject server = (*itr).toObject(); 293 | if (server.contains("name") && server["name"].toString() == serverName) { 294 | serverConfig["subscription"] = server.contains("subscription") 295 | ? server["subscription"].toString() 296 | : ""; 297 | *itr = serverConfig; 298 | isEdited = true; 299 | } 300 | } 301 | // Update the server in connected servers 302 | if (connectedServerNames.contains(serverName)) { 303 | connectedServerNames.removeAt(connectedServerNames.indexOf(serverName)); 304 | connectedServerNames.append(serverConfig["name"].toString()); 305 | } 306 | // Update servers in the config 307 | setAppConfig(QJsonObject{{"servers", servers}}); 308 | return isEdited; 309 | } 310 | 311 | int Configurator::removeServer(QString serverName) { 312 | QJsonArray servers = getServers(); 313 | int serverIndex = -1; 314 | for (int i = 0; i < servers.size(); ++i) { 315 | QJsonObject server = servers[i].toObject(); 316 | if (server.contains("name") && server["name"].toString() == serverName) { 317 | serverIndex = i; 318 | break; 319 | } 320 | } 321 | servers.removeAt(serverIndex); 322 | // Remove the server from connected servers 323 | if (connectedServerNames.contains(serverName)) { 324 | connectedServerNames.removeAt(connectedServerNames.indexOf(serverName)); 325 | } 326 | // Update servers in the config 327 | setAppConfig(QJsonObject{{"servers", servers}}); 328 | return serverIndex != -1; 329 | } 330 | 331 | QMap Configurator::removeSubscriptionServers( 332 | QString subscriptionUrl) { 333 | QJsonArray servers = getServers(); 334 | QMap removedServers; 335 | QJsonArray remainingServers; 336 | 337 | for (int i = 0; i < servers.size(); ++i) { 338 | QJsonObject server = servers[i].toObject(); 339 | QString serverName = 340 | server.contains("name") ? server["name"].toString() : ""; 341 | 342 | if (server.contains("subscription") && 343 | server["subscription"].toString() == subscriptionUrl) { 344 | removedServers[serverName] = server; 345 | // Remove the server from connected servers 346 | if (connectedServerNames.contains(serverName)) { 347 | connectedServerNames.removeAt(connectedServerNames.indexOf(serverName)); 348 | } 349 | continue; 350 | } 351 | remainingServers.append(server); 352 | } 353 | // Update servers in the config 354 | setAppConfig(QJsonObject{{"servers", remainingServers}}); 355 | return removedServers; 356 | } 357 | 358 | QJsonArray Configurator::getConnectedServers() { 359 | QJsonObject appConfig = getAppConfig(); 360 | 361 | QJsonArray servers = getServers(); 362 | QJsonArray connectedServers; 363 | for (auto itr = servers.begin(); itr != servers.end(); ++itr) { 364 | QJsonObject server = (*itr).toObject(); 365 | QString serverName = 366 | server.contains("name") ? server["name"].toString() : ""; 367 | 368 | if (connectedServerNames.contains(serverName)) { 369 | server.remove("autoConnect"); 370 | server.remove("subscription"); 371 | connectedServers.append(server); 372 | } 373 | } 374 | return connectedServers; 375 | } 376 | 377 | QStringList Configurator::getConnectedServerNames() { 378 | return connectedServerNames; 379 | } 380 | 381 | void Configurator::setServerConnection(QString serverName, bool connected) { 382 | int serverIndex = connectedServerNames.indexOf(serverName); 383 | if (connected && serverIndex == -1) { 384 | connectedServerNames.append(serverName); 385 | } else if (!connected && serverIndex != -1) { 386 | connectedServerNames.removeAt(serverIndex); 387 | } 388 | } 389 | 390 | QJsonArray Configurator::getRules() { 391 | QJsonObject appConfig = getAppConfig(); 392 | QString proxyMode = appConfig["proxyMode"].toString(); 393 | QString defaultAct = "MATCH, PROXY"; 394 | 395 | QJsonArray rules; 396 | if (proxyMode == "Direct") { 397 | defaultAct = "MATCH, DIRECT"; 398 | } else if (proxyMode == "Global") { 399 | defaultAct = "MATCH, PROXY"; 400 | } else { 401 | QJsonArray userRules = getGfwListRules(); 402 | QJsonArray gfwListRules = getGfwListRules(); 403 | if (userRules.size() + gfwListRules.size() == 0) { 404 | defaultAct = "MATCH, PROXY"; 405 | } 406 | rules.append("IP-CIDR, 127.0.0.0/8, DIRECT"); 407 | rules.append("IP-CIDR, 10.0.0.0/8, DIRECT"); 408 | rules.append("IP-CIDR, 172.16.0.0/12, DIRECT"); 409 | rules.append("IP-CIDR, 192.168.0.0/16, DIRECT"); 410 | for (auto itr = userRules.begin(); itr != userRules.end(); ++itr) { 411 | rules.append(*itr); 412 | } 413 | for (auto itr = gfwListRules.begin(); itr != gfwListRules.end(); ++itr) { 414 | rules.append(*itr); 415 | } 416 | rules.append("GEOIP, CN, DIRECT"); 417 | } 418 | rules.append(defaultAct); 419 | return rules; 420 | } 421 | 422 | QJsonArray Configurator::getGfwListRules() { 423 | return getRules(getGfwListFilePath()); 424 | } 425 | 426 | QJsonArray Configurator::getUserRules() { 427 | QJsonArray rules; 428 | // TODO 429 | return rules; 430 | } 431 | 432 | QJsonArray Configurator::getRules(const QString &fileName) { 433 | static const QStringList SUPPORTED_MATCHES{ 434 | "DOMAIN-SUFFIX", "DOMAIN", "DOMAIN-KEYWORD", "IP-CIDR", 435 | "SRC-IP-CIDR", "DST-PORT", "SRC-PORT"}; 436 | static const QStringList SUPPORTED_ACTS{"PROXY", "DIRECT", "REJECT"}; 437 | QJsonArray rules; 438 | QFile ruleFile(fileName); 439 | 440 | if (ruleFile.exists() && 441 | ruleFile.open(QIODevice::ReadOnly | QIODevice::Text)) { 442 | while (!ruleFile.atEnd()) { 443 | QString line = ruleFile.readLine().trimmed(); 444 | if (!line.startsWith('-')) { 445 | continue; 446 | } 447 | 448 | QStringList rule = line.mid(1).split(','); 449 | if (rule.size() != 3) { 450 | qWarning() << QString("Ignore rule: %1 in %2").arg(line, fileName); 451 | continue; 452 | } 453 | QString match = rule.at(0).trimmed().toUpper(); 454 | QString value = rule.at(1).trimmed().toLower(); 455 | QString action = rule.at(2).trimmed().toUpper(); 456 | 457 | if (!SUPPORTED_MATCHES.contains(match)) { 458 | qWarning() << QString("Unsupported match: %1 for rule: %2 in %3") 459 | .arg(match, line, fileName); 460 | continue; 461 | } 462 | if (!SUPPORTED_ACTS.contains(action)) { 463 | qWarning() << QString("Unsupported match: %1 for rule: %2 in %3") 464 | .arg(match, line, fileName); 465 | continue; 466 | } 467 | rules.append(QString("%1, %2, %3").arg(match, value, action)); 468 | } 469 | } 470 | return rules; 471 | } 472 | -------------------------------------------------------------------------------- /src/configurator.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIGURATOR_H 2 | #define CONFIGURATOR_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class Configurator : public QObject { 10 | Q_OBJECT 11 | public: 12 | static Configurator& getInstance(); 13 | Configurator(Configurator const&) = delete; 14 | void operator=(Configurator const&) = delete; 15 | static QDir getAppConfigDir(); 16 | static QDir getAppTempDir(); 17 | static QString getAppFilePath(); 18 | static QString getAppWorkingDirPath(); 19 | static QString getV2RayInstallDirPath(); 20 | static QString getLocaleDirPath(); 21 | static QString getAppLogFilePath(); 22 | static QString getAppConfigFilePath(); 23 | static QString getV2RayLogFilePath(); 24 | static QString getV2RayConfigFilePath(); 25 | static QString getGfwListFilePath(); 26 | QJsonObject getAppConfig(); 27 | QString getLanguage(); 28 | QJsonObject getV2RayConfig(); 29 | void setAppConfig(QJsonObject config); 30 | QJsonArray getServers(); 31 | QJsonObject getServer(QString serverName); 32 | QStringList getSubscriptionUrls(); 33 | int addServer(QJsonObject serverConfig); 34 | int editServer(QString serverName, QJsonObject serverConfig); 35 | int removeServer(QString serverName); 36 | QMap removeSubscriptionServers(QString subscriptionUrl); 37 | QJsonArray getConnectedServers(); 38 | QStringList getConnectedServerNames(); 39 | void setServerConnection(QString serverName, bool connected); 40 | QJsonArray getRules(); 41 | QJsonArray getGfwListRules(); 42 | QJsonArray getUserRules(); 43 | 44 | private: 45 | Configurator(); 46 | static QJsonObject DEFAULT_APP_CONFIG; 47 | QStringList connectedServerNames; 48 | static QString getDefaultLanguage(); 49 | QJsonArray getPrettyDnsServers(QString dnsString); 50 | QStringList getAutoConnectServers(); 51 | QJsonArray getRules(const QString& fileName); 52 | }; 53 | 54 | #endif // CONFIGURATOR_H 55 | -------------------------------------------------------------------------------- /src/constants.h: -------------------------------------------------------------------------------- 1 | #ifndef CONSTANTS_H 2 | #define CONSTANTS_H 3 | 4 | #include 5 | 6 | static const QString APP_NAME = "V2Ray-Desktop"; 7 | static const int APP_VERSION_MAJOR = 2; 8 | static const int APP_VERSION_MINOR = 4; 9 | static const int APP_VERSION_PATCH = 0; 10 | 11 | static const QString APP_RELEASES_URL = 12 | "https://api.github.com/repos/Dr-Incognito/V2Ray-Desktop/releases"; 13 | static const QString APP_ASSETS_URL = 14 | "https://github.com/Dr-Incognito/V2Ray-Desktop/releases/download/%1/" 15 | "V2Ray-Desktop-v%2-%3.%4"; 16 | static const QString V2RAY_RELEASES_URL = 17 | "https://api.github.com/repos/Dreamacro/clash/releases"; 18 | static const QString V2RAY_ASSETS_URL = 19 | "https://github.com/Dreamacro/clash/releases/download/%1/clash-%2-%3.%4"; 20 | 21 | static const int RELEASE_CHECK_INTERVAL = 7200; 22 | static const int TCP_PING_TIMEOUT = 2500; 23 | static const int HTTP_GET_TIMEOUT = 2500; 24 | static const int MAX_N_LOGS = 2500; 25 | 26 | static const bool V2RAY_USE_LOCAL_INSTALL = true; 27 | static const QString V2RAY_LOCAL_INSTALL_DIR = "clash-core"; 28 | static const QString V2RAY_SYS_INSTALL_DIR = "/usr/bin"; 29 | static const QString LOCALE_DIR = "locales"; 30 | static const QString V2RAY_CORE_LOG_FILE_NAME = "clash.log"; 31 | static const QString V2RAY_CORE_CFG_FILE_NAME = "config.yaml"; 32 | static const QString APP_LOG_FILE_NAME = "v2ray-desktop.log"; 33 | static const QString APP_CFG_FILE_NAME = "config.json"; 34 | static const QString GFW_LIST_FILE_NAME = "gfwlist.yml"; 35 | 36 | static const int DEFAULT_V2RAY_KCP_MTU = 1350; 37 | static const int DEFAULT_V2RAY_KCP_TTI = 50; 38 | static const int DEFAULT_V2RAY_KCP_UP_CAPACITY = 5; 39 | static const int DEFAULT_V2RAY_KCP_DOWN_CAPACITY = 20; 40 | static const int DEFAULT_V2RAY_KCP_READ_BUF_SIZE = 2; 41 | static const int DEFAULT_V2RAY_KCP_WRITE_BUF_SIZE = 2; 42 | static const QString DEFAULT_TROJRAN_SNI = ""; 43 | static const QString DEFAULT_TROJRAN_ALPN = "h2; http/1.1"; 44 | static const bool DEFAULT_TROJRAN_ENABLE_UDP = false; 45 | static const bool DEFAULT_TROJRAN_ALLOW_INSECURE = false; 46 | 47 | static const bool DEFAULT_AUTO_START = true; 48 | static const bool DEFAULT_HIDE_WINDOW = false; 49 | static const bool AUTO_ENABLE_SYS_PROXY = false; 50 | static const QString DEFAULT_SYS_PROXY_PROTOCOL = "http"; 51 | static const QString DEFAULT_LANGUAGE = "en-US"; 52 | static const QString DEFAULT_SERVER_IP = "127.0.0.1"; 53 | static const int DEFAULT_SOCKS_PORT = 1080; 54 | static const int DEFAULT_HTTP_PORT = 1087; 55 | static const QString DEFAULT_DNS_SERVER = "8.8.8.8; 4.4.4.4"; 56 | static const QString DEFAULT_PROXY_MODE = "Rule"; 57 | static const QString DEFAULT_GFW_LIST_URL = 58 | "https://raw.githubusercontent.com/du5/gfwlist/master/Rules/Clash/" 59 | "gfwlist.yml"; 60 | 61 | #endif // CONSTANTS_H 62 | -------------------------------------------------------------------------------- /src/images/icon-about.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/icon-dashboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/icon-logs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/icon-rules.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/icon-servers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/icon-settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dr-Incognito/V2Ray-Desktop/6f00169933c5ac64dd458661c1f5d328d869a5f7/src/images/logo.png -------------------------------------------------------------------------------- /src/images/v2ray.gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dr-Incognito/V2Ray-Desktop/6f00169933c5ac64dd458661c1f5d328d869a5f7/src/images/v2ray.gray.png -------------------------------------------------------------------------------- /src/images/v2ray.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dr-Incognito/V2Ray-Desktop/6f00169933c5ac64dd458661c1f5d328d869a5f7/src/images/v2ray.icns -------------------------------------------------------------------------------- /src/images/v2ray.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dr-Incognito/V2Ray-Desktop/6f00169933c5ac64dd458661c1f5d328d869a5f7/src/images/v2ray.ico -------------------------------------------------------------------------------- /src/images/v2ray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dr-Incognito/V2Ray-Desktop/6f00169933c5ac64dd458661c1f5d328d869a5f7/src/images/v2ray.png -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include "appproxy.h" 19 | #include "constants.h" 20 | #include "runguard.h" 21 | 22 | void messageHandler(QtMsgType msgType, 23 | const QMessageLogContext &context, 24 | const QString &msg) { 25 | Q_UNUSED(context); 26 | static QString lastMsg; 27 | if (lastMsg == msg) { 28 | return; 29 | } 30 | lastMsg = msg; 31 | 32 | QString dt = QDateTime::currentDateTime().toString("yyyy/MM/dd hh:mm:ss"); 33 | QString logMessage("%1 %2 v2ray-desktop: %3"); 34 | QString msgTypeStr; 35 | switch (msgType) { 36 | case QtDebugMsg: msgTypeStr = "[Debug]"; break; 37 | case QtInfoMsg: msgTypeStr = "[Info]"; break; 38 | case QtWarningMsg: msgTypeStr = "[Warning]"; break; 39 | case QtCriticalMsg: msgTypeStr = "[Critical]"; break; 40 | case QtFatalMsg: msgTypeStr = "[Fatal]"; break; 41 | default: break; 42 | } 43 | if (msgType != QtDebugMsg) { 44 | QFile logFile(Configurator::getAppLogFilePath()); 45 | logFile.open(QIODevice::WriteOnly | QIODevice::Append); 46 | QTextStream logTextStream(&logFile); 47 | logTextStream << logMessage.arg(dt, msgTypeStr, msg) << Qt::endl; 48 | logFile.close(); 49 | } else { 50 | QTextStream(stdout) << logMessage.arg(dt, msgTypeStr, msg) << Qt::endl; 51 | } 52 | } 53 | 54 | int main(int argc, char *argv[]) { 55 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 56 | QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 57 | #endif 58 | QApplication app(argc, argv); 59 | app.setApplicationName(APP_NAME); 60 | app.setApplicationVersion(QString("v%1.%2.%3") 61 | .arg(QString::number(APP_VERSION_MAJOR), 62 | QString::number(APP_VERSION_MINOR), 63 | QString::number(APP_VERSION_PATCH))); 64 | app.setWindowIcon(QIcon(":/images/v2ray.ico")); 65 | app.setOrganizationName("V2Ray"); 66 | app.setOrganizationDomain("v2ray.com"); 67 | 68 | // Make sure there is no other instance running 69 | RunGuard runGuard(APP_NAME); 70 | if (!runGuard.tryToRun()) { 71 | QMessageBox msgBox; 72 | msgBox.setIcon(QMessageBox::Icon::Critical); 73 | msgBox.setText( 74 | QString(QObject::tr("There is another %1 instance running!\n")) 75 | .arg(APP_NAME)); 76 | msgBox.exec(); 77 | return 127; 78 | } 79 | 80 | // Set up QML and AppProxy 81 | #if defined(Q_OS_WIN) 82 | QFont font("Microsoft YaHei", 10); 83 | app.setFont(font); 84 | #endif 85 | #if defined(Q_OS_LINUX) 86 | if (QProcessEnvironment::systemEnvironment().value("XDG_CURRENT_DESKTOP") == 87 | "KDE") { 88 | QQuickStyle::setStyle("fusion"); 89 | } 90 | #endif 91 | qmlRegisterSingletonType( 92 | "com.v2ray.desktop.AppProxy", APP_VERSION_MAJOR, APP_VERSION_MINOR, 93 | "AppProxy", [](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject * { 94 | Q_UNUSED(engine) 95 | Q_UNUSED(scriptEngine) 96 | AppProxy *appProxy = new AppProxy(); 97 | return appProxy; 98 | }); 99 | 100 | // Set up logging 101 | qInstallMessageHandler(messageHandler); 102 | 103 | // Set up the application 104 | QQmlApplicationEngine engine; 105 | const QUrl url(QStringLiteral("qrc:/ui/main.qml")); 106 | QObject::connect( 107 | &engine, &QQmlApplicationEngine::objectCreated, &app, 108 | [url](QObject *obj, const QUrl &objUrl) { 109 | if (!obj && url == objUrl) { 110 | QApplication::exit(-1); 111 | } 112 | }, 113 | Qt::QueuedConnection); 114 | engine.load(url); 115 | 116 | return app.exec(); 117 | } 118 | -------------------------------------------------------------------------------- /src/misc/tpl-linux-autostart.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=V2Ray Desktop 3 | Exec=%1 4 | Icon=v2ray-desktop 5 | Type=Application 6 | Categories=Utility; 7 | Terminal=false 8 | X-GNOME-Autostart-enabled=true 9 | -------------------------------------------------------------------------------- /src/misc/tpl-macos-autostart.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.v2ray.desktop 7 | LimitLoadToSessionType 8 | Aqua 9 | ProgramArguments 10 | 11 | %1 12 | 13 | RunAtLoad 14 | 15 | StandardErrorPath 16 | /dev/null 17 | StandardOutPath 18 | /dev/null 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/networkproxy.cpp: -------------------------------------------------------------------------------- 1 | #include "networkproxy.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | NetworkProxy NetworkProxyHelper::getSystemProxy() { 10 | #if defined(Q_OS_WIN) 11 | return getSystemProxyWindows(); 12 | #elif defined(Q_OS_MAC) 13 | return getSystemProxyMacOs(); 14 | #elif defined(Q_OS_LINUX) 15 | QString desktopEnv = 16 | QProcessEnvironment::systemEnvironment().value("XDG_CURRENT_DESKTOP"); 17 | if (desktopEnv.contains("GNOME") || desktopEnv.contains("Unity")) { 18 | return getSystemProxyLinuxGnome(); 19 | } else if (desktopEnv.contains("KDE")) { 20 | return getSystemProxyLinuxKde(); 21 | } else { 22 | return NetworkProxy(); 23 | } 24 | #else 25 | return NetworkProxy(); 26 | #endif 27 | } 28 | 29 | void NetworkProxyHelper::setSystemProxy(const NetworkProxy& proxy) { 30 | #if defined(Q_OS_WIN) 31 | return setSystemProxyWindows(proxy); 32 | #elif defined(Q_OS_MAC) 33 | return setSystemProxyMacOs(proxy); 34 | #elif defined(Q_OS_LINUX) 35 | QString desktopEnv = 36 | QProcessEnvironment::systemEnvironment().value("XDG_CURRENT_DESKTOP"); 37 | if (desktopEnv.contains("GNOME") || desktopEnv.contains("Unity")) { 38 | return setSystemProxyLinuxGnome(proxy); 39 | } else if (desktopEnv.contains("KDE")) { 40 | return setSystemProxyLinuxKde(proxy); 41 | } 42 | #endif 43 | } 44 | 45 | void NetworkProxyHelper::resetSystemProxy() { 46 | #if defined(Q_OS_WIN) 47 | return resetSystemProxyWindows(); 48 | #elif defined(Q_OS_MAC) 49 | return resetSystemProxyMacOs(); 50 | #elif defined(Q_OS_LINUX) 51 | QString desktopEnv = 52 | QProcessEnvironment::systemEnvironment().value("XDG_CURRENT_DESKTOP"); 53 | if (desktopEnv.contains("GNOME") || desktopEnv.contains("Unity")) { 54 | return resetSystemProxyLinuxGnome(); 55 | } else if (desktopEnv.contains("KDE")) { 56 | return resetSystemProxyLinuxKde(); 57 | } 58 | #endif 59 | } 60 | 61 | #if defined(Q_OS_WIN) 62 | NetworkProxy NetworkProxyHelper::getSystemProxyWindows() { 63 | NetworkProxy proxy; 64 | QSettings internetSettings( 65 | "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet " 66 | "Settings", 67 | QSettings::NativeFormat); 68 | if (internetSettings.contains("AutoConfigURL")) { 69 | proxy.setMode(NetworkProxyMode::PAC_MODE); 70 | proxy.setHost(internetSettings.value("AutoConfigURL").toString()); 71 | proxy.setPort(0); 72 | } else if (internetSettings.value("ProxyEnable").toInt() == 1 && 73 | internetSettings.contains("ProxyServer")) { 74 | QString proxyServer = internetSettings.value("ProxyServer").toString(); 75 | int colonIndex = proxyServer.indexOf(':'); 76 | proxy.setProtocol("http"); 77 | proxy.setMode(NetworkProxyMode::GLOBAL_MODE); 78 | proxy.setHost(proxyServer.left(colonIndex)); 79 | proxy.setPort(proxyServer.mid(colonIndex + 1).toInt()); 80 | } 81 | return proxy; 82 | } 83 | 84 | void NetworkProxyHelper::setSystemProxyWindows(const NetworkProxy& proxy) { 85 | QSettings internetSettings( 86 | "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet " 87 | "Settings", 88 | QSettings::NativeFormat); 89 | if (proxy.getMode() == NetworkProxyMode::GLOBAL_MODE) { 90 | internetSettings.setValue("ProxyEnable", 1); 91 | internetSettings.setValue( 92 | "ProxyServer", 93 | QString("%1:%2").arg(proxy.getHost(), QString::number(proxy.getPort()))); 94 | } 95 | } 96 | 97 | void NetworkProxyHelper::resetSystemProxyWindows() { 98 | QSettings internetSettings( 99 | "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet " 100 | "Settings", 101 | QSettings::NativeFormat); 102 | internetSettings.setValue("ProxyEnable", 0); 103 | internetSettings.remove("AutoConfigURL"); 104 | internetSettings.remove("ProxyServer"); 105 | } 106 | #elif defined(Q_OS_MAC) 107 | const QStringList NetworkProxyHelper::NETWORK_INTERFACES = {"Wi-Fi", 108 | "Enternet"}; 109 | 110 | NetworkProxy NetworkProxyHelper::getSystemProxyMacOs() { 111 | NetworkProxy proxy; 112 | QString stdOutput; 113 | for (QString ni : NETWORK_INTERFACES) { 114 | QProcess p; 115 | p.start("networksetup", QStringList{"-getautoproxyurl", ni}); 116 | p.waitForFinished(); 117 | stdOutput = p.readAllStandardOutput(); 118 | if (!stdOutput.startsWith("** Error:")) { 119 | QStringList sl = stdOutput.split('\n'); 120 | for (QString s : sl) { 121 | if (s.startsWith("Enabled: Yes")) { 122 | proxy.setMode(NetworkProxyMode::PAC_MODE); 123 | } else if (s.startsWith("URL: ")) { 124 | proxy.setHost(s.mid(5)); 125 | proxy.setPort(0); 126 | } 127 | } 128 | } 129 | p.start("networksetup", QStringList{"-getwebproxy", ni}); 130 | p.waitForFinished(); 131 | stdOutput = p.readAllStandardOutput(); 132 | if (!stdOutput.startsWith("** Error:")) { 133 | QStringList sl = stdOutput.split('\n'); 134 | for (QString s : sl) { 135 | if (s.startsWith("Enabled: Yes")) { 136 | proxy.setMode(NetworkProxyMode::GLOBAL_MODE); 137 | proxy.setProtocol("http"); 138 | } else if (proxy.getProtocol() == "http" && s.startsWith("Server: ")) { 139 | proxy.setHost(s.mid(8)); 140 | } else if (proxy.getProtocol() == "http" && s.startsWith("Port: ")) { 141 | proxy.setPort(s.mid(6).toInt()); 142 | } 143 | } 144 | } 145 | p.start("networksetup", QStringList{"-getsocksfirewallproxy", ni}); 146 | p.waitForFinished(); 147 | stdOutput = p.readAllStandardOutput(); 148 | if (!stdOutput.startsWith("** Error:")) { 149 | QStringList sl = stdOutput.split('\n'); 150 | for (QString s : sl) { 151 | if (s.startsWith("Enabled: Yes")) { 152 | proxy.setMode(NetworkProxyMode::GLOBAL_MODE); 153 | proxy.setProtocol("socks"); 154 | } else if (proxy.getProtocol() == "socks" && s.startsWith("Server: ")) { 155 | proxy.setHost(s.mid(8)); 156 | } else if (proxy.getProtocol() == "socks" && s.startsWith("Port: ")) { 157 | proxy.setPort(s.mid(6).toInt()); 158 | } 159 | } 160 | } 161 | } 162 | return proxy; 163 | } 164 | 165 | void NetworkProxyHelper::setSystemProxyMacOs(const NetworkProxy& proxy) { 166 | QString protocol = proxy.getProtocol(); 167 | static const QMap SETTING_KEYS = { 168 | {"http", "web"}, {"socks", "socksfirewall"}}; 169 | 170 | if (proxy.getMode() == NetworkProxyMode::GLOBAL_MODE) { 171 | QString settingKey = SETTING_KEYS.value(protocol); 172 | QProcess p; 173 | for (QString ni : NETWORK_INTERFACES) { 174 | p.start("networksetup", 175 | QStringList{QString("-set%1proxy").arg(settingKey), ni, 176 | proxy.getHost(), QString::number(proxy.getPort())}); 177 | p.waitForFinished(); 178 | p.start( 179 | "networksetup", 180 | QStringList{QString("-set%1proxystate").arg(settingKey), ni, "on"}); 181 | p.waitForFinished(); 182 | } 183 | } 184 | } 185 | 186 | void NetworkProxyHelper::resetSystemProxyMacOs() { 187 | for (QString ni : NETWORK_INTERFACES) { 188 | QProcess p; 189 | p.start("networksetup", QStringList{"-setautoproxystate", ni, "off"}); 190 | p.waitForFinished(); 191 | p.start("networksetup", QStringList{"-setwebproxystate", ni, "off"}); 192 | p.waitForFinished(); 193 | p.start("networksetup", 194 | QStringList{"-setsocksfirewallproxystate", ni, "off"}); 195 | p.waitForFinished(); 196 | } 197 | } 198 | #elif defined(Q_OS_LINUX) 199 | NetworkProxy NetworkProxyHelper::getSystemProxyLinuxGnome() { 200 | QProcess p; 201 | NetworkProxy proxy; 202 | p.start("gsettings", 203 | QStringList{"list-recursively", "org.gnome.system.proxy"}); 204 | p.waitForFinished(); 205 | QList settings = p.readAllStandardOutput().split('\n'); 206 | for (QByteArray s : settings) { 207 | if (s.startsWith("org.gnome.system.proxy mode")) { 208 | int qIndex = s.indexOf('\''); 209 | QString proxyMode = s.mid(qIndex + 1, s.lastIndexOf('\'') - qIndex - 1); 210 | if (proxyMode == "none") { 211 | proxy.setMode(NetworkProxyMode::DIRECT_MODE); 212 | } else if (proxyMode == "auto") { 213 | proxy.setMode(NetworkProxyMode::PAC_MODE); 214 | } else if (proxyMode == "manual") { 215 | proxy.setMode(NetworkProxyMode::GLOBAL_MODE); 216 | } 217 | } 218 | if (proxy.getMode() == NetworkProxyMode::PAC_MODE) { 219 | if (s.startsWith("org.gnome.system.proxy autoconfig-url")) { 220 | int qIndex = s.indexOf('\''); 221 | proxy.setHost(s.mid(qIndex + 1, s.lastIndexOf('\'') - qIndex - 1)); 222 | proxy.setPort(0); 223 | } 224 | } else if (proxy.getMode() == NetworkProxyMode::GLOBAL_MODE) { 225 | if (s.startsWith("org.gnome.system.proxy.http port")) { 226 | int port = s.mid(33).toInt(); 227 | if (port) { 228 | proxy.setPort(port); 229 | } 230 | } else if (s.startsWith("org.gnome.system.proxy.http host")) { 231 | int qIndex = s.indexOf('\''); 232 | QString host = s.mid(qIndex + 1, s.lastIndexOf('\'') - qIndex - 1); 233 | if (host.size()) { 234 | proxy.setHost(host); 235 | proxy.setProtocol("http"); 236 | } 237 | } else if (s.startsWith("org.gnome.system.proxy.socks port")) { 238 | int port = s.mid(33).toInt(); 239 | if (port) { 240 | proxy.setPort(port); 241 | } 242 | } else if (s.startsWith("org.gnome.system.proxy.socks host")) { 243 | int qIndex = s.indexOf('\''); 244 | QString host = s.mid(qIndex + 1, s.lastIndexOf('\'') - qIndex - 1); 245 | if (host.size()) { 246 | proxy.setHost(host); 247 | proxy.setProtocol("socks"); 248 | } 249 | } 250 | } 251 | } 252 | return proxy; 253 | } 254 | 255 | void NetworkProxyHelper::setSystemProxyLinuxGnome(const NetworkProxy& proxy) { 256 | if (proxy.getMode() == NetworkProxyMode::GLOBAL_MODE) { 257 | QProcess p; 258 | p.start("gsettings", 259 | QStringList{"set", "org.gnome.system.proxy", "mode", "manual"}); 260 | p.waitForFinished(); 261 | p.start( 262 | "gsettings", 263 | QStringList{"set", 264 | QString("org.gnome.system.proxy.%1").arg(proxy.getProtocol()), 265 | "host", proxy.getHost()}); 266 | p.waitForFinished(); 267 | p.start( 268 | "gsettings", 269 | QStringList{"set", 270 | QString("org.gnome.system.proxy.%1").arg(proxy.getProtocol()), 271 | "port", QString::number(proxy.getPort())}); 272 | p.waitForFinished(); 273 | } 274 | } 275 | 276 | void NetworkProxyHelper::resetSystemProxyLinuxGnome() { 277 | QProcess p; 278 | p.start("gsettings", 279 | QStringList{"set", "org.gnome.system.proxy", "mode", "none"}); 280 | p.waitForFinished(); 281 | p.start("gsettings", 282 | QStringList{"set", "org.gnome.system.proxy", "ignore-hosts", 283 | "['localhost', '127.0.0.0/8', '::1', '10.0.0.0/8', " 284 | "'172.16.0.0/12', '192.168.0.0/16']"}); 285 | p.waitForFinished(); 286 | p.start("gsettings", QStringList{"set", "org.gnome.system.proxy.http", 287 | "enabled", "false"}); 288 | p.waitForFinished(); 289 | p.start("gsettings", 290 | QStringList{"set", "org.gnome.system.proxy.http", "host", ""}); 291 | p.waitForFinished(); 292 | p.start("gsettings", 293 | QStringList{"set", "org.gnome.system.proxy.http", "port", "0"}); 294 | p.waitForFinished(); 295 | p.start("gsettings", 296 | QStringList{"set", "org.gnome.system.proxy.socks", "host", ""}); 297 | p.waitForFinished(); 298 | p.start("gsettings", 299 | QStringList{"set", "org.gnome.system.proxy.socks", "port", "0"}); 300 | p.waitForFinished(); 301 | } 302 | 303 | NetworkProxy NetworkProxyHelper::getSystemProxyLinuxKde() { 304 | QProcess p; 305 | NetworkProxy proxy; 306 | p.start("kreadconfig5", QStringList{"--file", "kioslaverc", "--group", 307 | "Proxy Settings", "--key", "ProxyType"}); 308 | p.waitForFinished(); 309 | int proxyMode = p.readAllStandardOutput().trimmed().toInt(); 310 | if (proxyMode == 1 || proxyMode == 4) { 311 | // Manual; Use System Settings 312 | proxy.setMode(NetworkProxyMode::GLOBAL_MODE); 313 | } else if (proxyMode == 2) { 314 | proxy.setMode(NetworkProxyMode::PAC_MODE); 315 | } 316 | if (proxy.getMode() == NetworkProxyMode::GLOBAL_MODE) { 317 | p.start("kreadconfig5", 318 | QStringList{"--file", "kioslaverc", "--group", "Proxy Settings", 319 | "--key", "httpProxy"}); 320 | p.waitForFinished(); 321 | QString httpProxy = p.readAllStandardOutput().trimmed(); 322 | if (httpProxy.size() > 0) { 323 | QStringList _proxy = httpProxy.split(' '); 324 | proxy.setProtocol("http"); 325 | proxy.setHost(_proxy.at(0)); 326 | proxy.setPort(_proxy.at(1).toInt()); 327 | } 328 | p.start("kreadconfig5", 329 | QStringList{"--file", "kioslaverc", "--group", "Proxy Settings", 330 | "--key", "socksProxy"}); 331 | p.waitForFinished(); 332 | QString socksProxy = p.readAllStandardOutput().trimmed(); 333 | if (socksProxy.size() > 0) { 334 | QStringList _proxy = socksProxy.split(' '); 335 | proxy.setProtocol("socks"); 336 | proxy.setHost(_proxy.at(0)); 337 | proxy.setPort(_proxy.at(1).toInt()); 338 | } 339 | } else if (proxy.getMode() == NetworkProxyMode::PAC_MODE) { 340 | p.start("kreadconfig5", 341 | QStringList{"--file", "kioslaverc", "--group", "Proxy Settings", 342 | "--key", "Proxy Config Script"}); 343 | p.waitForFinished(); 344 | proxy.setHost(p.readAllStandardOutput()); 345 | } 346 | return proxy; 347 | } 348 | 349 | void NetworkProxyHelper::setSystemProxyLinuxKde(const NetworkProxy& proxy) { 350 | QProcess p; 351 | if (proxy.getMode() == NetworkProxyMode::GLOBAL_MODE) { 352 | p.start("kwriteconfig5", 353 | QStringList{"--file", "kioslaverc", "--group", "Proxy Settings", 354 | "--key", "ProxyType", "1"}); 355 | p.waitForFinished(); 356 | p.start("kwriteconfig5", 357 | QStringList{"--file", "kioslaverc", "--group", "Proxy Settings", 358 | "--key", QString("%1Proxy").arg(proxy.getProtocol()), 359 | QString("%1 %2").arg( 360 | proxy.getHost(), QString::number(proxy.getPort()))}); 361 | p.waitForFinished(); 362 | } 363 | } 364 | 365 | void NetworkProxyHelper::resetSystemProxyLinuxKde() { 366 | QProcess p; 367 | p.start("kwriteconfig5", 368 | QStringList{"--file", "kioslaverc", "--group", "Proxy Settings", 369 | "--key", "ProxyType", "0"}); 370 | p.waitForFinished(); 371 | p.start("kwriteconfig5", 372 | QStringList{"--file", "kioslaverc", "--group", "Proxy Settings", 373 | "--key", "Proxy Config Script", ""}); 374 | p.waitForFinished(); 375 | p.start("kwriteconfig5", 376 | QStringList{"--file", "kioslaverc", "--group", "Proxy Settings", 377 | "--key", "httpProxy", ""}); 378 | p.waitForFinished(); 379 | p.start("kwriteconfig5", 380 | QStringList{"--file", "kioslaverc", "--group", "Proxy Settings", 381 | "--key", "socksProxy", ""}); 382 | p.waitForFinished(); 383 | } 384 | #endif 385 | -------------------------------------------------------------------------------- /src/networkproxy.h: -------------------------------------------------------------------------------- 1 | #ifndef NETWORKPROXY_H 2 | #define NETWORKPROXY_H 3 | 4 | #include 5 | 6 | enum class NetworkProxyMode { PAC_MODE, GLOBAL_MODE, DIRECT_MODE }; 7 | 8 | struct NetworkProxy { 9 | public: 10 | NetworkProxy() { this->mode = NetworkProxyMode::DIRECT_MODE; } 11 | 12 | NetworkProxy(QString protocol, 13 | QString host, 14 | int port, 15 | NetworkProxyMode mode) { 16 | this->protocol = protocol; 17 | this->host = host; 18 | this->port = port; 19 | this->mode = mode; 20 | } 21 | 22 | QString getProtocol() const { return this->protocol; } 23 | 24 | void setProtocol(QString protocol) { this->protocol = protocol; } 25 | 26 | QString getHost() const { return this->host; } 27 | 28 | void setHost(QString host) { this->host = host; } 29 | 30 | int getPort() const { return this->port; } 31 | 32 | void setPort(int port) { this->port = port; } 33 | 34 | NetworkProxyMode getMode() const { return this->mode; } 35 | 36 | void setMode(NetworkProxyMode mode) { this->mode = mode; } 37 | 38 | inline QString toString() { 39 | if (mode == NetworkProxyMode::DIRECT_MODE) { 40 | return "Disabled"; 41 | } else if (mode == NetworkProxyMode::PAC_MODE) { 42 | // The host contains the URL to the PAC file. 43 | // For example: http://127.0.0.1:1085/proxy.pac 44 | return host; 45 | } else { 46 | return QString("%1://%2:%3").arg(protocol, host, QString::number(port)); 47 | } 48 | } 49 | 50 | bool operator==(const NetworkProxy &other) { 51 | return this->mode == other.mode; 52 | if (mode == NetworkProxyMode::DIRECT_MODE) { 53 | return other.mode == NetworkProxyMode::DIRECT_MODE; 54 | } else if (mode == NetworkProxyMode::PAC_MODE) { 55 | return host == other.host; 56 | } else { 57 | return protocol == other.protocol && host == other.host && 58 | port == other.port; 59 | } 60 | } 61 | 62 | // bool operator!=(const NetworkProxy& other) { return !(*this == other); } 63 | private: 64 | NetworkProxyMode mode; 65 | QString protocol; 66 | QString host; 67 | int port; 68 | }; 69 | 70 | class NetworkProxyHelper : public QObject { 71 | Q_OBJECT 72 | public: 73 | static NetworkProxy getSystemProxy(); 74 | static void setSystemProxy(const NetworkProxy &proxy); 75 | static void resetSystemProxy(); 76 | 77 | private: 78 | #if defined(Q_OS_WIN) 79 | static NetworkProxy getSystemProxyWindows(); 80 | static void setSystemProxyWindows(const NetworkProxy &proxy); 81 | static void resetSystemProxyWindows(); 82 | #elif defined(Q_OS_MAC) 83 | static const QStringList NETWORK_INTERFACES; 84 | static NetworkProxy getSystemProxyMacOs(); 85 | static void setSystemProxyMacOs(const NetworkProxy &proxy); 86 | static void resetSystemProxyMacOs(); 87 | #elif defined(Q_OS_LINUX) 88 | static NetworkProxy getSystemProxyLinuxGnome(); 89 | static NetworkProxy getSystemProxyLinuxKde(); 90 | static void setSystemProxyLinuxGnome(const NetworkProxy &proxy); 91 | static void setSystemProxyLinuxKde(const NetworkProxy &proxy); 92 | static void resetSystemProxyLinuxGnome(); 93 | static void resetSystemProxyLinuxKde(); 94 | #endif 95 | }; 96 | 97 | #endif // NETWORKPROXY_H 98 | -------------------------------------------------------------------------------- /src/networkrequest.cpp: -------------------------------------------------------------------------------- 1 | #include "networkrequest.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "constants.h" 14 | 15 | NetworkRequest::NetworkRequest() {} 16 | 17 | QByteArray NetworkRequest::getNetworkResponse(QString url, 18 | const QNetworkProxy* proxy, 19 | int timeout) { 20 | QTimer timer; 21 | timer.setSingleShot(true); 22 | 23 | QNetworkAccessManager accessManager; 24 | if (proxy != nullptr) { 25 | accessManager.setProxy(*proxy); 26 | } 27 | QNetworkRequest request; 28 | 29 | request.setUrl(QUrl(url)); 30 | request.setSslConfiguration(QSslConfiguration::defaultConfiguration()); 31 | request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, 32 | QNetworkRequest::ManualRedirectPolicy); 33 | qInfo() << "Start to get url: " << url; 34 | QNetworkReply* networkReply = accessManager.get(request); 35 | QEventLoop eventLoop; 36 | connect(&timer, &QTimer::timeout, &eventLoop, &QEventLoop::quit); 37 | connect(networkReply, &QNetworkReply::finished, &eventLoop, 38 | &QEventLoop::quit); 39 | if (timeout > 0) { 40 | timer.start(timeout); 41 | } 42 | eventLoop.exec(); 43 | 44 | // Timeout handler 45 | if (timeout > 0 && !timer.isActive()) { 46 | disconnect(networkReply, &QNetworkReply::finished, &eventLoop, 47 | &QEventLoop::quit); 48 | networkReply->abort(); 49 | qWarning() << "Timed out when requesting " << url; 50 | } 51 | // Network error Handler 52 | if (networkReply->error() != QNetworkReply::NoError) { 53 | qCritical() << "Error occurred during requsting " << url 54 | << "; Error: " << networkReply->error(); 55 | return QByteArray(); 56 | } 57 | QByteArray responseBytes = networkReply->readAll(); 58 | networkReply->deleteLater(); 59 | networkReply->manager()->deleteLater(); 60 | return responseBytes; 61 | } 62 | 63 | int NetworkRequest::getLatency(QString host, int port) { 64 | QTcpSocket socket; 65 | QDateTime time = QDateTime::currentDateTime(); 66 | socket.connectToHost(host, port); 67 | socket.waitForConnected(TCP_PING_TIMEOUT); 68 | int timeEclipsed = time.msecsTo(QDateTime::currentDateTime()); 69 | return timeEclipsed >= TCP_PING_TIMEOUT ? -1 : timeEclipsed; 70 | } 71 | -------------------------------------------------------------------------------- /src/networkrequest.h: -------------------------------------------------------------------------------- 1 | #ifndef NETWORKREQUEST_H 2 | #define NETWORKREQUEST_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class NetworkRequest : public QObject { 9 | Q_OBJECT 10 | public: 11 | NetworkRequest(); 12 | static QByteArray getNetworkResponse(QString url, 13 | const QNetworkProxy* proxy = nullptr, 14 | int timeout = 0); 15 | static int getLatency(QString host, int port); 16 | }; 17 | 18 | #endif // NETWORKREQUEST_H 19 | -------------------------------------------------------------------------------- /src/qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | ui/main.qml 4 | images/v2ray.png 5 | images/v2ray.gray.png 6 | ui/dashboard.qml 7 | images/v2ray.ico 8 | images/v2ray.icns 9 | images/logo.png 10 | ui/about.qml 11 | ui/servers.qml 12 | ui/settings.qml 13 | ui/rules.qml 14 | images/icon-settings.svg 15 | images/icon-dashboard.svg 16 | images/icon-servers.svg 17 | images/icon-about.svg 18 | images/icon-rules.svg 19 | images/icon-logs.svg 20 | ui/logs.qml 21 | misc/tpl-linux-autostart.desktop 22 | misc/tpl-macos-autostart.plist 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/qrcodehelper.cpp: -------------------------------------------------------------------------------- 1 | #include "qrcodehelper.h" 2 | 3 | #include "3rdparty/qzxing/src/QZXing.h" 4 | 5 | QrCodeHelper::QrCodeHelper(QObject* parent) : QObject(parent) {} 6 | 7 | QString QrCodeHelper::decode(const QImage& img) { 8 | QZXing decoder; 9 | decoder.setDecoder(QZXing::DecoderFormat_QR_CODE | 10 | QZXing::DecoderFormat_EAN_13); 11 | return decoder.decodeImage(img); 12 | } 13 | -------------------------------------------------------------------------------- /src/qrcodehelper.h: -------------------------------------------------------------------------------- 1 | #ifndef QRCODEHELPER_H 2 | #define QRCODEHELPER_H 3 | 4 | #include 5 | #include 6 | 7 | class QrCodeHelper : public QObject { 8 | Q_OBJECT 9 | public: 10 | explicit QrCodeHelper(QObject* parent = nullptr); 11 | static QString decode(const QImage& img); 12 | 13 | signals: 14 | }; 15 | 16 | #endif // QRCODEHELPER_H 17 | -------------------------------------------------------------------------------- /src/runguard.cpp: -------------------------------------------------------------------------------- 1 | #include "runguard.h" 2 | 3 | #include 4 | 5 | QString RunGuard::hash(const QString &key, const QString &salt) { 6 | QByteArray hashValue; 7 | hashValue.append(key.toUtf8()); 8 | hashValue.append(salt.toUtf8()); 9 | 10 | return QCryptographicHash::hash(hashValue, QCryptographicHash::Sha1).toHex(); 11 | } 12 | 13 | RunGuard::RunGuard(const QString &key, QObject *parent) 14 | : QObject(parent), 15 | KEY(key), 16 | MEM_LOCK_KEY(hash(key, "MemLockKey")), 17 | SHARED_MEM_KEY(hash(key, "SharedMemoryKey")), 18 | sharedMemory(SHARED_MEM_KEY), 19 | memoryLock(MEM_LOCK_KEY, 1) { 20 | memoryLock.acquire(); 21 | { 22 | // Fix for *nix: http://habrahabr.ru/post/173281/ 23 | QSharedMemory fix(SHARED_MEM_KEY); 24 | fix.attach(); 25 | } 26 | memoryLock.release(); 27 | } 28 | 29 | RunGuard::~RunGuard() { release(); } 30 | 31 | bool RunGuard::isAnotherRunning() { 32 | if (sharedMemory.isAttached()) { 33 | return false; 34 | } 35 | memoryLock.acquire(); 36 | const bool isRunning = sharedMemory.attach(); 37 | if (isRunning) { 38 | sharedMemory.detach(); 39 | } 40 | memoryLock.release(); 41 | 42 | return isRunning; 43 | } 44 | 45 | bool RunGuard::tryToRun() { 46 | if (isAnotherRunning()) { 47 | return false; 48 | } 49 | 50 | memoryLock.acquire(); 51 | const bool result = sharedMemory.create(sizeof(quint64)); 52 | memoryLock.release(); 53 | if (!result) { 54 | release(); 55 | return false; 56 | } 57 | return true; 58 | } 59 | 60 | void RunGuard::release() { 61 | memoryLock.acquire(); 62 | if (sharedMemory.isAttached()) { 63 | sharedMemory.detach(); 64 | } 65 | memoryLock.release(); 66 | } 67 | -------------------------------------------------------------------------------- /src/runguard.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNGUARD_H 2 | #define RUNGUARD_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class RunGuard : public QObject { 9 | Q_OBJECT 10 | Q_DISABLE_COPY(RunGuard); 11 | 12 | public: 13 | explicit RunGuard(const QString& key, QObject* parent = nullptr); 14 | ~RunGuard(); 15 | bool tryToRun(); 16 | 17 | private: 18 | bool isAnotherRunning(); 19 | void release(); 20 | static QString hash(const QString& key, const QString& salt); 21 | 22 | const QString KEY; 23 | const QString MEM_LOCK_KEY; 24 | const QString SHARED_MEM_KEY; 25 | 26 | QSharedMemory sharedMemory; 27 | QSystemSemaphore memoryLock; 28 | }; 29 | 30 | #endif // RUNGUARD_H 31 | -------------------------------------------------------------------------------- /src/serverconfighelper.cpp: -------------------------------------------------------------------------------- 1 | #include "serverconfighelper.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "constants.h" 15 | #include "utility.h" 16 | 17 | QString ServerConfigHelper::getServerNameError(const QJsonObject& serverConfig, 18 | const QString* pServerName) { 19 | if (pServerName != nullptr) { 20 | QString newServerName = serverConfig["serverName"].toString(); 21 | if (newServerName == *pServerName) { 22 | return Utility::getStringConfigError(serverConfig, "serverName", 23 | tr("Server Name")); 24 | } 25 | } 26 | return Utility::getStringConfigError( 27 | serverConfig, "serverName", tr("Server Name"), 28 | {std::bind(&Utility::isServerNameNotUsed, std::placeholders::_1)}, false, 29 | tr("The '%1' has been used by another server.")); 30 | } 31 | 32 | ServerConfigHelper::Protocol ServerConfigHelper::getProtocol(QString protocol) { 33 | protocol = protocol.toLower(); 34 | 35 | if (protocol == "vmess" || protocol == "v2ray") { 36 | return Protocol::VMESS; 37 | } else if (protocol == "shadowsocks" || protocol == "ss" || 38 | protocol == "shadowsocksr" || protocol == "ssr") { 39 | return Protocol::SHADOWSOCKS; 40 | } else if (protocol == "trojan") { 41 | return Protocol::TROJAN; 42 | } else { 43 | return Protocol::UNKNOWN; 44 | } 45 | } 46 | 47 | QStringList ServerConfigHelper::getServerConfigErrors( 48 | Protocol protocol, 49 | const QJsonObject& serverConfig, 50 | const QString* pServerName) { 51 | if (protocol == Protocol::VMESS) { 52 | return getV2RayServerConfigErrors(serverConfig, pServerName); 53 | } else if (protocol == Protocol::SHADOWSOCKS) { 54 | return getShadowsocksServerConfigErrors(serverConfig, pServerName); 55 | } else if (protocol == Protocol::TROJAN) { 56 | return getTrojanServerConfigErrors(serverConfig, pServerName); 57 | } 58 | return QStringList{tr("Unknown Server protocol")}; 59 | } 60 | 61 | QJsonObject ServerConfigHelper::getPrettyServerConfig( 62 | Protocol protocol, const QJsonObject& serverConfig) { 63 | if (protocol == Protocol::VMESS) { 64 | return getPrettyV2RayConfig(serverConfig); 65 | } else if (protocol == Protocol::SHADOWSOCKS) { 66 | return getPrettyShadowsocksConfig(serverConfig); 67 | } else if (protocol == Protocol::TROJAN) { 68 | return getPrettyTrojanConfig(serverConfig); 69 | } 70 | return QJsonObject{}; 71 | } 72 | 73 | QJsonObject ServerConfigHelper::getServerConfigFromUrl( 74 | Protocol protocol, const QString& serverUrl, const QString& subscriptionUrl) { 75 | QString _serverUrl = serverUrl.trimmed(); 76 | if (protocol == Protocol::VMESS) { 77 | return getV2RayServerConfigFromUrl(_serverUrl, subscriptionUrl); 78 | } else if (protocol == Protocol::SHADOWSOCKS) { 79 | if (serverUrl.startsWith("ssr://")) { 80 | return getShadowsocksRServerConfigFromUrl(_serverUrl, subscriptionUrl); 81 | } else { 82 | return getShadowsocksServerConfigFromUrl(_serverUrl, subscriptionUrl); 83 | } 84 | } else if (protocol == Protocol::TROJAN) { 85 | return getTrojanServerConfigFromUrl(_serverUrl, subscriptionUrl); 86 | } 87 | return QJsonObject{}; 88 | } 89 | 90 | QStringList ServerConfigHelper::getV2RayServerConfigErrors( 91 | const QJsonObject& serverConfig, const QString* serverName) { 92 | QStringList errors; 93 | errors.append(getServerNameError(serverConfig, serverName)); 94 | errors.append(Utility::getStringConfigError( 95 | serverConfig, "serverAddr", tr("Server Address"), 96 | { 97 | std::bind(&Utility::isIpAddrValid, std::placeholders::_1), 98 | std::bind(&Utility::isDomainNameValid, std::placeholders::_1), 99 | })); 100 | errors.append(Utility::getNumericConfigError(serverConfig, "serverPort", 101 | tr("Server Port"), 0, 65535)); 102 | errors.append(Utility::getStringConfigError(serverConfig, "id", tr("ID"))); 103 | errors.append(Utility::getNumericConfigError(serverConfig, "alterId", 104 | tr("Alter ID"), 0, 65535)); 105 | errors.append( 106 | Utility::getStringConfigError(serverConfig, "security", tr("Security"))); 107 | errors.append( 108 | Utility::getStringConfigError(serverConfig, "network", tr("Network"))); 109 | errors.append(Utility::getStringConfigError(serverConfig, "networkSecurity", 110 | tr("Network Security"))); 111 | errors.append(Utility::getStringConfigError(serverConfig, "tcpHeaderType", 112 | tr("TCP Header"))); 113 | errors.append(getV2RayStreamSettingsErrors( 114 | serverConfig, serverConfig["network"].toString())); 115 | 116 | // Remove empty error messages generated by Utility::getNumericConfigError() 117 | // and Utility::getStringConfigError() 118 | errors.removeAll(""); 119 | return errors; 120 | } 121 | 122 | QStringList ServerConfigHelper::getV2RayStreamSettingsErrors( 123 | const QJsonObject& serverConfig, const QString& network) { 124 | QStringList errors; 125 | if (network != "tcp" && network != "ws") { 126 | // Clash only supports tcp and ws :( 127 | errors.append(QString(tr("Unspoorted 'Network': %1.")).arg(network)); 128 | } 129 | if (network == "ws") { 130 | errors.append(Utility::getStringConfigError( 131 | serverConfig, "networkHost", tr("Host"), 132 | {std::bind(&Utility::isDomainNameValid, std::placeholders::_1)}, true)); 133 | errors.append(Utility::getStringConfigError(serverConfig, "networkPath", 134 | tr("Path"), {}, true)); 135 | } 136 | return errors; 137 | } 138 | 139 | QJsonObject ServerConfigHelper::getPrettyV2RayConfig( 140 | const QJsonObject& serverConfig) { 141 | QJsonObject v2RayConfig{ 142 | {"autoConnect", serverConfig["autoConnect"].toBool()}, 143 | {"subscription", serverConfig.contains("subscription") 144 | ? serverConfig["subscription"].toString() 145 | : ""}, 146 | {"name", serverConfig["serverName"].toString()}, 147 | {"type", "vmess"}, 148 | {"udp", serverConfig["udp"].toBool()}, 149 | {"server", serverConfig["serverAddr"].toString()}, 150 | {"port", serverConfig["serverPort"].toVariant().toInt()}, 151 | {"uuid", serverConfig["id"].toString()}, 152 | {"alterId", serverConfig["alterId"].toVariant().toInt()}, 153 | {"cipher", serverConfig["security"].toString().toLower()}, 154 | {"tls", serverConfig["networkSecurity"].toString().toLower() == "tls"}, 155 | {"skip-cert-verify", serverConfig["allowInsecure"].toBool()}}; 156 | 157 | QString network = serverConfig["network"].toString(); 158 | QString tcpHeader = serverConfig["tcpHeaderType"].toString(); 159 | if (network == "ws") { 160 | v2RayConfig["network"] = "ws"; 161 | v2RayConfig["ws-path"] = serverConfig["networkPath"].toString(); 162 | v2RayConfig["ws-headers"] = 163 | QJsonObject{{"Host", serverConfig["networkHost"].toString()}}; 164 | } else if (network == "tcp" && tcpHeader == "none") { 165 | v2RayConfig["network"] = "tcp"; 166 | } else if (network == "tcp" && tcpHeader == "http") { 167 | v2RayConfig["network"] = "http"; 168 | v2RayConfig["http-opts"] = QJsonObject{ 169 | {"method", "GET"}, 170 | {"headers", 171 | QJsonObject{ 172 | {"host", 173 | QJsonArray{"www.baidu.com", "www.bing.com", "www.163.com", 174 | "www.netease.com", "www.qq.com", "www.tencent.com", 175 | "www.taobao.com", "www.tmall.com", "www.alibaba-inc.com", 176 | "www.aliyun.com", "www.sensetime.com", "www.megvii.com"}}, 177 | {"User-Agent", getRandomUserAgents(24)}, 178 | {"Accept-Encoding", QJsonArray{"gzip, deflate"}}, 179 | {"Connection", QJsonArray{"keep-alive"}}}}, 180 | }; 181 | } 182 | return v2RayConfig; 183 | } 184 | 185 | QJsonArray ServerConfigHelper::getRandomUserAgents(int n) { 186 | QStringList OPERATING_SYSTEMS{"Macintosh; Intel Mac OS X 10_15", 187 | "X11; Linux x86_64", 188 | "Windows NT 10.0; Win64; x64"}; 189 | QJsonArray userAgents; 190 | for (int i = 0; i < n; ++i) { 191 | int osIndex = std::rand() % 3; 192 | int chromeMajorVersion = std::rand() % 30 + 50; 193 | int chromeBuildVersion = std::rand() % 4000 + 1000; 194 | int chromePatchVersion = std::rand() % 100; 195 | userAgents.append(QString("Mozilla/5.0 (%1) AppleWebKit/537.36 (KHTML, " 196 | "like Gecko) Chrome/%2.0.%3.%4 Safari/537.36") 197 | .arg(OPERATING_SYSTEMS[osIndex], 198 | QString::number(chromeMajorVersion), 199 | QString::number(chromeBuildVersion), 200 | QString::number(chromePatchVersion))); 201 | } 202 | return userAgents; 203 | } 204 | 205 | QJsonObject ServerConfigHelper::getV2RayServerConfigFromUrl( 206 | const QString& server, const QString& subscriptionUrl) { 207 | // Ref: 208 | // https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) 209 | const QMap NETWORK_MAPPER = { 210 | {"tcp", "tcp"}, {"kcp", "kcp"}, {"ws", "ws"}, 211 | {"h2", "http"}, {"quic", "quic"}, 212 | }; 213 | QJsonObject rawServerConfig = 214 | QJsonDocument::fromJson( 215 | QByteArray::fromBase64(server.mid(8).toUtf8(), 216 | QByteArray::AbortOnBase64DecodingErrors)) 217 | .object(); 218 | QString network = 219 | rawServerConfig.contains("net") ? rawServerConfig["net"].toString() : "tcp"; 220 | QString serverAddr = 221 | rawServerConfig.contains("add") ? rawServerConfig["add"].toString() : ""; 222 | QString serverPort = rawServerConfig["port"].isString() 223 | ? rawServerConfig["port"].toString() 224 | : QString::number(rawServerConfig["port"].toInt()); 225 | int alterId = rawServerConfig.contains("aid") 226 | ? (rawServerConfig["aid"].isString() 227 | ? rawServerConfig["aid"].toString().toInt() 228 | : rawServerConfig["aid"].toInt()) 229 | : 0; 230 | 231 | QJsonObject serverConfig{ 232 | {"autoConnect", false}, 233 | {"serverName", rawServerConfig.contains("ps") 234 | ? rawServerConfig["ps"].toString().trimmed() 235 | : serverAddr}, 236 | {"serverAddr", serverAddr}, 237 | {"serverPort", rawServerConfig.contains("port") ? serverPort : ""}, 238 | {"subscription", subscriptionUrl}, 239 | {"id", 240 | rawServerConfig.contains("id") ? rawServerConfig["id"].toString() : ""}, 241 | {"alterId", alterId}, 242 | {"udp", false}, 243 | {"security", "auto"}, 244 | {"network", 245 | NETWORK_MAPPER.contains(network) ? NETWORK_MAPPER[network] : "tcp"}, 246 | {"networkHost", rawServerConfig.contains("host") 247 | ? rawServerConfig["host"].toString() 248 | : ""}, 249 | {"networkPath", rawServerConfig.contains("path") 250 | ? rawServerConfig["path"].toString() 251 | : ""}, 252 | {"tcpHeaderType", rawServerConfig.contains("type") 253 | ? rawServerConfig["type"].toString() 254 | : ""}, 255 | {"networkSecurity", rawServerConfig.contains("tls") && 256 | !rawServerConfig["tls"].toString().isEmpty() 257 | ? "tls" 258 | : "none"}}; 259 | 260 | return serverConfig; 261 | } 262 | 263 | bool ServerConfigHelper::isShadowsocksR(const QJsonObject& serverConfig) { 264 | if (!serverConfig.contains("plugins")) { 265 | return false; 266 | } 267 | QJsonObject plugins = serverConfig["plugins"].toObject(); 268 | return plugins.contains("protocol"); 269 | } 270 | 271 | QStringList ServerConfigHelper::getShadowsocksServerConfigErrors( 272 | const QJsonObject& serverConfig, const QString* pServerName) { 273 | QStringList errors; 274 | errors.append(getServerNameError(serverConfig, pServerName)); 275 | errors.append(Utility::getStringConfigError( 276 | serverConfig, "serverAddr", tr("Server Address"), 277 | { 278 | std::bind(&Utility::isIpAddrValid, std::placeholders::_1), 279 | std::bind(&Utility::isDomainNameValid, std::placeholders::_1), 280 | })); 281 | errors.append(Utility::getNumericConfigError(serverConfig, "serverPort", 282 | tr("Server Port"), 0, 65535)); 283 | errors.append( 284 | Utility::getStringConfigError(serverConfig, "encryption", tr("Security"))); 285 | errors.append( 286 | Utility::getStringConfigError(serverConfig, "password", tr("Password"))); 287 | 288 | // Remove empty error messages generated by getNumericConfigError() and 289 | // getStringConfigError() 290 | errors.removeAll(""); 291 | return errors; 292 | } 293 | 294 | QJsonObject ServerConfigHelper::getPrettyShadowsocksConfig( 295 | const QJsonObject& serverConfig) { 296 | bool isSSR = isShadowsocksR(serverConfig); 297 | QJsonObject prettyServerCfg = { 298 | {"autoConnect", serverConfig["autoConnect"].toBool()}, 299 | {"subscription", serverConfig.contains("subscription") 300 | ? serverConfig["subscription"].toString() 301 | : ""}, 302 | {"name", serverConfig["serverName"].toString()}, 303 | {"type", isSSR ? "ssr" : "ss"}, 304 | {"server", serverConfig["serverAddr"].toString()}, 305 | {"port", serverConfig["serverPort"].toVariant().toInt()}, 306 | {"cipher", serverConfig["encryption"].toString().toLower()}, 307 | {"password", serverConfig["password"].toString()}}; 308 | 309 | // Parse Shadowsocks Plugins 310 | if (!isSSR && serverConfig.contains("plugins")) { 311 | QJsonObject plugins = serverConfig["plugins"].toObject(); 312 | 313 | if (plugins.contains("obfs") && !plugins["obfs"].toString().isEmpty()) { 314 | prettyServerCfg["plugin"] = "obfs"; 315 | QJsonObject pluginOpts; 316 | pluginOpts["mode"] = plugins["obfs"].toString(); 317 | if (plugins.contains("obfs-host") && 318 | plugins["obfs-host"].toString().size()) { 319 | pluginOpts["host"] = plugins["obfs-host"].toString(); 320 | } 321 | prettyServerCfg["plugin-opts"] = pluginOpts; 322 | } 323 | } 324 | // Parse ShadowsocksR (SSR) 325 | if (isSSR) { 326 | QJsonObject plugins = serverConfig["plugins"].toObject(); 327 | 328 | prettyServerCfg["obfs"] = plugins["obfs"].toString().toLower(); 329 | prettyServerCfg["protocol"] = plugins["protocol"].toString().toLower(); 330 | if (plugins.contains("obfsparam") && 331 | plugins["obfsparam"].toString().size()) { 332 | prettyServerCfg["obfs-param"] = plugins["obfsparam"].toString(); 333 | } 334 | if (plugins.contains("protoparam") && 335 | plugins["protoparam"].toString().size()) { 336 | prettyServerCfg["protocol-param"] = 337 | plugins["protoparam"].toString().toLower(); 338 | } 339 | if (plugins.contains("udp")) { 340 | prettyServerCfg["udp"] = plugins["udp"].toBool(); 341 | } 342 | } 343 | return prettyServerCfg; 344 | } 345 | 346 | QJsonObject ServerConfigHelper::getShadowsocksServerConfigFromUrl( 347 | QString serverUrl, const QString& subscriptionUrl) { 348 | serverUrl = serverUrl.mid(5); 349 | int atIndex = serverUrl.indexOf('@'); 350 | int colonIndex = serverUrl.indexOf(':'); 351 | int splashIndex = serverUrl.indexOf('/'); 352 | int sharpIndex = serverUrl.indexOf('#'); 353 | int questionMarkIndex = serverUrl.indexOf('?'); 354 | 355 | QString confidential = QByteArray::fromBase64( 356 | serverUrl.left(atIndex).toUtf8(), QByteArray::AbortOnBase64DecodingErrors); 357 | QString serverAddr = serverUrl.mid(atIndex + 1, colonIndex - atIndex - 1); 358 | QString serverPort = serverUrl.mid( 359 | colonIndex + 1, splashIndex != -1 ? (splashIndex - colonIndex - 1) 360 | : (sharpIndex - colonIndex - 1)); 361 | QString plugins = 362 | serverUrl.mid(questionMarkIndex + 1, sharpIndex - questionMarkIndex - 1); 363 | QString serverName = 364 | QUrl::fromPercentEncoding(serverUrl.mid(sharpIndex + 1).toUtf8()).trimmed(); 365 | 366 | colonIndex = confidential.indexOf(':'); 367 | QString encryption = confidential.left(colonIndex); 368 | QString password = confidential.mid(colonIndex + 1); 369 | 370 | QJsonObject serverConfig{{"serverName", serverName}, 371 | {"autoConnect", false}, 372 | {"subscription", subscriptionUrl}, 373 | {"serverAddr", serverAddr}, 374 | {"serverPort", serverPort}, 375 | {"encryption", encryption}, 376 | {"password", password}}; 377 | 378 | QJsonObject pluginOptions = getShadowsocksPlugins(plugins); 379 | if (!pluginOptions.empty()) { 380 | serverConfig["plugins"] = pluginOptions; 381 | } 382 | return serverConfig; 383 | } 384 | 385 | QJsonObject ServerConfigHelper::getShadowsocksPlugins( 386 | const QString& pluginString) { 387 | QJsonObject plugins; 388 | for (QPair p : QUrlQuery(pluginString).queryItems()) { 389 | if (p.first == "plugin") { 390 | QStringList options = 391 | QUrl::fromPercentEncoding(p.second.toUtf8()).split(';'); 392 | for (QString o : options) { 393 | QStringList t = o.split('='); 394 | if (t.size() == 2) { 395 | plugins[t[0]] = t[1]; 396 | } 397 | } 398 | } 399 | } 400 | return plugins; 401 | } 402 | 403 | QJsonObject ServerConfigHelper::getShadowsocksRServerConfigFromUrl( 404 | QString server, const QString& subscriptionUrl) { 405 | server = server.mid(6); 406 | QString serverUrl = QByteArray::fromBase64( 407 | server.toUtf8(), QByteArray::AbortOnBase64DecodingErrors); 408 | // Failed to parse the SSR URL 409 | if (!serverUrl.size()) { 410 | serverUrl = QByteArray::fromBase64(server.replace('_', '/').toUtf8(), 411 | QByteArray::AbortOnBase64DecodingErrors); 412 | } 413 | int sepIndex = serverUrl.indexOf("/?"); 414 | QStringList essentialServerCfg = serverUrl.left(sepIndex).split(':'); 415 | if (essentialServerCfg.size() != 6) { 416 | return QJsonObject{}; 417 | } 418 | 419 | QString serverAddr = essentialServerCfg.at(0); 420 | int serverPort = essentialServerCfg.at(1).toInt(); 421 | QString serverName = 422 | QString("%1:%2").arg(serverAddr, QString::number(serverPort)); 423 | QJsonObject serverConfig{ 424 | {"serverName", serverName}, 425 | {"autoConnect", false}, 426 | {"subscription", subscriptionUrl}, 427 | {"serverAddr", serverAddr}, 428 | {"serverPort", serverPort}, 429 | {"encryption", essentialServerCfg.at(3)}, 430 | {"password", essentialServerCfg.at(5)}, 431 | {"plugins", QJsonObject{{"obfs", essentialServerCfg.at(4)}, 432 | {"protocol", essentialServerCfg.at(2)}}}}; 433 | 434 | QString optionalServerCfg = serverUrl.mid(sepIndex + 2); 435 | for (QPair p : QUrlQuery(optionalServerCfg).queryItems()) { 436 | serverConfig[p.first] = p.second; 437 | } 438 | return serverConfig; 439 | } 440 | 441 | QStringList ServerConfigHelper::getTrojanServerConfigErrors( 442 | const QJsonObject& serverConfig, const QString* pServerName) { 443 | QStringList errors; 444 | errors.append(getServerNameError(serverConfig, pServerName)); 445 | errors.append(Utility::getStringConfigError( 446 | serverConfig, "serverAddr", tr("Server Address"), 447 | { 448 | std::bind(&Utility::isIpAddrValid, std::placeholders::_1), 449 | std::bind(&Utility::isDomainNameValid, std::placeholders::_1), 450 | })); 451 | errors.append(Utility::getNumericConfigError(serverConfig, "serverPort", 452 | tr("Server Port"), 0, 65535)); 453 | errors.append( 454 | Utility::getStringConfigError(serverConfig, "password", tr("Password"))); 455 | errors.append(Utility::getStringConfigError( 456 | serverConfig, "sni", tr("SNI"), 457 | { 458 | std::bind(&Utility::isIpAddrValid, std::placeholders::_1), 459 | std::bind(&Utility::isDomainNameValid, std::placeholders::_1), 460 | }, 461 | true)); 462 | errors.append(Utility::getStringConfigError( 463 | serverConfig, "alpn", tr("ALPN"), 464 | { 465 | std::bind(&Utility::isAlpnValid, std::placeholders::_1), 466 | })); 467 | 468 | // Remove empty error messages generated by getNumericConfigError() and 469 | // getStringConfigError() 470 | errors.removeAll(""); 471 | return errors; 472 | } 473 | 474 | QJsonObject ServerConfigHelper::getPrettyTrojanConfig( 475 | const QJsonObject& serverConfig) { 476 | QJsonArray alpn; 477 | for (QString a : Utility::getAlpn(serverConfig["alpn"].toString())) { 478 | alpn.append(a); 479 | } 480 | 481 | QJsonObject prettyServerCfg = { 482 | {"autoConnect", serverConfig["autoConnect"].toBool()}, 483 | {"subscription", serverConfig.contains("subscription") 484 | ? serverConfig["subscription"].toString() 485 | : ""}, 486 | {"name", serverConfig["serverName"].toString()}, 487 | {"type", "trojan"}, 488 | {"server", serverConfig["serverAddr"].toString()}, 489 | {"port", serverConfig["serverPort"].toVariant().toInt()}, 490 | {"password", serverConfig["password"].toString()}, 491 | {"sni", serverConfig["sni"].toString()}, 492 | {"udp", serverConfig["udp"].toBool()}, 493 | {"alpn", alpn}, 494 | {"skip-cert-verify", serverConfig["allowInsecure"].toBool()}}; 495 | 496 | return prettyServerCfg; 497 | } 498 | 499 | QJsonObject ServerConfigHelper::getTrojanServerConfigFromUrl( 500 | QString serverUrl, const QString& subscriptionUrl) { 501 | serverUrl = serverUrl.mid(9); 502 | int atIndex = serverUrl.indexOf('@'); 503 | int colonIndex = serverUrl.indexOf(':'); 504 | int sharpIndex = serverUrl.indexOf('#'); 505 | int questionMarkIndex = 506 | serverUrl.indexOf('?') == -1 ? sharpIndex : serverUrl.indexOf('?'); 507 | 508 | QString password = serverUrl.left(atIndex); 509 | QString serverAddr = serverUrl.mid(atIndex + 1, colonIndex - atIndex - 1); 510 | QString serverPort = 511 | serverUrl.mid(colonIndex + 1, questionMarkIndex - colonIndex - 1); 512 | QString options = 513 | serverUrl.mid(questionMarkIndex + 1, sharpIndex - questionMarkIndex - 1); 514 | QString serverName = 515 | QUrl::fromPercentEncoding(serverUrl.mid(sharpIndex + 1).toUtf8()).trimmed(); 516 | 517 | QJsonObject serverConfig{ 518 | {"serverName", serverName}, {"autoConnect", false}, 519 | {"subscription", subscriptionUrl}, {"serverAddr", serverAddr}, 520 | {"serverPort", serverPort}, {"password", password}}; 521 | 522 | QJsonObject serverOptions = getTrojanOptions(options); 523 | for (auto itr = serverOptions.begin(); itr != serverOptions.end(); ++itr) { 524 | serverConfig[itr.key()] = itr.value(); 525 | } 526 | return serverConfig; 527 | } 528 | 529 | QJsonObject ServerConfigHelper::getTrojanOptions(const QString& optionString) { 530 | QJsonObject options{{"sni", DEFAULT_TROJRAN_SNI}, 531 | {"udp", DEFAULT_TROJRAN_ENABLE_UDP}, 532 | {"alpn", DEFAULT_TROJRAN_ALPN}, 533 | {"allowInsecure", DEFAULT_TROJRAN_ALLOW_INSECURE}}; 534 | 535 | for (QPair p : QUrlQuery(optionString).queryItems()) { 536 | if (options.contains(p.first)) { 537 | options[p.first] = p.second; 538 | } 539 | } 540 | return options; 541 | } 542 | 543 | QList ServerConfigHelper::getServerConfigFromV2RayConfig( 544 | const QJsonObject& config) { 545 | QList servers; 546 | QJsonArray serversConfig = config["outbounds"].toArray(); 547 | for (auto itr = serversConfig.begin(); itr != serversConfig.end(); ++itr) { 548 | QJsonObject server = (*itr).toObject(); 549 | QString protocol = server["protocol"].toString(); 550 | if (protocol != "vmess") { 551 | qWarning() << QString("Ignore the server protocol: %1").arg(protocol); 552 | continue; 553 | } 554 | QJsonObject serverSettings = 555 | getV2RayServerSettingsFromConfig(server["settings"].toObject()); 556 | QJsonObject streamSettings = getV2RayStreamSettingsFromConfig( 557 | server["streamSettings"].toObject(), config["transport"].toObject()); 558 | if (!serverSettings.empty()) { 559 | QJsonObject serverConfig = serverSettings; 560 | // Stream Settings 561 | for (auto itr = streamSettings.begin(); itr != streamSettings.end(); 562 | ++itr) { 563 | serverConfig.insert(itr.key(), itr.value()); 564 | } 565 | // MUX Settings 566 | if (server.contains("mux")) { 567 | QJsonObject muxObject = server["mux"].toObject(); 568 | int mux = muxObject["concurrency"].toVariant().toInt(); 569 | if (mux > 0) { 570 | serverConfig.insert("mux", QString::number(mux)); 571 | } 572 | } 573 | if (!serverConfig.contains("mux")) { 574 | // Default value for MUX 575 | serverConfig["mux"] = -1; 576 | } 577 | servers.append(serverConfig); 578 | } 579 | } 580 | return servers; 581 | } 582 | 583 | QJsonObject ServerConfigHelper::getV2RayServerSettingsFromConfig( 584 | const QJsonObject& settings) { 585 | QJsonObject server; 586 | QJsonArray vnext = settings["vnext"].toArray(); 587 | if (vnext.size()) { 588 | QJsonObject _server = vnext.at(0).toObject(); 589 | server["serverAddr"] = _server["address"].toString(); 590 | server["serverPort"] = _server["port"].toVariant().toInt(); 591 | server["serverName"] = 592 | QString("%1:%2").arg(server["serverAddr"].toString(), 593 | QString::number(server["serverPort"].toInt())); 594 | QJsonArray users = _server["users"].toArray(); 595 | if (users.size()) { 596 | QJsonObject user = users.at(0).toObject(); 597 | server["id"] = user["id"].toString(); 598 | server["alterId"] = user["alterId"].toVariant().toInt(); 599 | server["security"] = 600 | user.contains("security") ? user["security"].toString() : "auto"; 601 | } 602 | } 603 | return server; 604 | } 605 | 606 | QJsonObject ServerConfigHelper::getV2RayStreamSettingsFromConfig( 607 | const QJsonObject& transport, const QJsonObject& streamSettings) { 608 | QJsonObject _streamSettings = 609 | streamSettings.empty() ? transport : streamSettings; 610 | QJsonObject serverStreamSettings; 611 | QString network = _streamSettings.contains("network") 612 | ? _streamSettings["network"].toString() 613 | : "tcp"; 614 | serverStreamSettings["network"] = network; 615 | serverStreamSettings["networkSecurity"] = 616 | _streamSettings.contains("security") 617 | ? _streamSettings["security"].toString() 618 | : "none"; 619 | serverStreamSettings["allowInsecure"] = true; 620 | if (_streamSettings.contains("tlsSettings")) { 621 | QJsonObject tlsSettings = _streamSettings["tlsSettings"].toObject(); 622 | serverStreamSettings["allowInsecure"] = tlsSettings["allowInsecure"]; 623 | } 624 | if (network == "tcp") { 625 | QJsonObject tcpSettings = _streamSettings["tcpSettings"].toObject(); 626 | QJsonObject header = tcpSettings["header"].toObject(); 627 | serverStreamSettings["tcpHeaderType"] = 628 | header.contains("type") ? header["type"].toString() : "none"; 629 | } else if (network == "kcp") { 630 | QJsonObject kcpSettings = _streamSettings["kcpSettings"].toObject(); 631 | QJsonObject header = kcpSettings["header"].toObject(); 632 | serverStreamSettings["kcpMtu"] = kcpSettings.contains("mtu") 633 | ? kcpSettings["mtu"].toVariant().toInt() 634 | : DEFAULT_V2RAY_KCP_MTU; 635 | serverStreamSettings["kcpTti"] = kcpSettings.contains("tti") 636 | ? kcpSettings["tti"].toVariant().toInt() 637 | : DEFAULT_V2RAY_KCP_TTI; 638 | serverStreamSettings["kcpUpLink"] = 639 | kcpSettings.contains("uplinkCapacity") 640 | ? kcpSettings["uplinkCapacity"].toVariant().toInt() 641 | : DEFAULT_V2RAY_KCP_UP_CAPACITY; 642 | serverStreamSettings["kcpDownLink"] = 643 | kcpSettings.contains("downlinkCapacity") 644 | ? kcpSettings["downlinkCapacity"].toVariant().toInt() 645 | : DEFAULT_V2RAY_KCP_DOWN_CAPACITY; 646 | serverStreamSettings["kcpReadBuffer"] = 647 | kcpSettings.contains("readBufferSize") 648 | ? kcpSettings["readBufferSize"].toVariant().toInt() 649 | : DEFAULT_V2RAY_KCP_READ_BUF_SIZE; 650 | serverStreamSettings["kcpWriteBuffer"] = 651 | kcpSettings.contains("writeBufferSize") 652 | ? kcpSettings["writeBufferSize"].toVariant().toInt() 653 | : DEFAULT_V2RAY_KCP_READ_BUF_SIZE; 654 | serverStreamSettings["kcpCongestion"] = kcpSettings["congestion"].toBool(); 655 | serverStreamSettings["packetHeader"] = 656 | header.contains("type") ? header["type"].toString() : "none"; 657 | } else if (network == "ws") { 658 | QJsonObject wsSettings = _streamSettings["wsSettings"].toObject(); 659 | QJsonObject headers = wsSettings["headers"].toObject(); 660 | serverStreamSettings["networkHost"] = headers.contains("host") 661 | ? headers["host"].toString() 662 | : headers["Host"].toString(); 663 | serverStreamSettings["networkPath"] = wsSettings["path"]; 664 | } else if (network == "http") { 665 | QJsonObject httpSettings = _streamSettings["httpSettings"].toObject(); 666 | serverStreamSettings["networkHost"] = httpSettings["host"].toArray().at(0); 667 | serverStreamSettings["networkPath"] = httpSettings["path"].toString(); 668 | } else if (network == "domainsocket") { 669 | QJsonObject dsSettings = _streamSettings["dsSettings"].toObject(); 670 | serverStreamSettings["domainSocketFilePath"] = 671 | dsSettings["path"].toString(); 672 | } else if (network == "quic") { 673 | QJsonObject quicSettings = _streamSettings["quicSettings"].toObject(); 674 | QJsonObject header = quicSettings["header"].toObject(); 675 | serverStreamSettings["quicSecurity"] = 676 | quicSettings.contains("security") ? quicSettings["security"].toString() 677 | : "none"; 678 | serverStreamSettings["packetHeader"] = 679 | header.contains("type") ? header["type"].toString() : "none"; 680 | serverStreamSettings["quicKey"] = quicSettings["key"].toString(); 681 | } 682 | return serverStreamSettings; 683 | } 684 | 685 | QList ServerConfigHelper::getServerConfigFromShadowsocksQt5Config( 686 | const QJsonObject& config) { 687 | QList servers; 688 | QJsonArray serversConfig = config["configs"].toArray(); 689 | 690 | for (auto itr = serversConfig.begin(); itr != serversConfig.end(); ++itr) { 691 | QJsonObject server = (*itr).toObject(); 692 | QJsonObject serverConfig = { 693 | {"serverName", server["remarks"].toString().trimmed()}, 694 | {"serverAddr", server["server"].toString()}, 695 | {"serverPort", QString::number(server["server_port"].toInt())}, 696 | {"encryption", server["method"].toString()}, 697 | {"password", server["password"].toString()}, 698 | }; 699 | if (server.contains("plugin_opts") && 700 | !server["plugin_opts"].toString().isEmpty()) { 701 | QString plugins = 702 | QString("plugin=%1%3B%2") 703 | .arg( 704 | server["plugin"].toString(), 705 | QString(QUrl::toPercentEncoding(server["plugin_opts"].toString()))); 706 | serverConfig["plugins"] = getShadowsocksPlugins(plugins); 707 | } 708 | servers.append(serverConfig); 709 | } 710 | return servers; 711 | } 712 | -------------------------------------------------------------------------------- /src/serverconfighelper.h: -------------------------------------------------------------------------------- 1 | #ifndef SERVERCONFIGHELPER_H 2 | #define SERVERCONFIGHELPER_H 3 | 4 | #include 5 | 6 | class ServerConfigHelper : public QObject { 7 | Q_OBJECT 8 | public: 9 | explicit ServerConfigHelper(QObject *parent = nullptr) : QObject(parent) {} 10 | enum class Protocol { VMESS, SHADOWSOCKS, TROJAN, UNKNOWN }; 11 | 12 | static Protocol getProtocol(QString protocol); 13 | static QStringList getServerConfigErrors( 14 | Protocol protocol, 15 | const QJsonObject &serverConfig, 16 | const QString *pServerName = nullptr); 17 | static QJsonObject getPrettyServerConfig(Protocol protocol, 18 | const QJsonObject &serverConfig); 19 | static QJsonObject getServerConfigFromUrl(Protocol protocol, 20 | const QString &serverUrl, 21 | const QString &subscriptionUrl); 22 | static QList getServerConfigFromV2RayConfig( 23 | const QJsonObject &config); 24 | static QList getServerConfigFromShadowsocksQt5Config( 25 | const QJsonObject &config); 26 | 27 | private: 28 | static QString getServerNameError(const QJsonObject &serverConfig, 29 | const QString *pServerName = nullptr); 30 | static QStringList getV2RayServerConfigErrors( 31 | const QJsonObject &serverConfig, const QString *pServerName = nullptr); 32 | static QStringList getV2RayStreamSettingsErrors( 33 | const QJsonObject &serverConfig, const QString &network); 34 | static QJsonObject getV2RayServerSettingsFromConfig( 35 | const QJsonObject &settings); 36 | static QJsonObject getV2RayStreamSettingsFromConfig( 37 | const QJsonObject &transport, const QJsonObject &streamSettings); 38 | static QJsonObject getPrettyV2RayConfig(const QJsonObject &serverConfig); 39 | static QJsonArray getRandomUserAgents(int n); 40 | static QJsonObject getV2RayServerConfigFromUrl( 41 | const QString &server, const QString &subscriptionUrl); 42 | static bool isShadowsocksR(const QJsonObject &serverConfig); 43 | static QStringList getShadowsocksServerConfigErrors( 44 | const QJsonObject &serverConfig, const QString *pServerName = nullptr); 45 | static QJsonObject getPrettyShadowsocksConfig( 46 | const QJsonObject &serverConfig); 47 | static QJsonObject getShadowsocksServerConfigFromUrl( 48 | QString serverUrl, const QString &subscriptionUrl); 49 | static QJsonObject getShadowsocksPlugins(const QString &pluginString); 50 | static QJsonObject getShadowsocksRServerConfigFromUrl( 51 | QString serverUrl, const QString &subscriptionUrl); 52 | static QStringList getTrojanServerConfigErrors( 53 | const QJsonObject &serverConfig, const QString *pServerName = nullptr); 54 | static QJsonObject getPrettyTrojanConfig(const QJsonObject &serverConfig); 55 | static QJsonObject getTrojanServerConfigFromUrl( 56 | QString serverUrl, const QString &subscriptionUrl); 57 | static QJsonObject getTrojanOptions(const QString &optionString); 58 | }; 59 | 60 | #endif // SERVERCONFIGHELPER_H 61 | -------------------------------------------------------------------------------- /src/ui/about.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import QtQuick.Layouts 1.15 4 | 5 | import com.v2ray.desktop.AppProxy 2.4 6 | 7 | ColumnLayout { 8 | anchors.fill: parent 9 | anchors.margins: 10 10 | spacing: 20 11 | 12 | RowLayout { 13 | Image { 14 | source: "qrc:///images/icon-about.svg" 15 | sourceSize.width: 40 16 | sourceSize.height: 40 17 | mipmap: true 18 | } 19 | 20 | Text { 21 | text: qsTr("About") 22 | color: "white" 23 | font.pointSize: Qt.platform.os == "windows" ? 20 : 24 24 | } 25 | } 26 | 27 | GridLayout { 28 | columns: 3 29 | flow: GridLayout.LeftToRight 30 | rowSpacing: 20 31 | columnSpacing: 20 32 | 33 | Label { 34 | text: qsTr("V2Ray Desktop Version") 35 | color: "white" 36 | font.bold: true 37 | font.pointSize: 10.5 38 | } 39 | 40 | Label { 41 | id: labelAppVersion 42 | text: "N/a" 43 | color: "white" 44 | font.pointSize: 10.5 45 | property string value: "N/a" 46 | } 47 | 48 | Button { 49 | id: buttonAppCheckUpdates 50 | text: qsTr("Check for updates") 51 | contentItem: Text { 52 | text: parent.text 53 | color: parent.enabled ? "#3498db" : "#ccc" 54 | font.pointSize: 10.5 55 | } 56 | background: Rectangle { 57 | color: "#2e3e4e" 58 | radius: 4 59 | } 60 | onClicked: function() { 61 | buttonAppCheckUpdates.text = qsTr("Checking updates ...") 62 | buttonAppCheckUpdates.enabled = false 63 | AppProxy.getLatestRelease("v2ray-desktop") 64 | } 65 | } 66 | 67 | Label { 68 | text: qsTr("Clash Version") 69 | color: "white" 70 | font.bold: true 71 | font.pointSize: 10.5 72 | } 73 | 74 | Label { 75 | id: labelV2rayVersion 76 | text: "N/a" 77 | color: "white" 78 | font.pointSize: 10.5 79 | property string value: "N/a" 80 | } 81 | 82 | Button { 83 | id: buttonV2RayCheckUpdates 84 | text: qsTr("Check for updates") 85 | contentItem: Text { 86 | text: parent.text 87 | color: parent.enabled ? "#3498db" : "#ccc" 88 | font.pointSize: 10.5 89 | } 90 | background: Rectangle { 91 | color: "#2e3e4e" 92 | radius: 4 93 | } 94 | onClicked: function() { 95 | buttonV2RayCheckUpdates.text = qsTr("Checking updates ...") 96 | buttonV2RayCheckUpdates.enabled = false 97 | AppProxy.getLatestRelease("v2ray-core") 98 | } 99 | } 100 | 101 | Label { 102 | text: qsTr("Project Page") 103 | color: "white" 104 | font.bold: true 105 | font.pointSize: 10.5 106 | } 107 | 108 | Label { 109 | color: "white" 110 | font.pointSize: 10.5 111 | text: "https://github.com/Dr-Incognito/V2Ray-Desktop" 112 | } 113 | } 114 | 115 | Item { // spacer item 116 | Layout.fillWidth: true 117 | Layout.fillHeight: true 118 | Rectangle { 119 | anchors.fill: parent 120 | color: "transparent" 121 | } 122 | } 123 | 124 | Connections { 125 | target: AppProxy 126 | 127 | function onAppVersionReady(appVersion) { 128 | labelAppVersion.text = labelAppVersion.value = appVersion 129 | } 130 | 131 | function onV2RayCoreVersionReady(v2RayVersion) { 132 | labelV2rayVersion.text = labelV2rayVersion.value = v2RayVersion 133 | } 134 | 135 | function onLatestReleaseReady(name, latestVersion) { 136 | var buttonCheckUpdates, labelVersion 137 | 138 | if (name === "v2ray-core") { 139 | buttonCheckUpdates = buttonV2RayCheckUpdates 140 | labelVersion = labelV2rayVersion 141 | } else if (name === "v2ray-desktop") { 142 | buttonCheckUpdates = buttonAppCheckUpdates 143 | labelVersion = labelAppVersion 144 | } 145 | if (latestVersion.length === 0) { 146 | labelVersion.text = labelVersion.value + " (" + 147 | qsTr("Already the latest verion") + ")" 148 | } else { 149 | labelVersion.text = labelVersion.value + " (" + 150 | qsTr("Newer verion available: ") + latestVersion + ")" 151 | } 152 | buttonCheckUpdates.text = qsTr("Check for updates") 153 | buttonCheckUpdates.enabled = true 154 | } 155 | 156 | function onLatestReleaseError(name, errorMsg) { 157 | var buttonCheckUpdates, labelVersion 158 | 159 | if (name === "v2ray-core") { 160 | buttonCheckUpdates = buttonV2RayCheckUpdates 161 | labelVersion = labelV2rayVersion 162 | } else if (name === "v2ray-desktop") { 163 | buttonCheckUpdates = buttonAppCheckUpdates 164 | labelVersion = labelAppVersion 165 | } 166 | labelVersion.text = labelVersion.value + " (" + errorMsg + ")" 167 | buttonCheckUpdates.text = qsTr("Check for updates") 168 | buttonCheckUpdates.enabled = true 169 | } 170 | } 171 | 172 | Component.onCompleted: function() { 173 | AppProxy.getAppVersion() 174 | AppProxy.getV2RayCoreVersion() 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/ui/dashboard.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import QtQuick.Layouts 1.15 4 | 5 | import com.v2ray.desktop.AppProxy 2.4 6 | 7 | ColumnLayout { 8 | id: layoutDashboard 9 | anchors.fill: parent 10 | anchors.margins: 10 11 | spacing: 20 12 | 13 | RowLayout { 14 | Image { 15 | source: "qrc:///images/icon-dashboard.svg" 16 | sourceSize.width: 40 17 | sourceSize.height: 40 18 | mipmap: true 19 | } 20 | 21 | Text { 22 | text: qsTr("Dashboard") 23 | color: "white" 24 | font.pointSize: Qt.platform.os == "windows" ? 20 : 24 25 | } 26 | } 27 | 28 | GridLayout { 29 | columns: 2 30 | flow: GridLayout.LeftToRight 31 | rowSpacing: 20 32 | columnSpacing: 20 33 | 34 | Label { 35 | text: qsTr("Network Status") 36 | color: "white" 37 | font.pointSize: 10.5 38 | font.bold: true 39 | Layout.alignment: Qt.AlignTop 40 | } 41 | 42 | Label { 43 | id: labelNetworkStatus 44 | font.pointSize: 10.5 45 | text: qsTr("N/a") 46 | color: "white" 47 | } 48 | 49 | Label { 50 | text: qsTr("Proxy Settings") 51 | font.pointSize: 10.5 52 | color: "white" 53 | font.bold: true 54 | Layout.alignment: Qt.AlignTop 55 | } 56 | 57 | Label { 58 | id: labelProxySettings 59 | font.pointSize: 10.5 60 | text: qsTr("N/a") 61 | color: "white" 62 | } 63 | 64 | Label { 65 | text: qsTr("Operating System") 66 | font.pointSize: 10.5 67 | color: "white" 68 | font.bold: true 69 | } 70 | 71 | Label { 72 | id: labelOperatingSystem 73 | font.pointSize: 10.5 74 | text: qsTr("N/a") 75 | color: "white" 76 | } 77 | 78 | Label { 79 | text: qsTr("V2Ray Desktop Version") 80 | font.pointSize: 10.5 81 | color: "white" 82 | font.bold: true 83 | } 84 | 85 | Label { 86 | id: labelAppVersion 87 | font.pointSize: 10.5 88 | text: qsTr("N/a") 89 | color: "white" 90 | } 91 | 92 | Label { 93 | text: qsTr("Clash Version") 94 | font.pointSize: 10.5 95 | color: "white" 96 | font.bold: true 97 | } 98 | 99 | Label { 100 | id: labelV2rayVersion 101 | font.pointSize: 10.5 102 | text: qsTr("N/a") 103 | color: "white" 104 | } 105 | } 106 | 107 | Item { // spacer item 108 | Layout.fillWidth: true 109 | Layout.fillHeight: true 110 | Rectangle { 111 | anchors.fill: parent 112 | color: "transparent" 113 | } 114 | } 115 | 116 | Timer { 117 | interval: 5000 118 | running: true 119 | repeat: true 120 | onTriggered: function() { 121 | if (appWindow.visible) { 122 | AppProxy.getNetworkStatus() 123 | AppProxy.getProxySettings() 124 | } 125 | } 126 | } 127 | 128 | Connections { 129 | target: AppProxy 130 | 131 | function onAppVersionReady(appVersion) { 132 | labelAppVersion.text = appVersion 133 | } 134 | 135 | function onV2RayCoreVersionReady(v2RayVersion) { 136 | labelV2rayVersion.text = v2RayVersion 137 | } 138 | 139 | function onOperatingSystemReady(operatingSystem) { 140 | labelOperatingSystem.text = operatingSystem 141 | } 142 | 143 | function onNetworkStatusReady(networkStatus) { 144 | networkStatus = JSON.parse(networkStatus) 145 | if (networkStatus["isGoogleAccessible"]) { 146 | labelNetworkStatus.text = qsTr("Everything works fine.\nYou can access the free Internet.") 147 | } else if (networkStatus["isBaiduAccessible"]) { 148 | labelNetworkStatus.text = qsTr("Please check your proxy settings.") 149 | } else { 150 | labelNetworkStatus.text = qsTr("You're offline.\nPlease check the network connection.") 151 | } 152 | } 153 | 154 | function onProxySettingsReady(proxySettings) { 155 | proxySettings = JSON.parse(proxySettings) 156 | var pSettings = ""; 157 | pSettings += qsTr("System Proxy: ") + proxySettings["systemProxy"] + "\n" 158 | pSettings += qsTr("Proxy Mode: ") + proxySettings["proxyMode"] + "\n" 159 | pSettings += qsTr("Clash: ") + (proxySettings["isV2RayRunning"] ? qsTr("Running") : qsTr("Not running")) + "\n" 160 | if (proxySettings["isV2RayRunning"]) { 161 | pSettings += qsTr("Connected Servers:") + "\n" 162 | for (var i = 0; i < proxySettings["connectedServers"].length; ++ i) { 163 | pSettings += "- " + proxySettings["connectedServers"][i] + "\n" 164 | } 165 | } else if (proxySettings["connectedServers"].length === 0) { 166 | pSettings += qsTr("Please connect to at least one server!") 167 | } 168 | 169 | labelProxySettings.text = pSettings 170 | } 171 | } 172 | 173 | Component.onCompleted: function() { 174 | AppProxy.getAppVersion() 175 | AppProxy.getOperatingSystem() 176 | AppProxy.getV2RayCoreVersion() 177 | AppProxy.getNetworkStatus() 178 | AppProxy.getProxySettings() 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/ui/logs.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import QtQuick.Layouts 1.15 4 | 5 | import com.v2ray.desktop.AppProxy 2.4 6 | 7 | ColumnLayout { 8 | anchors.fill: parent 9 | anchors.margins: 10 10 | spacing: 20 11 | 12 | RowLayout { 13 | Image { 14 | source: "qrc:///images/icon-logs.svg" 15 | sourceSize.width: 40 16 | sourceSize.height: 40 17 | } 18 | 19 | Text { 20 | text: qsTr("Logs") 21 | color: "white" 22 | font.pointSize: Qt.platform.os == "windows" ? 20 : 24 23 | } 24 | 25 | Item { // spacer item 26 | Layout.fillWidth: true 27 | 28 | Rectangle { 29 | anchors.fill: parent 30 | color: "transparent" 31 | } 32 | } 33 | 34 | Button { 35 | /* Fix the layout bug introduced in Qt 5.15 */ 36 | Layout.rightMargin: 20 37 | 38 | text: qsTr("Clear Logs") 39 | contentItem: Text { 40 | text: parent.text 41 | color: "white" 42 | font.pointSize: 10.5 43 | padding: 4 44 | } 45 | background: Rectangle { 46 | color: parent.enabled ? (parent.down ? "#c0392b" : "#e74c3c") : "#bdc3c7" 47 | radius: 4 48 | } 49 | onClicked: function() { 50 | AppProxy.clearLogs() 51 | textLogs.text = "" 52 | } 53 | } 54 | } 55 | 56 | Item { 57 | Layout.fillWidth: true 58 | Layout.fillHeight: true 59 | /* Fix the layout bug introduced in Qt 5.15 */ 60 | Layout.rightMargin: 20 61 | 62 | Rectangle { 63 | id: rectLogs 64 | width:parent.width 65 | height:parent.height 66 | border.color: "#fff" 67 | color: "#2e3e4e" 68 | 69 | Flickable { 70 | id: flick 71 | anchors.fill: parent 72 | contentWidth: textLogs.paintedWidth 73 | contentHeight: textLogs.paintedHeight 74 | clip: true 75 | 76 | function ensureVisible(r) { 77 | if (contentX >= r.x) { 78 | contentX = r.x; 79 | } else if (contentX+width <= r.x+r.width) { 80 | contentX = r.x+r.width-width; 81 | } if (contentY >= r.y) { 82 | contentY = r.y; 83 | } else if (contentY+height <= r.y+r.height) { 84 | contentY = r.y+r.height-height; 85 | } 86 | } 87 | 88 | TextEdit { 89 | id: textLogs 90 | color: "white" 91 | focus: true 92 | readOnly: true 93 | selectByMouse: true 94 | text: qsTr("Loading logs ...") 95 | font.pointSize: 10.5 96 | textMargin: 10 97 | wrapMode: Text.Wrap 98 | width: flick.width 99 | onCursorRectangleChanged: flick.ensureVisible(cursorRectangle) 100 | } 101 | } 102 | } 103 | } 104 | 105 | Connections { 106 | target: AppProxy 107 | 108 | function onLogsReady(logs) { 109 | textLogs.text = logs 110 | } 111 | } 112 | 113 | Timer { 114 | interval: 5000 115 | running: true 116 | repeat: true 117 | onTriggered: function() { 118 | AppProxy.getLogs() 119 | } 120 | } 121 | 122 | Component.onCompleted: function() { 123 | AppProxy.getLogs() 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/ui/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import Qt.labs.platform 1.1 4 | 5 | import com.v2ray.desktop.AppProxy 2.4 6 | 7 | ApplicationWindow { 8 | id: appWindow 9 | visible: true 10 | width: 960 11 | height: 640 12 | minimumWidth: 640 13 | minimumHeight: 480 14 | title: qsTr("V2Ray Desktop") 15 | x: (screen.width - appWindow.width) / 2 16 | y: (screen.height - appWindow.height) / 2 17 | property bool firstRun: true 18 | property string currentSysProxyProtocol: "" 19 | 20 | onClosing: function() { 21 | appWindow.hide() 22 | } 23 | 24 | Shortcut { 25 | sequence: StandardKey.Close 26 | onActivated: function() { 27 | appWindow.close() 28 | } 29 | } 30 | 31 | SystemTrayIcon { 32 | visible: true 33 | icon.source: Qt.platform.os === "osx" ? "qrc:/images/v2ray.gray.png" : "qrc:/images/v2ray.png" 34 | icon.mask: true 35 | 36 | menu: Menu { 37 | MenuItem { 38 | id: appName 39 | text: qsTr("V2Ray Desktop") 40 | enabled: false 41 | } 42 | 43 | MenuItem { 44 | id: triggerV2RayCore 45 | text: qsTr("Turn V2Ray Desktop On") 46 | property bool isV2RayRunning: false 47 | 48 | onTriggered: function() { 49 | if (!isV2RayRunning) { 50 | AppProxy.setV2RayCoreRunning(true) 51 | } else if (isV2RayRunning) { 52 | AppProxy.setV2RayCoreRunning(false) 53 | } 54 | } 55 | } 56 | 57 | MenuSeparator {} 58 | 59 | MenuItem { 60 | id: menuItemRuleMode 61 | text: qsTr("Rule Mode") 62 | checkable: true 63 | enabled: false 64 | onTriggered: function() { 65 | AppProxy.setProxyMode("Rule") 66 | } 67 | } 68 | 69 | MenuItem { 70 | id: menuItemGlobalMode 71 | text: qsTr("Global Mode") 72 | checkable: true 73 | enabled: false 74 | onTriggered: function() { 75 | AppProxy.setProxyMode("Global") 76 | } 77 | } 78 | 79 | MenuItem { 80 | id: menuItemDirectMode 81 | text: qsTr("Direct Mode") 82 | checkable: true 83 | checked: true 84 | onTriggered: function() { 85 | AppProxy.setProxyMode("Direct") 86 | } 87 | } 88 | 89 | MenuSeparator {} 90 | 91 | MenuItem { 92 | id: menuItemSetHttpProxy 93 | text: qsTr("Set System Proxy (HTTP)") 94 | checkable: true 95 | enabled: false 96 | onTriggered: function() { 97 | menuItemSetSocksProxy.checked = false 98 | AppProxy.setSystemProxy(menuItemSetHttpProxy.checked, "http") 99 | } 100 | } 101 | 102 | MenuItem { 103 | id: menuItemSetSocksProxy 104 | text: qsTr("Set System Proxy (SOCKS)") 105 | checkable: true 106 | enabled: false 107 | visible: Qt.platform.os == "windows" ? false : true 108 | onTriggered: function() { 109 | menuItemSetHttpProxy.checked = false 110 | AppProxy.setSystemProxy(menuItemSetSocksProxy.checked, "socks") 111 | } 112 | } 113 | 114 | MenuSeparator {} 115 | 116 | MenuItem { 117 | text: qsTr("Dashboard") 118 | onTriggered: function() { 119 | mouseAreaDashboard.clicked(null) 120 | appWindow.show() 121 | appWindow.requestActivate() 122 | appWindow.raise() 123 | } 124 | } 125 | 126 | MenuItem { 127 | id: menuItemServers 128 | text: qsTr("Servers") 129 | onTriggered: function() { 130 | mouseAreaServers.clicked(null) 131 | appWindow.show() 132 | appWindow.requestActivate() 133 | appWindow.raise() 134 | } 135 | } 136 | 137 | MenuItem { 138 | text: Qt.platform.os == "osx" ? qsTr("Preferences") : qsTr("Settings") 139 | onTriggered: function() { 140 | mouseAreaSettings.clicked(null) 141 | appWindow.show() 142 | appWindow.requestActivate() 143 | appWindow.raise() 144 | 145 | } 146 | } 147 | 148 | MenuItem { 149 | text: qsTr("Scan QR Code on the Screen") 150 | onTriggered: function() { 151 | AppProxy.scanQrCodeScreen() 152 | mouseAreaServers.clicked(null) 153 | appWindow.show() 154 | appWindow.requestActivate() 155 | appWindow.raise() 156 | } 157 | } 158 | 159 | MenuSeparator {} 160 | 161 | MenuItem { 162 | text: qsTr("Logs") 163 | onTriggered: function() { 164 | mouseAreaLogs.clicked(null) 165 | appWindow.show() 166 | appWindow.requestActivate() 167 | appWindow.raise() 168 | } 169 | } 170 | 171 | MenuItem { 172 | text: qsTr("Feedback") 173 | onTriggered: Qt.openUrlExternally("https://github.com/Dr-Incognito/V2Ray-Desktop/issues") 174 | } 175 | 176 | MenuItem { 177 | text: qsTr("About") 178 | onTriggered: function() { 179 | mouseAreaAbout.clicked(null) 180 | appWindow.show() 181 | appWindow.requestActivate() 182 | appWindow.raise() 183 | } 184 | } 185 | 186 | MenuItem { 187 | text: qsTr("Quit V2Ray Desktop") 188 | onTriggered: Qt.quit() 189 | shortcut: StandardKey.Quit 190 | } 191 | } 192 | } 193 | 194 | Rectangle{ 195 | id: sidebar 196 | color: "#293846" 197 | width: 240 198 | height: parent.height 199 | anchors.top: parent.top 200 | anchors.bottom: parent.bottom 201 | 202 | Image { 203 | id: logo 204 | source: "qrc:///images/logo.png" 205 | width: 200 206 | height: 50 207 | x: 20 208 | y: 10 209 | mipmap: true 210 | } 211 | 212 | Item { 213 | width: parent.width 214 | height: parent.height - 70 215 | y: 70 216 | 217 | Rectangle { 218 | id: navDashboard 219 | color: "#354759" 220 | width: parent.width 221 | height: 40 222 | y: 0 223 | 224 | Image { 225 | source: "qrc:///images/icon-dashboard.svg" 226 | height: 14 227 | width: 14 228 | x: 20 229 | y: 12 230 | } 231 | 232 | Text { 233 | color: "white" 234 | text: qsTr("Dashboard") 235 | font.pointSize: Qt.platform.os == "windows" ? 12 : 14 236 | x: 40 237 | y: 10 238 | } 239 | 240 | MouseArea { 241 | id: mouseAreaDashboard 242 | width: parent.width 243 | height: parent.height 244 | 245 | onClicked: function() { 246 | navServers.color = "#263441" 247 | navRules.color = "#263441" 248 | navSettings.color = "#263441" 249 | navAbout.color = "#263441" 250 | navLogs.color = "#263441" 251 | navDashboard.color = "#354759" 252 | pageLoader.source = "dashboard.qml" 253 | } 254 | } 255 | } 256 | 257 | Rectangle { 258 | id: navServers 259 | color: "#263441" 260 | width: parent.width 261 | height: 40 262 | y: 40 263 | 264 | MouseArea { 265 | id: mouseAreaServers 266 | width: parent.width 267 | height: parent.height 268 | 269 | onClicked: function() { 270 | navDashboard.color = "#263441" 271 | navRules.color = "#263441" 272 | navSettings.color = "#263441" 273 | navAbout.color = "#263441" 274 | navLogs.color = "#263441" 275 | navServers.color = "#354759" 276 | pageLoader.source = "servers.qml" 277 | } 278 | } 279 | 280 | Image { 281 | source: "qrc:///images/icon-servers.svg" 282 | height: 14 283 | width: 14 284 | x: 20 285 | y: 12 286 | } 287 | 288 | Text { 289 | color: "white" 290 | text: qsTr("Servers") 291 | font.pointSize: Qt.platform.os == "windows" ? 12 : 14 292 | x: 40 293 | y: 10 294 | } 295 | } 296 | 297 | Rectangle { 298 | id: navRules 299 | color: "#263441" 300 | width: parent.width 301 | height: 40 302 | y: 80 303 | 304 | MouseArea { 305 | width: parent.width 306 | height: parent.height 307 | 308 | onClicked: function() { 309 | navDashboard.color = "#263441" 310 | navServers.color = "#263441" 311 | navSettings.color = "#263441" 312 | navAbout.color = "#263441" 313 | navLogs.color = "#263441" 314 | navRules.color = "#354759" 315 | pageLoader.source = "rules.qml" 316 | } 317 | } 318 | 319 | Image { 320 | source: "qrc:///images/icon-rules.svg" 321 | height: 14 322 | width: 14 323 | x: 20 324 | y: 12 325 | } 326 | 327 | Text { 328 | color: "white" 329 | text: qsTr("Rules") 330 | font.pointSize: Qt.platform.os == "windows" ? 12 : 14 331 | x: 40 332 | y: 10 333 | } 334 | } 335 | 336 | Rectangle { 337 | id: navSettings 338 | color: "#263441" 339 | width: parent.width 340 | height: 40 341 | y: 120 342 | 343 | MouseArea { 344 | id: mouseAreaSettings 345 | width: parent.width 346 | height: parent.height 347 | 348 | onClicked: function() { 349 | navDashboard.color = "#263441" 350 | navServers.color = "#263441" 351 | navRules.color = "#263441" 352 | navAbout.color = "#263441" 353 | navLogs.color = "#263441" 354 | navSettings.color = "#354759" 355 | pageLoader.source = "settings.qml" 356 | } 357 | } 358 | 359 | Image { 360 | source: "qrc:///images/icon-settings.svg" 361 | height: 14 362 | width: 14 363 | x: 20 364 | y: 12 365 | } 366 | 367 | Text { 368 | color: "white" 369 | text: Qt.platform.os == "osx" ? qsTr("Preferences") : qsTr("Settings") 370 | font.pointSize: Qt.platform.os == "windows" ? 12 : 14 371 | x: 40 372 | y: 10 373 | } 374 | } 375 | 376 | Rectangle { 377 | id: navLogs 378 | color: "#263441" 379 | width: parent.width 380 | height: 40 381 | y: 160 382 | 383 | MouseArea { 384 | id: mouseAreaLogs 385 | width: parent.width 386 | height: parent.height 387 | 388 | onClicked: function() { 389 | navDashboard.color = "#263441" 390 | navServers.color = "#263441" 391 | navRules.color = "#263441" 392 | navSettings.color = "#263441" 393 | navAbout.color = "#263441" 394 | navLogs.color = "#354759" 395 | pageLoader.source = "logs.qml" 396 | } 397 | } 398 | 399 | Image { 400 | source: "qrc:///images/icon-logs.svg" 401 | height: 14 402 | width: 14 403 | x: 20 404 | y: 12 405 | } 406 | 407 | Text { 408 | color: "white" 409 | text: qsTr("Logs") 410 | font.pointSize: Qt.platform.os == "windows" ? 12 : 14 411 | x: 40 412 | y: 10 413 | } 414 | } 415 | 416 | Rectangle { 417 | id: navAbout 418 | color: "#263441" 419 | width: parent.width 420 | height: 40 421 | y: 200 422 | 423 | MouseArea { 424 | id: mouseAreaAbout 425 | width: parent.width 426 | height: parent.height 427 | 428 | onClicked: function() { 429 | navDashboard.color = "#263441" 430 | navServers.color = "#263441" 431 | navRules.color = "#263441" 432 | navSettings.color = "#263441" 433 | navLogs.color = "#263441" 434 | navAbout.color = "#354759" 435 | pageLoader.source = "about.qml" 436 | } 437 | } 438 | 439 | Image { 440 | source: "qrc:///images/icon-about.svg" 441 | height: 14 442 | width: 14 443 | x: 20 444 | y: 12 445 | } 446 | 447 | Text { 448 | color: "white" 449 | text: qsTr("About") 450 | font.pointSize: Qt.platform.os == "windows" ? 12 : 14 451 | x: 40 452 | y: 10 453 | } 454 | } 455 | } 456 | } 457 | 458 | Rectangle { 459 | id: content 460 | color: "#2e3e4e" 461 | width: parent.width - 240 462 | height: parent.height 463 | anchors.left: sidebar.right 464 | anchors.top: parent.top 465 | anchors.bottom: parent.bottom 466 | 467 | Loader { 468 | id: pageLoader 469 | anchors.fill: parent 470 | source: "dashboard.qml" 471 | } 472 | } 473 | 474 | Connections { 475 | target: AppProxy 476 | 477 | function onAppVersionReady(appVersion) { 478 | appName.text = qsTr("V2Ray Desktop") + " " + appVersion 479 | } 480 | 481 | function onAppConfigReady(config) { 482 | config = JSON.parse(config) 483 | if (appWindow.firstRun && config["hideWindow"]) { 484 | appWindow.close() 485 | appWindow.firstRun = false 486 | } 487 | if (config["enableSysProxy"]) { 488 | appWindow.currentSysProxyProtocol = config["defaultSysProxyProtocol"] 489 | } 490 | } 491 | 492 | function onV2RayCoreStatusReady(isRunning) { 493 | if (!isRunning) { 494 | triggerV2RayCore.text = qsTr("Turn V2Ray Desktop On") 495 | triggerV2RayCore.isV2RayRunning = false 496 | menuItemRuleMode.enabled = false 497 | menuItemGlobalMode.enabled = false 498 | menuItemSetHttpProxy.enabled = false 499 | menuItemSetSocksProxy.enabled = false 500 | menuItemRuleMode.checked = false 501 | menuItemGlobalMode.checked = false 502 | menuItemDirectMode.checked = true 503 | menuItemSetHttpProxy.checked = false 504 | menuItemSetSocksProxy.checked = false 505 | AppProxy.setSystemProxy(false) 506 | } else { 507 | triggerV2RayCore.text = qsTr("Turn V2Ray Desktop Off") 508 | triggerV2RayCore.isV2RayRunning = true 509 | menuItemRuleMode.enabled = true 510 | menuItemGlobalMode.enabled = true 511 | menuItemSetHttpProxy.enabled = true 512 | menuItemSetSocksProxy.enabled = true 513 | 514 | // Set system proxy automatically 515 | AppProxy.setProxyMode() 516 | 517 | if (appWindow.currentSysProxyProtocol === "http") { 518 | AppProxy.setSystemProxy(true, "http") 519 | menuItemSetHttpProxy.checked = true 520 | } else if (appWindow.currentSysProxyProtocol === "socks") { 521 | AppProxy.setSystemProxy(true, "socks") 522 | menuItemSetSocksProxy.checked = true 523 | } 524 | } 525 | } 526 | 527 | function onProxyModeChanged(proxyMode) { 528 | menuItemRuleMode.checked = false 529 | menuItemGlobalMode.checked = false 530 | menuItemDirectMode.checked = false 531 | 532 | if (proxyMode === "Rule") { 533 | menuItemRuleMode.checked = true 534 | } else if (proxyMode === "Global") { 535 | menuItemGlobalMode.checked = true 536 | } else if (proxyMode === "Direct") { 537 | menuItemDirectMode.checked = true 538 | } 539 | } 540 | } 541 | 542 | Component.onCompleted: { 543 | // Get App Version 544 | AppProxy.getAppVersion() 545 | // Get App Config 546 | AppProxy.getAppConfig() 547 | // Translate 548 | AppProxy.retranslate() 549 | // Start V2Ray Core automatically 550 | AppProxy.setV2RayCoreRunning(true) 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /src/ui/rules.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import QtQuick.Layouts 1.15 4 | import Qt.labs.platform 1.1 5 | 6 | import com.v2ray.desktop.AppProxy 2.4 7 | 8 | ColumnLayout { 9 | anchors.fill: parent 10 | anchors.margins: 10 11 | spacing: 20 12 | 13 | RowLayout { 14 | Image { 15 | source: "qrc:///images/icon-rules.svg" 16 | sourceSize.width: 40 17 | sourceSize.height: 40 18 | } 19 | 20 | Text { 21 | text: qsTr("Rules") 22 | color: "white" 23 | font.pointSize: Qt.platform.os == "windows" ? 20 : 24 24 | } 25 | } 26 | 27 | Label { 28 | id: labelErrorMsg 29 | background: Rectangle { 30 | color: "#ee8989" 31 | } 32 | color: "#652424" 33 | font.pointSize: 10.5 34 | Layout.fillWidth: true 35 | padding: 10 36 | visible: false 37 | wrapMode: Text.Wrap 38 | } 39 | 40 | GridLayout { 41 | columns: 3 42 | flow: GridLayout.LeftToRight 43 | rowSpacing: 20 44 | columnSpacing: 20 45 | 46 | Label { 47 | text: qsTr("GFW List Last Updated on") 48 | color: "white" 49 | font.pointSize: 10.5 50 | } 51 | 52 | Label { 53 | id: labelGfwLastUpdatedTime 54 | Layout.fillWidth: true 55 | color: "white" 56 | font.pointSize: 10.5 57 | } 58 | 59 | Button { 60 | id: buttonUpdateGfwList 61 | text: qsTr("Update GFW List Now") 62 | contentItem: Text { 63 | id: buttonUpdateGfwListContentItem 64 | text: parent.text 65 | color: "#3498db" 66 | font.pointSize: 10.5 67 | } 68 | background: Rectangle { 69 | color: "#2e3e4e" 70 | radius: 4 71 | } 72 | onClicked: function() { 73 | buttonUpdateGfwList.enabled = false 74 | buttonUpdateGfwListContentItem.color = "white" 75 | buttonUpdateGfwList.text = qsTr("Updating ...") 76 | AppProxy.updateGfwList() 77 | } 78 | } 79 | } 80 | 81 | Item { // spacer item 82 | Layout.fillWidth: true 83 | Layout.fillHeight: true 84 | Rectangle { 85 | anchors.fill: parent 86 | color: "transparent" 87 | } 88 | } 89 | 90 | MessageDialog { 91 | id: messageDialog 92 | title: qsTr("Message from V2Ray Desktop") 93 | text: qsTr("Settings saved.") 94 | buttons: MessageDialog.Ok 95 | } 96 | 97 | Connections { 98 | target: AppProxy 99 | 100 | function onAppConfigReady(config) { 101 | config = JSON.parse(config) 102 | labelGfwLastUpdatedTime.text = config["gfwListLastUpdated"] 103 | } 104 | 105 | function onAppConfigError(errorMsg) { 106 | labelErrorMsg.text = errorMsg 107 | labelErrorMsg.visible = true 108 | } 109 | 110 | function onGfwListUpdated(updatedTime) { 111 | buttonUpdateGfwList.text = qsTr("Update GFW List Now") 112 | buttonUpdateGfwListContentItem.color = "#3498db" 113 | buttonUpdateGfwList.enabled = true 114 | labelGfwLastUpdatedTime.text = updatedTime 115 | } 116 | } 117 | 118 | Component.onCompleted: function() { 119 | AppProxy.getAppConfig() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/ui/settings.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import QtQuick.Layouts 1.15 4 | import Qt.labs.platform 1.1 5 | 6 | import com.v2ray.desktop.AppProxy 2.4 7 | 8 | ColumnLayout { 9 | anchors.fill: parent 10 | anchors.margins: 10 11 | spacing: 20 12 | 13 | RowLayout { 14 | Image { 15 | source: "qrc:///images/icon-settings.svg" 16 | sourceSize.width: 40 17 | sourceSize.height: 40 18 | } 19 | 20 | Text { 21 | text: Qt.platform.os === "osx" ? qsTr("Preferences") : qsTr("Settings") 22 | color: "white" 23 | font.pointSize: Qt.platform.os === "windows" ? 20 : 24 24 | } 25 | } 26 | 27 | Label { 28 | id: labelErrorMsg 29 | background: Rectangle { 30 | color: "#ee8989" 31 | } 32 | color: "#652424" 33 | font.pointSize: 10.5 34 | Layout.fillWidth: true 35 | padding: 10 36 | visible: false 37 | wrapMode: Text.Wrap 38 | } 39 | 40 | GridLayout { 41 | columns: 4 42 | flow: GridLayout.LeftToRight 43 | rowSpacing: 20 44 | columnSpacing: 20 45 | 46 | Label { 47 | text: qsTr("Launch V2Ray Desktop at Login") 48 | color: "white" 49 | font.pointSize: 10.5 50 | } 51 | 52 | CheckBox { 53 | id: checkboxAutoStart 54 | leftPadding: -3 55 | } 56 | 57 | Label { 58 | text: qsTr("Hide Window on Start Up") 59 | color: "white" 60 | font.pointSize: 10.5 61 | } 62 | 63 | CheckBox { 64 | id: checkboxHideWindow 65 | leftPadding: -3 66 | } 67 | 68 | Label { 69 | text: qsTr("Language") 70 | color: "white" 71 | font.pointSize: 10.5 72 | } 73 | 74 | ComboBox { 75 | id: comboLanguage 76 | Layout.fillWidth: true 77 | textRole: "text" 78 | valueRole: "value" 79 | model: ListModel{ 80 | ListElement { text: "English"; value: "en-US" } 81 | ListElement { text: "简体中文"; value: "zh-CN" } 82 | } 83 | background: Rectangle { 84 | color: Qt.rgba(255, 255, 255, .1) 85 | border.color: Qt.rgba(120, 130, 140, .2) 86 | } 87 | contentItem: Text { 88 | text: comboLanguage.displayText 89 | color: "white" 90 | font.pointSize: 10.5 91 | padding: 7 92 | verticalAlignment: Text.AlignVCenter 93 | } 94 | } 95 | 96 | Label { 97 | text: qsTr("Proxy Mode") 98 | color: "white" 99 | font.pointSize: 10.5 100 | } 101 | 102 | ComboBox { 103 | id: comboProxyMode 104 | Layout.fillWidth: true 105 | textRole: "text" 106 | valueRole: "value" 107 | model: ListModel{ 108 | ListElement { text: "Rule Mode"; value: "Rule" } 109 | ListElement { text: "Global Mode"; value: "Global" } 110 | ListElement { text: "Direct Mode"; value: "Direct" } 111 | } 112 | background: Rectangle { 113 | color: Qt.rgba(255, 255, 255, .1) 114 | border.color: Qt.rgba(120, 130, 140, .2) 115 | } 116 | contentItem: Text { 117 | text: comboProxyMode.displayText 118 | color: "white" 119 | font.pointSize: 10.5 120 | padding: 7 121 | verticalAlignment: Text.AlignVCenter 122 | } 123 | } 124 | 125 | Label { 126 | text: qsTr("Listening IP Address") 127 | color: "white" 128 | font.pointSize: 10.5 129 | } 130 | 131 | TextField { 132 | id: textServerIpAddr 133 | color: "white" 134 | font.pointSize: 10.5 135 | padding: 7 136 | Layout.fillWidth: true 137 | placeholderText: qsTr("Example: 127.0.0.1") 138 | placeholderTextColor: "white" 139 | background: Rectangle { 140 | color: Qt.rgba(255, 255, 255, .1) 141 | border.color: Qt.rgba(120, 130, 140, .2) 142 | } 143 | } 144 | 145 | Label { 146 | text: qsTr("DNS Servers") 147 | color: "white" 148 | font.pointSize: 10.5 149 | } 150 | 151 | TextField { 152 | id: textDnsServers 153 | color: "white" 154 | font.pointSize: 10.5 155 | padding: 7 156 | Layout.fillWidth: true 157 | placeholderText: qsTr("Example: 8.8.8.8,8.8.4.4") 158 | placeholderTextColor: "white" 159 | background: Rectangle { 160 | color: Qt.rgba(255, 255, 255, .1) 161 | border.color: Qt.rgba(120, 130, 140, .2) 162 | } 163 | } 164 | 165 | Label { 166 | text: qsTr("SOCKS Port") 167 | color: "white" 168 | font.pointSize: 10.5 169 | } 170 | 171 | TextField { 172 | id: textSocksPort 173 | color: "white" 174 | font.pointSize: 10.5 175 | padding: 7 176 | Layout.fillWidth: true 177 | placeholderText: qsTr("Example: 1080") 178 | placeholderTextColor: "white" 179 | text: "1080" 180 | background: Rectangle { 181 | color: Qt.rgba(255, 255, 255, .1) 182 | border.color: Qt.rgba(120, 130, 140, .2) 183 | } 184 | } 185 | 186 | Label { 187 | text: qsTr("HTTP Port") 188 | color: "white" 189 | font.pointSize: 10.5 190 | } 191 | 192 | TextField { 193 | id: textHttpPort 194 | color: "white" 195 | font.pointSize: 10.5 196 | padding: 7 197 | Layout.fillWidth: true 198 | placeholderText: qsTr("Example: 1087") 199 | placeholderTextColor: "white" 200 | text: "1087" 201 | background: Rectangle { 202 | color: Qt.rgba(255, 255, 255, .1) 203 | border.color: Qt.rgba(120, 130, 140, .2) 204 | } 205 | } 206 | 207 | Label { 208 | text: qsTr("GFW List URL") 209 | color: "white" 210 | font.pointSize: 10.5 211 | } 212 | 213 | TextField { 214 | id: textGfwListUrl 215 | color: "white" 216 | font.pointSize: 10.5 217 | padding: 7 218 | Layout.fillWidth: true 219 | Layout.columnSpan: 3 220 | placeholderText: qsTr("Example: https://url/to/gfwlist.yml") 221 | placeholderTextColor: "white" 222 | background: Rectangle { 223 | color: Qt.rgba(255, 255, 255, .1) 224 | border.color: Qt.rgba(120, 130, 140, .2) 225 | } 226 | } 227 | 228 | Button { 229 | id: buttonSaveSettings 230 | text: qsTr("Save Settings") 231 | contentItem: Text { 232 | text: parent.text 233 | color: "white" 234 | font.pointSize: 10.5 235 | padding: 4 236 | } 237 | background: Rectangle { 238 | color: parent.enabled ? (parent.down ? "#2980b9" : "#3498db") : "#bdc3c7" 239 | radius: 4 240 | } 241 | onClicked: function() { 242 | var config = { 243 | "autoStart": checkboxAutoStart.checked, 244 | "hideWindow": checkboxHideWindow.checked, 245 | "language": comboLanguage.currentValue, 246 | "serverIp": textServerIpAddr.text, 247 | "httpPort": textHttpPort.text, 248 | "socksPort": textSocksPort.text, 249 | "dns": textDnsServers.text, 250 | "gfwListUrl": textGfwListUrl.text 251 | } 252 | labelErrorMsg.visible = false 253 | AppProxy.setProxyMode(comboProxyMode.currentValue) 254 | AppProxy.setAppConfig(JSON.stringify(config)) 255 | } 256 | } 257 | } 258 | 259 | Item { // spacer item 260 | Layout.fillWidth: true 261 | Layout.fillHeight: true 262 | Rectangle { 263 | anchors.fill: parent 264 | color: "transparent" 265 | } 266 | } 267 | 268 | MessageDialog { 269 | id: messageDialog 270 | title: qsTr("Message from V2Ray Desktop") 271 | text: qsTr("Settings saved.") 272 | buttons: MessageDialog.Ok 273 | } 274 | 275 | Connections { 276 | target: AppProxy 277 | 278 | function onAppConfigReady(config) { 279 | config = JSON.parse(config) 280 | checkboxAutoStart.checked = config["autoStart"] 281 | checkboxHideWindow.checked = config["hideWindow"] 282 | comboLanguage.currentIndex = comboLanguage.indexOfValue(config["language"]) 283 | comboProxyMode.currentIndex = comboProxyMode.indexOfValue(config["proxyMode"]) 284 | textServerIpAddr.text = config["serverIp"] 285 | textSocksPort.text = config["socksPort"] 286 | textHttpPort.text = config["httpPort"] 287 | textDnsServers.text = config["dns"] 288 | textGfwListUrl.text = config["gfwListUrl"] 289 | } 290 | 291 | function onV2RayCoreStatusReady(isRunning) { 292 | comboProxyMode.enabled = isRunning 293 | } 294 | 295 | function onAppConfigChanged() { 296 | messageDialog.open() 297 | } 298 | 299 | function onAppConfigError(errorMsg) { 300 | labelErrorMsg.text = errorMsg 301 | labelErrorMsg.visible = true 302 | } 303 | 304 | function onProxyModeChanged(proxyMode) { 305 | comboProxyMode.currentIndex = comboProxyMode.indexOfValue(proxyMode) 306 | } 307 | } 308 | 309 | Component.onCompleted: function() { 310 | AppProxy.getAppConfig() 311 | AppProxy.getV2RayCoreStatus() 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/utility.cpp: -------------------------------------------------------------------------------- 1 | #include "utility.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "configurator.h" 12 | #include "constants.h" 13 | #include "networkrequest.h" 14 | 15 | QString Utility::getNumericConfigError(const QJsonObject& config, 16 | const QString& key, 17 | const QString& name, 18 | int lowerBound, 19 | int upperBound) { 20 | if (!config.contains(key) || 21 | (config[key].isString() && config[key].toString().isEmpty())) { 22 | return QString(tr("Missing the value of '%1'.")).arg(name); 23 | } else { 24 | bool isConverted = false; 25 | int value = config[key].toVariant().toInt(&isConverted); 26 | if (!isConverted) { 27 | return QString(tr("The value of '%1' seems invalid.")).arg(name); 28 | } else if (upperBound == -127 && value < lowerBound) { 29 | return QString(tr("The value of '%1' should above %2.")) 30 | .arg(name, QString::number(lowerBound)); 31 | } else if (value < lowerBound || value > upperBound) { 32 | return QString(tr("The value of '%1' should between %2 and %3.")) 33 | .arg(name, QString::number(lowerBound), QString::number(upperBound)); 34 | } 35 | return ""; 36 | } 37 | } 38 | 39 | QString Utility::getStringConfigError( 40 | const QJsonObject& config, 41 | const QString& key, 42 | const QString& name, 43 | const QList>& checkpoints, 44 | bool allowEmpty, 45 | const QString& notPassedMsg) { 46 | if (allowEmpty && config[key].toString().isEmpty()) { 47 | return ""; 48 | } 49 | if (!config.contains(key) || config[key].toString().isEmpty()) { 50 | return QString(tr("Missing the value of '%1'.")).arg(name); 51 | } 52 | if (checkpoints.size() > 0) { 53 | bool isMatched = false; 54 | for (std::function ckpt : checkpoints) { 55 | if (ckpt(config[key].toString())) { 56 | isMatched = true; 57 | } 58 | } 59 | if (!isMatched) { 60 | return notPassedMsg.arg(name); 61 | } 62 | } 63 | return ""; 64 | } 65 | 66 | bool Utility::isIpAddrValid(const QString& ipAddr) { 67 | QRegularExpression ipAddrRegex( 68 | "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]" 69 | "|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"); 70 | return ipAddrRegex.match(ipAddr).hasMatch(); 71 | } 72 | 73 | bool Utility::isIpAddrListValid(const QString& ipAddrList) { 74 | QStringList ips = ipAddrList.split(";"); 75 | for (QString ip : ips) { 76 | if (!Utility::isIpAddrValid(ip.trimmed())) { 77 | return false; 78 | } 79 | } 80 | return true; 81 | } 82 | 83 | bool Utility::isDomainNameValid(const QString& domainName) { 84 | QRegularExpression domainNameRegex( 85 | "^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-" 86 | "9]$"); 87 | return domainNameRegex.match(domainName).hasMatch(); 88 | } 89 | 90 | bool Utility::isUrlValid(const QString& url) { 91 | QRegularExpression urlRegex( 92 | "^https?:\\/\\/" 93 | "(?:www\\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www\\.[" 94 | "a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|https?:\\/\\/" 95 | "(?:www\\.|(?!www))[a-zA-Z0-9]+\\.[^\\s]{2,}|www\\.[a-zA-Z0-9]+\\.[^\\s]{2," 96 | "}$", 97 | QRegularExpression::CaseInsensitiveOption); 98 | return urlRegex.match(url).hasMatch(); 99 | } 100 | 101 | bool Utility::isFileExists(const QString& filePath) { 102 | return QDir(filePath).exists(); 103 | } 104 | 105 | bool Utility::isServerNameNotUsed(const QString& serverName) { 106 | Configurator& configurator(Configurator::getInstance()); 107 | QJsonArray servers = configurator.getServers(); 108 | 109 | for (auto itr = servers.begin(); itr != servers.end(); ++itr) { 110 | QJsonObject server = (*itr).toObject(); 111 | QString _serverName = server["name"].toString(); 112 | if (serverName == _serverName) { 113 | return false; 114 | } 115 | } 116 | return true; 117 | } 118 | 119 | QStringList Utility::getAlpn(const QString& alpn) { 120 | QStringList alpns; 121 | for (QString a : alpn.split(";")) { 122 | if (a.isEmpty()) { 123 | continue; 124 | } 125 | alpns.append(a.trimmed()); 126 | } 127 | return alpns; 128 | } 129 | 130 | bool Utility::isAlpnValid(const QString& alpn) { 131 | static const QStringList ALPN_CANDIDATES = {"h2", "http/1.1"}; 132 | QStringList alpns = getAlpn(alpn); 133 | for (QString a : alpns) { 134 | if (!ALPN_CANDIDATES.contains(a)) { 135 | return false; 136 | } 137 | } 138 | return true; 139 | } 140 | 141 | QString Utility::formatV2RayLog(const QString& log) { 142 | int timeStart = log.indexOf("time="); 143 | int levelStart = log.indexOf("level="); 144 | int msgStart = log.indexOf("msg="); 145 | 146 | QString time = log.mid(timeStart + 6, levelStart - timeStart - 14) 147 | .replace('-', '/') 148 | .replace('T', ' '); 149 | QString level = log.mid(levelStart + 6, msgStart - levelStart - 7); 150 | QString msg = log.mid(msgStart + 5, log.size() - msgStart - 6); 151 | if (time.size() == 0) { 152 | return ""; 153 | } 154 | return QString("%1 [%2] clash: %3").arg(time, level, msg); 155 | } 156 | 157 | QString Utility::getLatestRelease(const QString& releaseUrl, 158 | const QNetworkProxy* proxy) { 159 | QByteArray releaseJsonStr = 160 | NetworkRequest::getNetworkResponse(releaseUrl, proxy, HTTP_GET_TIMEOUT); 161 | QJsonObject latestRelease; 162 | QJsonDocument releaseJsonDoc = QJsonDocument::fromJson(releaseJsonStr); 163 | QJsonArray releases = releaseJsonDoc.array(); 164 | for (int i = 0; i < releases.size(); ++i) { 165 | QJsonObject release = releases[i].toObject(); 166 | latestRelease = release; 167 | break; 168 | } 169 | return latestRelease.empty() ? "" : latestRelease["name"].toString(); 170 | } 171 | 172 | bool Utility::isVersionNewer(const QString& currentVersion, 173 | const QString& version) { 174 | QList _currentVersion = getVersion(currentVersion); 175 | QList _version = getVersion(version); 176 | 177 | for (int i = 0; i < _version.size() && i < _currentVersion.size(); ++i) { 178 | if (_version.at(i) > _currentVersion.at(i)) { 179 | return true; 180 | } else if (_version.at(i) < _currentVersion.at(i)) { 181 | return false; 182 | } 183 | } 184 | return false; 185 | } 186 | 187 | QList Utility::getVersion(QString version) { 188 | QList _version; 189 | if (version.startsWith('v') || version.startsWith('v')) { 190 | version = version.mid(1); 191 | } 192 | for (QString v : version.split('.')) { 193 | _version.append(v.toInt()); 194 | } 195 | return _version; 196 | } 197 | -------------------------------------------------------------------------------- /src/utility.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILITY_H 2 | #define UTILITY_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | class Utility : public QObject { 11 | Q_OBJECT 12 | public: 13 | explicit Utility(QObject *parent = nullptr) : QObject(parent) {} 14 | static QString getNumericConfigError(const QJsonObject &serverConfig, 15 | const QString &key, 16 | const QString &name, 17 | int lowerBound, 18 | int upperBound); 19 | static QString getStringConfigError( 20 | const QJsonObject &serverConfig, 21 | const QString &key, 22 | const QString &name, 23 | const QList> &checkpoints = {}, 24 | bool allowEmpty = false, 25 | const QString ¬PassedMsg = tr("The value of '%1' seems invalid.")); 26 | static bool isIpAddrValid(const QString &ipAddr); 27 | static bool isIpAddrListValid(const QString &ipAddrList); 28 | static bool isDomainNameValid(const QString &domainName); 29 | static bool isUrlValid(const QString &url); 30 | static bool isFileExists(const QString &filePath); 31 | static bool isServerNameNotUsed(const QString &serverName); 32 | static QStringList getAlpn(const QString &alpn); 33 | static bool isAlpnValid(const QString &alpn); 34 | static QString formatV2RayLog(const QString &log); 35 | static QString getLatestRelease(const QString &releaseUrl, 36 | const QNetworkProxy *proxy); 37 | static bool isVersionNewer(const QString ¤tVersion, 38 | const QString &version); 39 | static QList getVersion(QString version); 40 | }; 41 | 42 | #endif // UTILITY_H 43 | -------------------------------------------------------------------------------- /src/v2raycore.cpp: -------------------------------------------------------------------------------- 1 | #include "v2raycore.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "configurator.h" 11 | #include "constants.h" 12 | #include "utility.h" 13 | 14 | V2RayCore::V2RayCore() { 15 | QString v2RayInstallFolderPath = Configurator::getV2RayInstallDirPath(); 16 | #if defined(Q_OS_WIN) 17 | v2RayExecFilePath = QDir(v2RayInstallFolderPath).filePath("clash.exe"); 18 | #elif defined(Q_OS_LINUX) or defined(Q_OS_MAC) 19 | v2RayExecFilePath = QDir(v2RayInstallFolderPath).filePath("clash"); 20 | #endif 21 | QDir v2RayInstallFolder(v2RayInstallFolderPath); 22 | // Create the install folder and copy the Country.mmdb file 23 | if (!v2RayInstallFolder.exists()) { 24 | v2RayInstallFolder.mkpath("."); 25 | } 26 | QString configFilePath = Configurator::getV2RayConfigFilePath(); 27 | QString srcMmdbFilePath = 28 | QDir(v2RayInstallFolderPath).filePath("Country.mmdb"); 29 | QString dstMmdbFilePath = 30 | QFileInfo(configFilePath).dir().filePath("Country.mmdb"); 31 | if (QFile(srcMmdbFilePath).exists() && !QFile(dstMmdbFilePath).exists()) { 32 | QFile::copy(srcMmdbFilePath, dstMmdbFilePath); 33 | } 34 | 35 | // Initialize QProcess 36 | v2rayProcess = new QProcess(); 37 | } 38 | 39 | V2RayCore& V2RayCore::getInstance() { 40 | static V2RayCore v2RayCoreInstance; 41 | return v2RayCoreInstance; 42 | } 43 | 44 | V2RayCore::~V2RayCore() { 45 | this->stop(); 46 | delete v2rayProcess; 47 | } 48 | 49 | QString V2RayCore::getVersion() { 50 | if (!isInstalled()) { 51 | return tr("Not Installed"); 52 | } 53 | QProcess _v2rayProcess; 54 | _v2rayProcess.start(v2RayExecFilePath, {"-v"}); 55 | _v2rayProcess.waitForFinished(); 56 | QString v2RayVersion = _v2rayProcess.readAllStandardOutput(); 57 | if (v2RayVersion.isEmpty()) { 58 | return tr("Unknown"); 59 | } 60 | int sIndex = v2RayVersion.indexOf('.'); 61 | int pIndex = v2RayVersion.indexOf(' ', sIndex); 62 | return v2RayVersion.mid(sIndex - 1, pIndex - sIndex + 1).trimmed(); 63 | } 64 | 65 | bool V2RayCore::start() { 66 | if (!isInstalled()) { 67 | return false; 68 | } 69 | // Get latest configuration for Clash 70 | Configurator& configurator(Configurator::getInstance()); 71 | QJsonObject v2RayConfig = configurator.getV2RayConfig(); 72 | QString configFilePath = Configurator::getV2RayConfigFilePath(); 73 | QFile configFile(Configurator::getV2RayConfigFilePath()); 74 | configFile.open(QFile::WriteOnly); 75 | configFile.write(QJsonDocument(v2RayConfig).toJson(QJsonDocument::Indented)); 76 | configFile.flush(); 77 | 78 | // Start Clash 79 | QStringList arguments; 80 | QFileInfo configFileInfo(configFilePath); 81 | arguments << "-d" << configFileInfo.dir().absolutePath(); 82 | v2rayProcess->start(v2RayExecFilePath, arguments); 83 | v2rayProcess->waitForFinished(500); 84 | int exitCode = v2rayProcess->exitCode(); 85 | if (exitCode != 0) { 86 | qCritical() << "Failed to start Clash."; 87 | qCritical() << Utility::formatV2RayLog(v2rayProcess->readAll()); 88 | } 89 | v2rayProcess->setStandardErrorFile(Configurator::getV2RayLogFilePath()); 90 | v2rayProcess->setStandardOutputFile(Configurator::getV2RayLogFilePath()); 91 | return exitCode == 0; 92 | } 93 | 94 | bool V2RayCore::stop() { 95 | v2rayProcess->kill(); 96 | v2rayProcess->waitForFinished(); 97 | return v2rayProcess->state() == QProcess::NotRunning; 98 | } 99 | 100 | bool V2RayCore::restart() { 101 | stop(); 102 | start(); 103 | return isRunning(); 104 | } 105 | 106 | bool V2RayCore::isRunning() { 107 | return v2rayProcess->state() == QProcess::Running; 108 | } 109 | 110 | bool V2RayCore::isInstalled() { return QFile(v2RayExecFilePath).exists(); } 111 | -------------------------------------------------------------------------------- /src/v2raycore.h: -------------------------------------------------------------------------------- 1 | #ifndef V2RAYCORE_H 2 | #define V2RAYCORE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class V2RayCore : public QObject { 10 | Q_OBJECT 11 | public: 12 | static V2RayCore& getInstance(); 13 | V2RayCore(V2RayCore const&) = delete; 14 | void operator=(V2RayCore const&) = delete; 15 | ~V2RayCore(); 16 | QString getVersion(); 17 | bool isRunning(); 18 | bool start(); 19 | bool stop(); 20 | bool restart(); 21 | 22 | private: 23 | V2RayCore(); 24 | bool isInstalled(); 25 | QProcess* v2rayProcess; 26 | QString v2RayExecFilePath; 27 | }; 28 | 29 | #endif // V2RAYCORE_H 30 | --------------------------------------------------------------------------------