├── .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 | [](https://travis-ci.com/Dr-Incognito/V2Ray-Desktop)
4 | [](https://ci.appveyor.com/project/Dr-Incognito/V2Ray-Desktop)
5 | [](https://lgtm.com/projects/g/Dr-Incognito/V2Ray-Desktop/context:cpp)
6 | [](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 |
44 |
45 |
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 |
--------------------------------------------------------------------------------
/src/images/icon-dashboard.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/images/icon-logs.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/images/icon-rules.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/images/icon-servers.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/images/icon-settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------