├── .env ├── .gitignore ├── .smooth-releaserc ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASE.md ├── docs ├── README.md ├── development │ ├── README.md │ └── blinksocks-desktop.png ├── screenshots │ ├── client-settings.png │ ├── main-screen.png │ ├── pac-settings.png │ ├── server-settings.png │ └── updating.png └── usage │ └── README.md ├── package.json ├── public ├── assets │ └── images │ │ └── icon.png └── index.html ├── resources ├── adp-scripts.js ├── gfwlist.txt ├── icon.icns ├── icon.ico ├── icon.png ├── proxy_conf_helper.gz ├── sudo-agent_darwin_x64.gz ├── sysproxy.exe.gz ├── sysproxy64.exe.gz ├── tray-icon.ico └── tray-icon.png ├── src ├── backend │ ├── constants.js │ ├── helpers │ │ ├── fs.js │ │ ├── logger.js │ │ └── shell.js │ ├── init.js │ ├── main.js │ ├── modules │ │ ├── bs.js │ │ ├── log.js │ │ ├── pac.js │ │ ├── qrcode.js │ │ ├── sys.js │ │ └── update.js │ └── system │ │ ├── create.js │ │ └── platforms │ │ ├── darwin.js │ │ ├── index.js │ │ ├── interface.js │ │ ├── linux.js │ │ └── win32.js ├── components │ ├── DatePicker │ │ ├── DatePicker.css │ │ ├── DatePicker.js │ │ └── index.js │ ├── PopupDialog │ │ ├── PopupDialog.js │ │ └── index.js │ ├── PresetItem │ │ ├── PresetItem.css │ │ ├── PresetItem.js │ │ └── index.js │ ├── PresetParamItem │ │ ├── PresetParamItem.css │ │ ├── PresetParamItem.js │ │ └── index.js │ ├── ScreenMask │ │ ├── SceenMask.css │ │ ├── ScreenMask.js │ │ └── index.js │ ├── ServerItem │ │ ├── ServerItem.js │ │ └── index.js │ └── index.js ├── containers │ ├── App │ │ ├── App.css │ │ ├── App.js │ │ ├── iceland.woff2 │ │ └── index.js │ ├── AppSlider │ │ ├── AppSlider.css │ │ ├── AppSlider.js │ │ ├── github.svg │ │ └── index.js │ ├── ClientEditor │ │ ├── ClientEditor.css │ │ ├── ClientEditor.js │ │ └── index.js │ ├── General │ │ ├── General.css │ │ ├── General.js │ │ └── index.js │ ├── Logs │ │ ├── Logs.css │ │ ├── Logs.js │ │ └── index.js │ ├── PacEditor │ │ ├── PacEditor.css │ │ ├── PacEditor.js │ │ └── index.js │ ├── PresetEditor │ │ ├── PresetEditor.css │ │ ├── PresetEditor.js │ │ └── index.js │ ├── QRCode │ │ ├── QRCode.css │ │ ├── QRCode.js │ │ └── index.js │ ├── ServerEditor │ │ ├── ServerEditor.css │ │ ├── ServerEditor.js │ │ └── index.js │ ├── ServerList │ │ ├── ServerList.css │ │ ├── ServerList.js │ │ └── index.js │ └── index.js ├── defs │ ├── bs-config-template.js │ ├── events.js │ └── presets.js ├── helpers │ ├── index.js │ ├── qrcode.js │ ├── screenshot.js │ └── toast.js ├── index.css ├── index.js ├── theme.js └── thirdparty │ └── sudo-agent │ └── src │ ├── lib │ ├── constants.go │ └── executor.go │ └── main.go ├── tasks ├── compress.sh ├── create-patch.sh ├── github-upload.sh ├── release.js ├── sha256sum.sh └── thirdparty-build.sh └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | PUBLIC_URL=. 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /logs 3 | 4 | # See https://help.github.com/ignore-files/ for more about ignoring files. 5 | 6 | # dependencies 7 | /node_modules 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | /releases 15 | 16 | # misc 17 | .DS_Store 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /.smooth-releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "dataType": "issues", 4 | "changelog": { 5 | "outputPath": "./CHANGELOG.md", 6 | "ignoredLabels": [ 7 | "invalid", 8 | "wontfix", 9 | "question", 10 | "duplicate", 11 | "discussion" 12 | ], 13 | "bug": { 14 | "title": "#### Fixes (bugs & defects):", 15 | "labels": [ 16 | "bug", 17 | "defect" 18 | ] 19 | }, 20 | "breaking": { 21 | "title": "#### Breaking Changes:", 22 | "labels": [ 23 | "breaking" 24 | ] 25 | }, 26 | "feature": { 27 | "title": "#### Features:", 28 | "labels": [ 29 | "feature", 30 | "enhancement" 31 | ] 32 | } 33 | } 34 | }, 35 | "publish": { 36 | "branch": "master", 37 | "inSyncWithRemote": true, 38 | "noUncommittedChanges": true, 39 | "noUntrackedFiles": true, 40 | "validNpmCredentials": true, 41 | "validGithubToken": true, 42 | "packageFilesFilter": "files" 43 | }, 44 | "tasks": { 45 | "validations": true, 46 | "npm-publish": null, 47 | "npm-version": null, 48 | "gh-release": null, 49 | "gh-release-all": false, 50 | "changelog": null 51 | } 52 | } -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Micooz 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | 5 | ## [v0.1.3](https://github.com/blinksocks/blinksocks-desktop/tree/v0.1.3) (2017-07-25) 6 | [Full Changelog](https://github.com/blinksocks/blinksocks-desktop/compare/v0.1.2...v0.1.3) 7 | 8 | #### Fixes (bugs & defects): 9 | 10 | - Release 0.1.2 Crashed [#21](https://github.com/blinksocks/blinksocks-desktop/issues/21) 11 | 12 | ## [v0.1.2](https://github.com/blinksocks/blinksocks-desktop/tree/v0.1.2) (2017-06-30) 13 | [Full Changelog](https://github.com/blinksocks/blinksocks-desktop/compare/v0.1.1...v0.1.2) 14 | 15 | #### Features: 16 | 17 | - Refactor sudo agent for darwin using Golang [#20](https://github.com/blinksocks/blinksocks-desktop/issues/20) 18 | 19 | ## [v0.1.1](https://github.com/blinksocks/blinksocks-desktop/tree/v0.1.1) (2017-06-27) 20 | [Full Changelog](https://github.com/blinksocks/blinksocks-desktop/compare/v0.1.0...v0.1.1) 21 | 22 | #### Fixes (bugs & defects): 23 | 24 | - Auto proxy issue on Windows [#19](https://github.com/blinksocks/blinksocks-desktop/issues/19) 25 | - Cannot do incremental update when patch 100% downloaded [#17](https://github.com/blinksocks/blinksocks-desktop/issues/17) 26 | 27 | #### Features: 28 | 29 | - Display total size of patch file when doing incremental update [#16](https://github.com/blinksocks/blinksocks-desktop/issues/16) 30 | 31 | ## [v0.1.0](https://github.com/blinksocks/blinksocks-desktop/tree/v0.1.0) (2017-06-26) 32 | [Full Changelog](https://github.com/blinksocks/blinksocks-desktop/compare/v0.0.5...v0.1.0) 33 | 34 | #### Features: 35 | 36 | - Add QR code support [#15](https://github.com/blinksocks/blinksocks-desktop/issues/15) 37 | 38 | #### Breaking Changes: 39 | 40 | - Allow to custom PAC rules [#14](https://github.com/blinksocks/blinksocks-desktop/issues/14) 41 | 42 | #### Fixes (bugs & defects): 43 | 44 | - Cannot find module '/Applications/blinksocks-desktop.app/Contents/Resources/app.asar/src/backend/system/sudo-agent.js' [#13](https://github.com/blinksocks/blinksocks-desktop/issues/13) 45 | 46 | ## [v0.0.5](https://github.com/blinksocks/blinksocks-desktop/tree/v0.0.5) (2017-06-19) 47 | [Full Changelog](https://github.com/blinksocks/blinksocks-desktop/compare/v0.0.4...v0.0.5) 48 | 49 | #### Features: 50 | 51 | - Update obfs-tls1.2-ticket preset definition [#12](https://github.com/blinksocks/blinksocks-desktop/issues/12) 52 | - Be able to configure "dns" option [#11](https://github.com/blinksocks/blinksocks-desktop/issues/11) 53 | - Support aead-random-cipher in server config dialog [#10](https://github.com/blinksocks/blinksocks-desktop/issues/10) 54 | - Upgrade blinksocks to v2.4.8 [#9](https://github.com/blinksocks/blinksocks-desktop/issues/9) 55 | - Add tray support [#5](https://github.com/blinksocks/blinksocks-desktop/issues/5) 56 | 57 | #### Fixes (bugs & defects): 58 | 59 | - Fail to increment update: Request failed with status code 404 [#8](https://github.com/blinksocks/blinksocks-desktop/issues/8) 60 | 61 | ## [v0.0.4](https://github.com/blinksocks/blinksocks-desktop/tree/v0.0.4) (2017-06-12) 62 | [Full Changelog](https://github.com/blinksocks/blinksocks-desktop/compare/v0.0.3...v0.0.4) 63 | 64 | #### Features: 65 | 66 | - Upgrade blinksocks to v2.4.7 [#7](https://github.com/blinksocks/blinksocks-desktop/issues/7) 67 | - Upgrade blinksocks to v2.4.6 [#6](https://github.com/blinksocks/blinksocks-desktop/issues/6) 68 | - Add live log viewer [#4](https://github.com/blinksocks/blinksocks-desktop/issues/4) 69 | - Upgrade blinksocks to v2.4.5 [#3](https://github.com/blinksocks/blinksocks-desktop/issues/3) 70 | 71 | ## [v0.0.3](https://github.com/blinksocks/blinksocks-desktop/tree/v0.0.3) (2017-06-08) 72 | [Full Changelog](https://github.com/blinksocks/blinksocks-desktop/compare/v0.0.2...v0.0.3) 73 | 74 | #### Features: 75 | 76 | - Add feedback when fail to update PAC [#2](https://github.com/blinksocks/blinksocks-desktop/issues/2) 77 | - Compress .patch file to reduce patch size [#1](https://github.com/blinksocks/blinksocks-desktop/issues/1) 78 | 79 | ## [v0.0.2](https://github.com/blinksocks/blinksocks-desktop/tree/v0.0.2) (2017-06-05) 80 | [Full Changelog](https://github.com/blinksocks/blinksocks-desktop/compare/v0.0.1...v0.0.2) 81 | 82 | ## [v0.0.1](https://github.com/blinksocks/blinksocks-desktop/tree/v0.0.1) (2017-06-04) 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Micooz 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blinksocks-desktop 2 | 3 | [![version](https://img.shields.io/npm/v/blinksocks-desktop.svg)](https://www.npmjs.com/package/blinksocks-desktop) 4 | [![downloads](https://img.shields.io/npm/dt/blinksocks-desktop.svg)](https://www.npmjs.com/package/blinksocks-desktop) 5 | [![license](https://img.shields.io/npm/l/blinksocks-desktop.svg)](https://github.com/blinksocks/blinksocks-desktop/blob/master/LICENSE) 6 | [![dependencies](https://img.shields.io/david/blinksocks/blinksocks.svg)](https://www.npmjs.com/package/blinksocks-desktop) 7 | [![devDependencies](https://img.shields.io/david/dev/blinksocks/blinksocks-desktop.svg)](https://www.npmjs.com/package/blinksocks-desktop) 8 | [![%e2%9d%a4](https://img.shields.io/badge/made%20with-%e2%9d%a4-ff69b4.svg)](https://github.com/blinksocks/blinksocks-desktop) 9 | 10 | > The official GUI for [blinksocks](https://github.com/blinksocks/blinksocks). 11 | 12 | ## Features 13 | 14 | * Cross-platform: Linux, Windows and macOS, ia32/x64 15 | * Portable: no installation and other requirements 16 | * Integrate blinksocks and local PAC service 17 | * QR code generation and scan 18 | * Incremental update 19 | * Material UI 20 | 21 | ## Links 22 | 23 | * [Downloads](RELEASE.md): download the latest released binaries. 24 | * [Documents](docs): usage and development guidance. 25 | 26 | ## ScreenShots 27 | 28 | ![main-screen](docs/screenshots/main-screen.png) 29 | ![client-settings](docs/screenshots/client-settings.png) 30 | ![server-settings](docs/screenshots/server-settings.png) 31 | ![pac-settings](docs/screenshots/pac-settings.png) 32 | ![updating](docs/screenshots/updating.png) 33 | 34 | ## Contributors 35 | 36 | See [authors](AUTHORS). 37 | 38 | ## License 39 | 40 | Apache License 2.0 41 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | [//]: # (THIS IS AN AUTO-GENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.) 2 | 3 | # Release 4 | 5 | | NAME | SHA256 | SIZE | 6 | | :--- | :----- | :--- | 7 | | [blinksocks-desktop-win32-x64-v0.1.3.tar.gz] | d0b6842359b276121c748eb1a765b395f22af917581260c3b62ccd2652ab455f | 53.67 MB | 8 | | [blinksocks-desktop-win32-ia32-v0.1.3.tar.gz] | 6876ecf872765076c371c7c129c1727349be7db73ae854743bef0e26c09b23d1 | 44.62 MB | 9 | | [blinksocks-desktop-linux-x64-v0.1.3.tar.gz] | a7b9a24449b2d0ebb1e6bd771d6d9dc5dfd299f8eca1e9f1255cacd1baee3858 | 51.31 MB | 10 | | [blinksocks-desktop-linux-ia32-v0.1.3.tar.gz] | 8edf289aeac0e47c270442a09eec7be73353793d2344ac9e95acb533665fa122 | 52.37 MB | 11 | | [blinksocks-desktop-darwin-x64-v0.1.3.tar.gz] | 0275d09924ae24a90f96b94dfd55179d26abe5f53c7783cbf75d301618c7fee9 | 47.42 MB | 12 | 13 | Looking for old versions? Please visit [releases](https://github.com/blinksocks/blinksocks-desktop/releases). 14 | 15 | [blinksocks-desktop-win32-x64-v0.1.3.tar.gz]: https://github.com/blinksocks/blinksocks-desktop/releases/download/v0.1.3/blinksocks-desktop-win32-x64-v0.1.3.tar.gz 16 | [blinksocks-desktop-win32-ia32-v0.1.3.tar.gz]: https://github.com/blinksocks/blinksocks-desktop/releases/download/v0.1.3/blinksocks-desktop-win32-ia32-v0.1.3.tar.gz 17 | [blinksocks-desktop-linux-x64-v0.1.3.tar.gz]: https://github.com/blinksocks/blinksocks-desktop/releases/download/v0.1.3/blinksocks-desktop-linux-x64-v0.1.3.tar.gz 18 | [blinksocks-desktop-linux-ia32-v0.1.3.tar.gz]: https://github.com/blinksocks/blinksocks-desktop/releases/download/v0.1.3/blinksocks-desktop-linux-ia32-v0.1.3.tar.gz 19 | [blinksocks-desktop-darwin-x64-v0.1.3.tar.gz]: https://github.com/blinksocks/blinksocks-desktop/releases/download/v0.1.3/blinksocks-desktop-darwin-x64-v0.1.3.tar.gz 20 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | 1. [Usage](usage) 4 | 2. [Development](development) 5 | -------------------------------------------------------------------------------- /docs/development/README.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Architecture 4 | 5 | blinksocks-desktop is based on [Node.js](https://nodejs.org/en/) and [electron](https://electron.atom.io/). 6 | 7 | ![blinksocks-desktop](blinksocks-desktop.png) 8 | 9 | ## Requirements 10 | 11 | We assume that your system has basic unix tools and shell environment. 12 | Apart from that, you should have had the following tools installed: 13 | 14 | * Node.js 6 or later 15 | * OpenSSL 16 | * gzip 17 | * Golang 18 | 19 | ## Quick Start 20 | 21 | First download source code via git: 22 | 23 | ``` 24 | # Clone the repository 25 | $ git clone https://github.com/blinksocks/blinksocks-desktop 26 | 27 | # Go into the repository 28 | $ cd blinksocks-desktop 29 | ``` 30 | 31 | There are several npm scripts(see package.json) used for different purposes: 32 | 33 | ``` 34 | # Install the dependencies and run 35 | $ yarn install 36 | 37 | # Start front-end development server 38 | $ yarn start-dev 39 | 40 | # Open a new terminal then start electron 41 | $ yarn start-app 42 | 43 | # Start electron with devtools 44 | $ yarn start-app -- --devtools 45 | ``` 46 | 47 | ## Build for multiple platforms 48 | 49 | ``` 50 | # Compile front-end sources 51 | $ yarn build 52 | 53 | # Pack, generate patch, gzip then calculate sha256, compile sudo-agent for macOS 54 | $ yarn release 55 | ``` 56 | 57 | ## Trouble Shooting 58 | 59 | 1. Install is too slow and always fail. 60 | 61 | Try to change npm registry and electron mirror to another one: 62 | 63 | ``` 64 | // ~/.npmrc 65 | registry=https://registry.npm.taobao.org/ 66 | electron_mirror="https://npm.taobao.org/mirrors/electron/" 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/development/blinksocks-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/docs/development/blinksocks-desktop.png -------------------------------------------------------------------------------- /docs/screenshots/client-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/docs/screenshots/client-settings.png -------------------------------------------------------------------------------- /docs/screenshots/main-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/docs/screenshots/main-screen.png -------------------------------------------------------------------------------- /docs/screenshots/pac-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/docs/screenshots/pac-settings.png -------------------------------------------------------------------------------- /docs/screenshots/server-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/docs/screenshots/server-settings.png -------------------------------------------------------------------------------- /docs/screenshots/updating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/docs/screenshots/updating.png -------------------------------------------------------------------------------- /docs/usage/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | 1. Download **blinksocks-desktop-[platform]-[arch]-[version].tar.gz** for your platform and architecture. 4 | 2. Decompress it to anywhere you can access to. 5 | 3. Double-Click **blinksocks-desktop.exe**(Windows), **blinksocks-desktop.app**(macOS), **blinksocks-desktop**(Linux). 6 | 7 | ## Notice 8 | 9 | Although blinksocks-desktop is cross-platform, but there are still some differences you need to concern among platforms. 10 | 11 | ## Windows 12 | 13 | All things should work well normally. 14 | 15 | ## macOS 16 | 17 | Manage system settings requires **root** privilege, blinksocks-desktop will ask for credentials once you start 18 | **blinksocks-desktop.app**, if credentials is invalid, it will fallback to manual mode. 19 | 20 | ## Linux 21 | 22 | There is no system-wide approach to manage system proxy among different distributions. So **you must config them manually**. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blinksocks-desktop", 3 | "version": "0.1.3", 4 | "description": "Cross-platform desktop GUI for blinksocks", 5 | "main": "src/backend/main.js", 6 | "files": [], 7 | "scripts": { 8 | "start-dev": "react-scripts start", 9 | "start-app": "cross-env NODE_ENV=development electron .", 10 | "build": "react-scripts build", 11 | "postbuild": "rimraf build/**/**/*.map && npm run build-thirdparty", 12 | "build-thirdparty": "tasks/thirdparty-build.sh", 13 | "test": "react-scripts test --env=jsdom", 14 | "eject": "react-scripts eject", 15 | "prerelease": "rimraf releases/*", 16 | "release": "tasks/release.js" 17 | }, 18 | "dependencies": { 19 | "axios": "^0.16.2", 20 | "blinksocks": "^2.4.8", 21 | "electron-is-dev": "^0.3.0", 22 | "qrcode": "^0.9.0", 23 | "sudo-prompt": "^7.1.1", 24 | "winston": "^2.3.1" 25 | }, 26 | "devDependencies": { 27 | "cross-env": "^5.0.1", 28 | "date-fns": "^1.28.5", 29 | "electron": "^1.6.11", 30 | "electron-packager": "^8.7.2", 31 | "filesize": "^3.5.10", 32 | "flatpickr": "^3.0.6", 33 | "github-markdown-css": "^2.8.0", 34 | "lodash.debounce": "^4.0.8", 35 | "marked": "^0.3.6", 36 | "material-ui": "^0.18.7", 37 | "notie": "^4.3.1", 38 | "prop-types": "^15.5.10", 39 | "qrcode-reader": "^1.0.2", 40 | "react": "^15.6.1", 41 | "react-dom": "^15.6.1", 42 | "react-router-dom": "^4.1.2", 43 | "react-scripts": "^1.0.10", 44 | "react-tap-event-plugin": "^2.0.1", 45 | "rimraf": "^2.6.1" 46 | }, 47 | "author": "Micooz ", 48 | "repository": { 49 | "url": "https://github.com/blinksocks/blinksocks-desktop", 50 | "type": "git" 51 | }, 52 | "bugs": { 53 | "url": "https://github.com/blinksocks/blinksocks-desktop/issues" 54 | }, 55 | "homepage": "https://github.com/blinksocks/blinksocks-desktop", 56 | "keywords": [ 57 | "blinksocks", 58 | "blinksocks-desktop", 59 | "cross-platform", 60 | "gui" 61 | ], 62 | "license": "Apache-2.0", 63 | "engines": { 64 | "node": ">= 6" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/public/assets/images/icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /resources/adp-scripts.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Adblock Plus , 3 | * Copyright (C) 2006-2014 Eyeo GmbH 4 | * 5 | * Adblock Plus is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License version 3 as 7 | * published by the Free Software Foundation. 8 | * 9 | * Adblock Plus is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with Adblock Plus. If not, see . 16 | */ 17 | 18 | function createDict() { 19 | var result = {}; 20 | result.__proto__ = null; 21 | return result; 22 | } 23 | 24 | function getOwnPropertyDescriptor(obj, key) { 25 | if (obj.hasOwnProperty(key)) { 26 | return obj[key]; 27 | } 28 | return null; 29 | } 30 | 31 | function extend(subclass, superclass, definition) { 32 | if (Object.__proto__) { 33 | definition.__proto__ = superclass.prototype; 34 | subclass.prototype = definition; 35 | } 36 | else { 37 | var tmpclass = function () { 38 | }, ret; 39 | tmpclass.prototype = superclass.prototype; 40 | subclass.prototype = new tmpclass(); 41 | subclass.prototype.constructor = superclass; 42 | for (var i in definition) { 43 | if (definition.hasOwnProperty(i)) { 44 | subclass.prototype[i] = definition[i]; 45 | } 46 | } 47 | } 48 | } 49 | 50 | function Filter(text) { 51 | this.text = text; 52 | this.subscriptions = []; 53 | } 54 | Filter.prototype = { 55 | text: null, 56 | subscriptions: null, 57 | toString: function () { 58 | return this.text; 59 | } 60 | }; 61 | Filter.knownFilters = createDict(); 62 | Filter.elemhideRegExp = /^([^\/\*\|\@"!]*?)#(\@)?(?:([\w\-]+|\*)((?:\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\))*)|#([^{}]+))$/; 63 | Filter.regexpRegExp = /^(@@)?\/.*\/(?:\$~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)?$/; 64 | Filter.optionsRegExp = /\$(~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)$/; 65 | Filter.fromText = function (text) { 66 | if (text in Filter.knownFilters) { 67 | return Filter.knownFilters[text]; 68 | } 69 | var ret; 70 | if (text[0] == "!") { 71 | ret = new CommentFilter(text); 72 | } 73 | else { 74 | ret = RegExpFilter.fromText(text); 75 | } 76 | Filter.knownFilters[ret.text] = ret; 77 | return ret; 78 | }; 79 | 80 | function InvalidFilter(text, reason) { 81 | Filter.call(this, text); 82 | this.reason = reason; 83 | } 84 | extend(InvalidFilter, Filter, { 85 | reason: null 86 | }); 87 | 88 | function CommentFilter(text) { 89 | Filter.call(this, text); 90 | } 91 | extend(CommentFilter, Filter, {}); 92 | 93 | function ActiveFilter(text, domains) { 94 | Filter.call(this, text); 95 | this.domainSource = domains; 96 | } 97 | extend(ActiveFilter, Filter, { 98 | domainSource: null, 99 | domainSeparator: null, 100 | ignoreTrailingDot: true, 101 | domainSourceIsUpperCase: false, 102 | getDomains: function () { 103 | var prop = getOwnPropertyDescriptor(this, "domains"); 104 | if (prop) { 105 | return prop; 106 | } 107 | var domains = null; 108 | if (this.domainSource) { 109 | var source = this.domainSource; 110 | if (!this.domainSourceIsUpperCase) { 111 | source = source.toUpperCase(); 112 | } 113 | var list = source.split(this.domainSeparator); 114 | if (list.length == 1 && list[0][0] != "~") { 115 | domains = createDict(); 116 | domains[""] = false; 117 | if (this.ignoreTrailingDot) { 118 | list[0] = list[0].replace(/\.+$/, ""); 119 | } 120 | domains[list[0]] = true; 121 | } 122 | else { 123 | var hasIncludes = false; 124 | for (var i = 0; i < list.length; i++) { 125 | var domain = list[i]; 126 | if (this.ignoreTrailingDot) { 127 | domain = domain.replace(/\.+$/, ""); 128 | } 129 | if (domain == "") { 130 | continue; 131 | } 132 | var include; 133 | if (domain[0] == "~") { 134 | include = false; 135 | domain = domain.substr(1); 136 | } 137 | else { 138 | include = true; 139 | hasIncludes = true; 140 | } 141 | if (!domains) { 142 | domains = createDict(); 143 | } 144 | domains[domain] = include; 145 | } 146 | domains[""] = !hasIncludes; 147 | } 148 | this.domainSource = null; 149 | } 150 | return this.domains; 151 | }, 152 | sitekeys: null, 153 | isActiveOnDomain: function (docDomain, sitekey) { 154 | if (this.getSitekeys() && (!sitekey || this.getSitekeys().indexOf(sitekey.toUpperCase()) < 0)) { 155 | return false; 156 | } 157 | if (!this.getDomains()) { 158 | return true; 159 | } 160 | if (!docDomain) { 161 | return this.getDomains()[""]; 162 | } 163 | if (this.ignoreTrailingDot) { 164 | docDomain = docDomain.replace(/\.+$/, ""); 165 | } 166 | docDomain = docDomain.toUpperCase(); 167 | while (true) { 168 | if (docDomain in this.getDomains()) { 169 | return this.domains[docDomain]; 170 | } 171 | var nextDot = docDomain.indexOf("."); 172 | if (nextDot < 0) { 173 | break; 174 | } 175 | docDomain = docDomain.substr(nextDot + 1); 176 | } 177 | return this.domains[""]; 178 | }, 179 | isActiveOnlyOnDomain: function (docDomain) { 180 | if (!docDomain || !this.getDomains() || this.getDomains()[""]) { 181 | return false; 182 | } 183 | if (this.ignoreTrailingDot) { 184 | docDomain = docDomain.replace(/\.+$/, ""); 185 | } 186 | docDomain = docDomain.toUpperCase(); 187 | for (var domain in this.getDomains()) { 188 | if (this.domains[domain] && domain != docDomain && (domain.length <= docDomain.length || domain.indexOf("." + docDomain) != domain.length - docDomain.length - 1)) { 189 | return false; 190 | } 191 | } 192 | return true; 193 | } 194 | }); 195 | 196 | function RegExpFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys) { 197 | ActiveFilter.call(this, text, domains, sitekeys); 198 | if (contentType != null) { 199 | this.contentType = contentType; 200 | } 201 | if (matchCase) { 202 | this.matchCase = matchCase; 203 | } 204 | if (thirdParty != null) { 205 | this.thirdParty = thirdParty; 206 | } 207 | if (sitekeys != null) { 208 | this.sitekeySource = sitekeys; 209 | } 210 | if (regexpSource.length >= 2 && regexpSource[0] == "/" && regexpSource[regexpSource.length - 1] == "/") { 211 | var regexp = new RegExp(regexpSource.substr(1, regexpSource.length - 2), this.matchCase ? "" : "i"); 212 | this.regexp = regexp; 213 | } 214 | else { 215 | this.regexpSource = regexpSource; 216 | } 217 | } 218 | extend(RegExpFilter, ActiveFilter, { 219 | domainSourceIsUpperCase: true, 220 | length: 1, 221 | domainSeparator: "|", 222 | regexpSource: null, 223 | getRegexp: function () { 224 | var prop = getOwnPropertyDescriptor(this, "regexp"); 225 | if (prop) { 226 | return prop; 227 | } 228 | var source = this.regexpSource.replace(/\*+/g, "*").replace(/\^\|$/, "^").replace(/\W/g, "\\$&").replace(/\\\*/g, ".*").replace(/\\\^/g, "(?:[\\x00-\\x24\\x26-\\x2C\\x2F\\x3A-\\x40\\x5B-\\x5E\\x60\\x7B-\\x7F]|$)").replace(/^\\\|\\\|/, "^[\\w\\-]+:\\/+(?!\\/)(?:[^\\/]+\\.)?").replace(/^\\\|/, "^").replace(/\\\|$/, "$").replace(/^(\.\*)/, "").replace(/(\.\*)$/, ""); 229 | var regexp = new RegExp(source, this.matchCase ? "" : "i"); 230 | this.regexp = regexp; 231 | return regexp; 232 | }, 233 | contentType: 2147483647, 234 | matchCase: false, 235 | thirdParty: null, 236 | sitekeySource: null, 237 | getSitekeys: function () { 238 | var prop = getOwnPropertyDescriptor(this, "sitekeys"); 239 | if (prop) { 240 | return prop; 241 | } 242 | var sitekeys = null; 243 | if (this.sitekeySource) { 244 | sitekeys = this.sitekeySource.split("|"); 245 | this.sitekeySource = null; 246 | } 247 | this.sitekeys = sitekeys; 248 | return this.sitekeys; 249 | }, 250 | matches: function (location, contentType, docDomain, thirdParty, sitekey) { 251 | if (this.getRegexp().test(location) && this.isActiveOnDomain(docDomain, sitekey)) { 252 | return true; 253 | } 254 | return false; 255 | } 256 | }); 257 | RegExpFilter.prototype["0"] = "#this"; 258 | RegExpFilter.fromText = function (text) { 259 | var blocking = true; 260 | var origText = text; 261 | if (text.indexOf("@@") == 0) { 262 | blocking = false; 263 | text = text.substr(2); 264 | } 265 | var contentType = null; 266 | var matchCase = null; 267 | var domains = null; 268 | var sitekeys = null; 269 | var thirdParty = null; 270 | var collapse = null; 271 | var options; 272 | var match = text.indexOf("$") >= 0 ? Filter.optionsRegExp.exec(text) : null; 273 | if (match) { 274 | options = match[1].toUpperCase().split(","); 275 | text = match.input.substr(0, match.index); 276 | for (var _loopIndex6 = 0; _loopIndex6 < options.length; ++_loopIndex6) { 277 | var option = options[_loopIndex6]; 278 | var value = null; 279 | var separatorIndex = option.indexOf("="); 280 | if (separatorIndex >= 0) { 281 | value = option.substr(separatorIndex + 1); 282 | option = option.substr(0, separatorIndex); 283 | } 284 | option = option.replace(/-/, "_"); 285 | if (option in RegExpFilter.typeMap) { 286 | if (contentType == null) { 287 | contentType = 0; 288 | } 289 | contentType |= RegExpFilter.typeMap[option]; 290 | } 291 | else if (option[0] == "~" && option.substr(1) in RegExpFilter.typeMap) { 292 | if (contentType == null) { 293 | contentType = RegExpFilter.prototype.contentType; 294 | } 295 | contentType &= ~RegExpFilter.typeMap[option.substr(1)]; 296 | } 297 | else if (option == "MATCH_CASE") { 298 | matchCase = true; 299 | } 300 | else if (option == "~MATCH_CASE") { 301 | matchCase = false; 302 | } 303 | else if (option == "DOMAIN" && typeof value != "undefined") { 304 | domains = value; 305 | } 306 | else if (option == "THIRD_PARTY") { 307 | thirdParty = true; 308 | } 309 | else if (option == "~THIRD_PARTY") { 310 | thirdParty = false; 311 | } 312 | else if (option == "COLLAPSE") { 313 | collapse = true; 314 | } 315 | else if (option == "~COLLAPSE") { 316 | collapse = false; 317 | } 318 | else if (option == "SITEKEY" && typeof value != "undefined") { 319 | sitekeys = value; 320 | } 321 | else { 322 | return new InvalidFilter(origText, "Unknown option " + option.toLowerCase()); 323 | } 324 | } 325 | } 326 | if (!blocking && (contentType == null || contentType & RegExpFilter.typeMap.DOCUMENT) && (!options || options.indexOf("DOCUMENT") < 0) && !/^\|?[\w\-]+:/.test(text)) { 327 | if (contentType == null) { 328 | contentType = RegExpFilter.prototype.contentType; 329 | } 330 | contentType &= ~RegExpFilter.typeMap.DOCUMENT; 331 | } 332 | try { 333 | if (blocking) { 334 | return new BlockingFilter(origText, text, contentType, matchCase, domains, thirdParty, sitekeys, collapse); 335 | } 336 | else { 337 | return new WhitelistFilter(origText, text, contentType, matchCase, domains, thirdParty, sitekeys); 338 | } 339 | } 340 | catch (e) { 341 | return new InvalidFilter(origText, e); 342 | } 343 | }; 344 | RegExpFilter.typeMap = { 345 | OTHER: 1, 346 | SCRIPT: 2, 347 | IMAGE: 4, 348 | STYLESHEET: 8, 349 | OBJECT: 16, 350 | SUBDOCUMENT: 32, 351 | DOCUMENT: 64, 352 | XBL: 1, 353 | PING: 1, 354 | XMLHTTPREQUEST: 2048, 355 | OBJECT_SUBREQUEST: 4096, 356 | DTD: 1, 357 | MEDIA: 16384, 358 | FONT: 32768, 359 | BACKGROUND: 4, 360 | POPUP: 268435456, 361 | ELEMHIDE: 1073741824 362 | }; 363 | RegExpFilter.prototype.contentType &= ~(RegExpFilter.typeMap.ELEMHIDE | RegExpFilter.typeMap.POPUP); 364 | 365 | function BlockingFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys, collapse) { 366 | RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys); 367 | this.collapse = collapse; 368 | } 369 | extend(BlockingFilter, RegExpFilter, { 370 | collapse: null 371 | }); 372 | 373 | function WhitelistFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys) { 374 | RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys); 375 | } 376 | extend(WhitelistFilter, RegExpFilter, {}); 377 | 378 | function Matcher() { 379 | this.clear(); 380 | } 381 | Matcher.prototype = { 382 | filterByKeyword: null, 383 | keywordByFilter: null, 384 | clear: function () { 385 | this.filterByKeyword = createDict(); 386 | this.keywordByFilter = createDict(); 387 | }, 388 | add: function (filter) { 389 | if (filter.text in this.keywordByFilter) { 390 | return; 391 | } 392 | var keyword = this.findKeyword(filter); 393 | var oldEntry = this.filterByKeyword[keyword]; 394 | if (typeof oldEntry == "undefined") { 395 | this.filterByKeyword[keyword] = filter; 396 | } 397 | else if (oldEntry.length == 1) { 398 | this.filterByKeyword[keyword] = [oldEntry, filter]; 399 | } 400 | else { 401 | oldEntry.push(filter); 402 | } 403 | this.keywordByFilter[filter.text] = keyword; 404 | }, 405 | remove: function (filter) { 406 | if (!(filter.text in this.keywordByFilter)) { 407 | return; 408 | } 409 | var keyword = this.keywordByFilter[filter.text]; 410 | var list = this.filterByKeyword[keyword]; 411 | if (list.length <= 1) { 412 | delete this.filterByKeyword[keyword]; 413 | } 414 | else { 415 | var index = list.indexOf(filter); 416 | if (index >= 0) { 417 | list.splice(index, 1); 418 | if (list.length == 1) { 419 | this.filterByKeyword[keyword] = list[0]; 420 | } 421 | } 422 | } 423 | delete this.keywordByFilter[filter.text]; 424 | }, 425 | findKeyword: function (filter) { 426 | var result = ""; 427 | var text = filter.text; 428 | if (Filter.regexpRegExp.test(text)) { 429 | return result; 430 | } 431 | var match = Filter.optionsRegExp.exec(text); 432 | if (match) { 433 | text = match.input.substr(0, match.index); 434 | } 435 | if (text.substr(0, 2) == "@@") { 436 | text = text.substr(2); 437 | } 438 | var candidates = text.toLowerCase().match(/[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g); 439 | if (!candidates) { 440 | return result; 441 | } 442 | var hash = this.filterByKeyword; 443 | var resultCount = 16777215; 444 | var resultLength = 0; 445 | for (var i = 0, l = candidates.length; i < l; i++) { 446 | var candidate = candidates[i].substr(1); 447 | var count = candidate in hash ? hash[candidate].length : 0; 448 | if (count < resultCount || count == resultCount && candidate.length > resultLength) { 449 | result = candidate; 450 | resultCount = count; 451 | resultLength = candidate.length; 452 | } 453 | } 454 | return result; 455 | }, 456 | hasFilter: function (filter) { 457 | return filter.text in this.keywordByFilter; 458 | }, 459 | getKeywordForFilter: function (filter) { 460 | if (filter.text in this.keywordByFilter) { 461 | return this.keywordByFilter[filter.text]; 462 | } 463 | else { 464 | return null; 465 | } 466 | }, 467 | _checkEntryMatch: function (keyword, location, contentType, docDomain, thirdParty, sitekey) { 468 | var list = this.filterByKeyword[keyword]; 469 | for (var i = 0; i < list.length; i++) { 470 | var filter = list[i]; 471 | if (filter == "#this") { 472 | filter = list; 473 | } 474 | if (filter.matches(location, contentType, docDomain, thirdParty, sitekey)) { 475 | return filter; 476 | } 477 | } 478 | return null; 479 | }, 480 | matchesAny: function (location, contentType, docDomain, thirdParty, sitekey) { 481 | var candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g); 482 | if (candidates === null) { 483 | candidates = []; 484 | } 485 | candidates.push(""); 486 | for (var i = 0, l = candidates.length; i < l; i++) { 487 | var substr = candidates[i]; 488 | if (substr in this.filterByKeyword) { 489 | var result = this._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, sitekey); 490 | if (result) { 491 | return result; 492 | } 493 | } 494 | } 495 | return null; 496 | } 497 | }; 498 | 499 | function CombinedMatcher() { 500 | this.blacklist = new Matcher(); 501 | this.whitelist = new Matcher(); 502 | this.resultCache = createDict(); 503 | } 504 | CombinedMatcher.maxCacheEntries = 1000; 505 | CombinedMatcher.prototype = { 506 | blacklist: null, 507 | whitelist: null, 508 | resultCache: null, 509 | cacheEntries: 0, 510 | clear: function () { 511 | this.blacklist.clear(); 512 | this.whitelist.clear(); 513 | this.resultCache = createDict(); 514 | this.cacheEntries = 0; 515 | }, 516 | add: function (filter) { 517 | if (filter instanceof WhitelistFilter) { 518 | this.whitelist.add(filter); 519 | } 520 | else { 521 | this.blacklist.add(filter); 522 | } 523 | if (this.cacheEntries > 0) { 524 | this.resultCache = createDict(); 525 | this.cacheEntries = 0; 526 | } 527 | }, 528 | remove: function (filter) { 529 | if (filter instanceof WhitelistFilter) { 530 | this.whitelist.remove(filter); 531 | } 532 | else { 533 | this.blacklist.remove(filter); 534 | } 535 | if (this.cacheEntries > 0) { 536 | this.resultCache = createDict(); 537 | this.cacheEntries = 0; 538 | } 539 | }, 540 | findKeyword: function (filter) { 541 | if (filter instanceof WhitelistFilter) { 542 | return this.whitelist.findKeyword(filter); 543 | } 544 | else { 545 | return this.blacklist.findKeyword(filter); 546 | } 547 | }, 548 | hasFilter: function (filter) { 549 | if (filter instanceof WhitelistFilter) { 550 | return this.whitelist.hasFilter(filter); 551 | } 552 | else { 553 | return this.blacklist.hasFilter(filter); 554 | } 555 | }, 556 | getKeywordForFilter: function (filter) { 557 | if (filter instanceof WhitelistFilter) { 558 | return this.whitelist.getKeywordForFilter(filter); 559 | } 560 | else { 561 | return this.blacklist.getKeywordForFilter(filter); 562 | } 563 | }, 564 | isSlowFilter: function (filter) { 565 | var matcher = filter instanceof WhitelistFilter ? this.whitelist : this.blacklist; 566 | if (matcher.hasFilter(filter)) { 567 | return !matcher.getKeywordForFilter(filter); 568 | } 569 | else { 570 | return !matcher.findKeyword(filter); 571 | } 572 | }, 573 | matchesAnyInternal: function (location, contentType, docDomain, thirdParty, sitekey) { 574 | var candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g); 575 | if (candidates === null) { 576 | candidates = []; 577 | } 578 | candidates.push(""); 579 | var blacklistHit = null; 580 | for (var i = 0, l = candidates.length; i < l; i++) { 581 | var substr = candidates[i]; 582 | if (substr in this.whitelist.filterByKeyword) { 583 | var result = this.whitelist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, sitekey); 584 | if (result) { 585 | return result; 586 | } 587 | } 588 | if (substr in this.blacklist.filterByKeyword && blacklistHit === null) { 589 | blacklistHit = this.blacklist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, sitekey); 590 | } 591 | } 592 | return blacklistHit; 593 | }, 594 | matchesAny: function (location, docDomain) { 595 | var key = location + " " + docDomain + " "; 596 | if (key in this.resultCache) { 597 | return this.resultCache[key]; 598 | } 599 | var result = this.matchesAnyInternal(location, 0, docDomain, null, null); 600 | if (this.cacheEntries >= CombinedMatcher.maxCacheEntries) { 601 | this.resultCache = createDict(); 602 | this.cacheEntries = 0; 603 | } 604 | this.resultCache[key] = result; 605 | this.cacheEntries++; 606 | return result; 607 | } 608 | }; 609 | -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/resources/icon.icns -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/resources/icon.png -------------------------------------------------------------------------------- /resources/proxy_conf_helper.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/resources/proxy_conf_helper.gz -------------------------------------------------------------------------------- /resources/sudo-agent_darwin_x64.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/resources/sudo-agent_darwin_x64.gz -------------------------------------------------------------------------------- /resources/sysproxy.exe.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/resources/sysproxy.exe.gz -------------------------------------------------------------------------------- /resources/sysproxy64.exe.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/resources/sysproxy64.exe.gz -------------------------------------------------------------------------------- /resources/tray-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/resources/tray-icon.ico -------------------------------------------------------------------------------- /resources/tray-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/resources/tray-icon.png -------------------------------------------------------------------------------- /src/backend/constants.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const url = require('url'); 4 | const isProduction = !require('electron-is-dev'); 5 | 6 | // development 7 | const DEV_ADDRESS = 'http://localhost:3000'; 8 | 9 | const APP_HOME = path.resolve(__dirname, '..', '..'); 10 | const HOME_DIR = os.homedir(); 11 | 12 | const APP_ICON = path.join(APP_HOME, 'resources', 'icon.png'); 13 | const APP_TRAY_ICON = path.join(APP_HOME, 'resources', { 14 | 'win32': 'tray-icon.ico', 15 | 'darwin': 'tray-icon.png', 16 | 'linux': 'tray-icon.png' 17 | }[process.platform]); 18 | 19 | const BLINKSOCKS_DIR = path.join(HOME_DIR, '.blinksocks'); 20 | const LOG_DIR = path.join(BLINKSOCKS_DIR, 'logs'); 21 | const LOG_FILE_PATH = path.join(LOG_DIR, 'blinksocks-desktop.log'); 22 | const DEFAULT_GFWLIST_PATH = path.join(BLINKSOCKS_DIR, 'gfwlist.txt'); 23 | const DEFAULT_CONFIG_FILE = path.join(BLINKSOCKS_DIR, 'blinksocks.client.js'); 24 | const BUILT_IN_GFWLIST_PATH = path.join(APP_HOME, 'resources', 'gfwlist.txt'); 25 | const BUILT_IN_ADP_SCRIPTS_PATH = path.join(APP_HOME, 'resources', 'adp-scripts.js'); 26 | 27 | // darwin 28 | const DARWIN_BUILT_IN_SUDO_AGENT_GZ_PATH = path.join(APP_HOME, 'resources', 'sudo-agent_darwin_x64.gz'); 29 | const DARWIN_SUDO_AGENT_PATH = path.join(BLINKSOCKS_DIR, 'sudo-agent'); 30 | const DARWIN_SUDO_AGENT_PORT_FILE = path.join(BLINKSOCKS_DIR, '.sudo-agent-port'); 31 | const DARWIN_BUILT_IN_SYSPROXY_GZ_PATH = path.join(APP_HOME, 'resources', 'proxy_conf_helper.gz'); 32 | const DARWIN_SYSPROXY_HELPER = path.join(BLINKSOCKS_DIR, 'proxy_conf_helper'); 33 | 34 | // win32 35 | const WIN32_BUILT_IN_SYSPROXY_GZ_PATH = { 36 | 'ia32': path.join(APP_HOME, 'resources', 'sysproxy.exe.gz'), 37 | 'x64': path.join(APP_HOME, 'resources', 'sysproxy64.exe.gz') 38 | }[process.arch]; 39 | 40 | const WIN32_SYSPROXY_HELPER = { 41 | 'ia32': path.join(BLINKSOCKS_DIR, 'sysproxy.exe'), 42 | 'x64': path.join(BLINKSOCKS_DIR, 'sysproxy64.exe') 43 | }[process.arch]; 44 | 45 | // app 46 | 47 | const APP_MAIN_URL = isProduction ? 48 | `file://${path.join(__dirname, '..', '..', 'build/index.html')}#/main` : 49 | `${DEV_ADDRESS}/#/main`; 50 | 51 | const APP_LOG_URL = isProduction ? 52 | `file://${path.join(__dirname, '..', '..', 'build/index.html')}#/logs` : 53 | `${DEV_ADDRESS}/#/logs`; 54 | 55 | module.exports = { 56 | HOME_DIR, 57 | BLINKSOCKS_DIR, 58 | LOG_DIR, 59 | LOG_FILE_PATH, 60 | APP_ICON, 61 | APP_TRAY_ICON, 62 | APP_HOME, 63 | APP_MAIN_URL, 64 | APP_LOG_URL, 65 | DEFAULT_GFWLIST_PATH, 66 | DEFAULT_CONFIG_FILE, 67 | BUILT_IN_GFWLIST_PATH, 68 | BUILT_IN_ADP_SCRIPTS_PATH, 69 | DARWIN_BUILT_IN_SUDO_AGENT_GZ_PATH, 70 | DARWIN_SUDO_AGENT_PATH, 71 | DARWIN_SUDO_AGENT_PORT_FILE, 72 | DARWIN_SYSPROXY_HELPER, 73 | DARWIN_BUILT_IN_SYSPROXY_GZ_PATH, 74 | WIN32_BUILT_IN_SYSPROXY_GZ_PATH, 75 | WIN32_SYSPROXY_HELPER, 76 | GFWLIST_URL: 'https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt', 77 | RELEASES_URL: 'https://github.com/blinksocks/blinksocks-desktop/releases' 78 | }; 79 | -------------------------------------------------------------------------------- /src/backend/helpers/fs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const zlib = require('zlib'); 3 | const logger = require('../helpers/logger'); 4 | const existsSync = fs.existsSync; 5 | const chmodSync = fs.chmodSync; 6 | 7 | async function lstat(target) { 8 | return new Promise((resolve, reject) => { 9 | fs.lstat(target, (err, stats) => { 10 | if (err) { 11 | reject(err); 12 | } else { 13 | resolve(stats); 14 | } 15 | }); 16 | }); 17 | } 18 | 19 | async function unzip(from, to) { 20 | try { 21 | await lstat(from); 22 | 23 | const gzip = zlib.createUnzip(); 24 | const inp = fs.createReadStream(from); 25 | const out = fs.createWriteStream(to); 26 | inp.pipe(gzip).pipe(out); 27 | 28 | } catch (err) { 29 | logger.error(err); 30 | } 31 | } 32 | 33 | function mkdirSync(dir) { 34 | try { 35 | fs.lstatSync(dir); 36 | } catch (err) { 37 | if (err.code === 'ENOENT') { 38 | fs.mkdirSync(dir); 39 | } 40 | } 41 | } 42 | 43 | function copySync(from, to) { 44 | if (fs.existsSync(from)) { 45 | const inp = fs.createReadStream(from); 46 | const out = fs.createWriteStream(to); 47 | inp.pipe(out); 48 | } 49 | } 50 | 51 | module.exports = {lstat, unzip, mkdirSync, copySync, existsSync, chmodSync}; 52 | -------------------------------------------------------------------------------- /src/backend/helpers/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | const {LOG_FILE_PATH} = require('../constants'); 3 | 4 | module.exports = new (winston.Logger)({ 5 | level: 'silly', 6 | transports: [ 7 | new (winston.transports.Console)({ 8 | colorize: true, 9 | prettyPrint: true 10 | }), 11 | new (winston.transports.File)({ 12 | filename: LOG_FILE_PATH, 13 | maxsize: 2 * 1024 * 1024, // 2MB 14 | }) 15 | ] 16 | }); 17 | -------------------------------------------------------------------------------- /src/backend/helpers/shell.js: -------------------------------------------------------------------------------- 1 | const child_process = require('child_process'); 2 | const logger = require('./logger'); 3 | 4 | /** 5 | * Promised child_process.exec() 6 | * @param command 7 | * @param options 8 | * @param onCreated 9 | * @returns {Promise} 10 | */ 11 | function exec(command, options = {}, onCreated = null) { 12 | return new Promise((resolve, reject) => { 13 | const opts = Object.assign({ 14 | encoding: 'utf-8' 15 | }, options); 16 | logger.debug(`[shell] executing: ${command}`); 17 | const child = child_process.exec(command, opts, function (error, stdout, stderr) { 18 | if (error) { 19 | reject({code: error.code, stdout, stderr}); 20 | } else { 21 | resolve({code: 0, stdout, stderr}); 22 | } 23 | }); 24 | if (typeof onCreated === 'function') { 25 | onCreated(child); 26 | } 27 | }); 28 | } 29 | 30 | module.exports = { 31 | exec 32 | }; 33 | -------------------------------------------------------------------------------- /src/backend/init.js: -------------------------------------------------------------------------------- 1 | const {unzip, mkdirSync, copySync, existsSync, chmodSync} = require('./helpers/fs'); 2 | const { 3 | BLINKSOCKS_DIR, 4 | LOG_DIR, 5 | DEFAULT_GFWLIST_PATH, 6 | BUILT_IN_GFWLIST_PATH, 7 | DARWIN_BUILT_IN_SYSPROXY_GZ_PATH, 8 | DARWIN_SYSPROXY_HELPER, 9 | DARWIN_BUILT_IN_SUDO_AGENT_GZ_PATH, 10 | DARWIN_SUDO_AGENT_PATH, 11 | WIN32_SYSPROXY_HELPER, 12 | WIN32_BUILT_IN_SYSPROXY_GZ_PATH 13 | } = require('./constants'); 14 | 15 | module.exports = function init() { 16 | // create ~/.blinksocks directory if not exist 17 | mkdirSync(BLINKSOCKS_DIR); 18 | 19 | // create ~/.blinksocks/logs if not exist 20 | mkdirSync(LOG_DIR); 21 | 22 | // create ~/.blinksocks/gfwlist.txt if not exist 23 | if (!existsSync(DEFAULT_GFWLIST_PATH)) { 24 | copySync(BUILT_IN_GFWLIST_PATH, DEFAULT_GFWLIST_PATH); 25 | } 26 | 27 | // overwrite ~/proxy_conf_helper and ~/sudo-agent_darwin_x64 for macOS 28 | if (process.platform === 'darwin') { 29 | unzip(DARWIN_BUILT_IN_SYSPROXY_GZ_PATH, DARWIN_SYSPROXY_HELPER); 30 | unzip(DARWIN_BUILT_IN_SUDO_AGENT_GZ_PATH, DARWIN_SUDO_AGENT_PATH); 31 | chmodSync(DARWIN_SYSPROXY_HELPER, 0o755); 32 | chmodSync(DARWIN_SUDO_AGENT_PATH, 0o755); 33 | } 34 | 35 | // overwrite ~/sysproxy(64).exe for Windows 36 | if (process.platform === 'win32') { 37 | unzip(WIN32_BUILT_IN_SYSPROXY_GZ_PATH, WIN32_SYSPROXY_HELPER); 38 | chmodSync(WIN32_SYSPROXY_HELPER, 0o755); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/backend/main.js: -------------------------------------------------------------------------------- 1 | require('./init')(); 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const {app, shell, BrowserWindow, ipcMain, Tray, Menu} = require('electron'); 6 | const isProduction = !require('electron-is-dev'); 7 | const bsLogger = require('blinksocks').logger; 8 | const logger = require('./helpers/logger'); 9 | const {DEFAULT_CONFIG_STRUCTURE} = require('../defs/bs-config-template'); 10 | 11 | const { 12 | MAIN_INIT, 13 | MAIN_ERROR, 14 | RENDERER_INIT, 15 | RENDERER_QUIT, 16 | RENDERER_START_BS, 17 | RENDERER_STOP_BS, 18 | RENDERER_START_PAC, 19 | RENDERER_STOP_PAC, 20 | RENDERER_SET_SYS_PROXY, 21 | RENDERER_SET_SYS_PAC, 22 | RENDERER_RESTORE_SYS_PROXY, 23 | RENDERER_RESTORE_SYS_PAC, 24 | RENDERER_SAVE_CONFIG, 25 | RENDERER_PREVIEW_LOGS 26 | } = require('../defs/events'); 27 | 28 | const packageJson = require('../../package.json'); 29 | 30 | const {createSysProxy} = require('./system/create'); 31 | 32 | const { 33 | APP_ICON, 34 | APP_TRAY_ICON, 35 | APP_MAIN_URL, 36 | APP_LOG_URL, 37 | DEFAULT_GFWLIST_PATH, 38 | DEFAULT_CONFIG_FILE 39 | } = require('./constants'); 40 | 41 | // Keep a global reference of the window object, if you don't, the window will 42 | // be closed automatically when the JavaScript object is garbage collected. 43 | let mainWindow = null; 44 | let logWindow = null; 45 | let tray = null; 46 | let config; 47 | let sysProxy; 48 | let directModuleCall = null; 49 | 50 | function loadConfig() { 51 | let json; 52 | // resolve to absolute path 53 | const file = DEFAULT_CONFIG_FILE; 54 | logger.info(`loading configuration from ${file}`); 55 | try { 56 | const ext = path.extname(file); 57 | if (ext === '.js') { 58 | // require .js directly 59 | delete require.cache[require.resolve(file)]; 60 | json = require(file); 61 | } else { 62 | // others are treated as .json 63 | const jsonFile = fs.readFileSync(file); 64 | json = JSON.parse(jsonFile); 65 | } 66 | if (Object.keys(json).length < 1) { 67 | logger.warn(`empty ${DEFAULT_CONFIG_FILE} detected, use DEFAULT_CONFIG_STRUCTURE instead`); 68 | json = DEFAULT_CONFIG_STRUCTURE; 69 | } 70 | } catch (err) { 71 | if (err.code === 'ENOENT' || err.code === 'MODULE_NOT_FOUND') { 72 | saveConfig(DEFAULT_CONFIG_STRUCTURE); 73 | } 74 | logger.warn(`fail to load or parse: ${DEFAULT_CONFIG_FILE}, use DEFAULT_CONFIG_STRUCTURE instead`); 75 | return DEFAULT_CONFIG_STRUCTURE; 76 | } 77 | return json; 78 | } 79 | 80 | function saveConfig(json) { 81 | const data = `module.exports = ${JSON.stringify(json, null, ' ')};`; 82 | fs.writeFile(DEFAULT_CONFIG_FILE, data, (err) => { 83 | if (err) { 84 | logger.error(err); 85 | } 86 | }); 87 | logger.info('saving configuration'); 88 | } 89 | 90 | function getPacUrl() { 91 | const {pac_type, pac_host, pac_port, pac_remote_url} = config; 92 | return pac_type === 0 ? `http://${pac_host || 'localhost'}:${pac_port || 1090}` : (pac_remote_url || ''); 93 | } 94 | 95 | function onAppClose() { 96 | // 1. restore all system settings 97 | if (sysProxy) { 98 | const restores = [ 99 | sysProxy.restoreGlobal({ 100 | host: config.host, 101 | port: config.port, 102 | bypass: config.bypass 103 | }), 104 | sysProxy.restorePAC({url: getPacUrl()}) 105 | ]; 106 | Promise.all(restores).then(() => null); 107 | 108 | // shutdown sudo agent if on darwin 109 | if (process.platform === 'darwin' && typeof sysProxy.kill === 'function') { 110 | sysProxy.kill(); 111 | } 112 | sysProxy = null; 113 | } 114 | // 2. save config 115 | saveConfig(Object.assign({}, config, {app_status: 0, pac_status: 0})); 116 | // 3. quit app 117 | app.exit(0); 118 | } 119 | 120 | function createWindow() { 121 | // Create the browser window. 122 | mainWindow = new BrowserWindow({ 123 | title: `${packageJson.name} v${packageJson.version}`, 124 | icon: APP_ICON, 125 | width: 380, 126 | height: 620, 127 | minWidth: 380, 128 | minHeight: 620, 129 | show: false 130 | }); 131 | 132 | // and load the index.html of the app. 133 | mainWindow.loadURL(APP_MAIN_URL); 134 | 135 | // Open the DevTools. 136 | if (!isProduction && process.argv[2] === '--devtools') { 137 | mainWindow.webContents.openDevTools(); 138 | } 139 | 140 | mainWindow.webContents.on('new-window', function (e, url) { 141 | e.preventDefault(); 142 | shell.openExternal(url); 143 | }); 144 | 145 | if (isProduction) { 146 | mainWindow.webContents.on('will-navigate', function (e, url) { 147 | e.preventDefault(); 148 | shell.openExternal(url); 149 | }); 150 | } 151 | 152 | mainWindow.on('ready-to-show', () => mainWindow.show()); 153 | 154 | mainWindow.on('close', (e) => { 155 | if (tray !== null) { 156 | // balloon only available on Windows 157 | // https://electron.atom.io/docs/api/tray/#traydisplayballoonoptions-windows 158 | tray.displayBalloon({ 159 | title: 'blinksocks-desktop', 160 | content: 'blinksocks-desktop is running at background' 161 | }); 162 | } 163 | }); 164 | 165 | // Emitted when the window is closed. 166 | mainWindow.on('closed', () => { 167 | // Dereference the window object, usually you would store windows 168 | // in an array if your app supports multi windows, this is the time 169 | // when you should delete the corresponding element. 170 | mainWindow = null; 171 | if (logWindow !== null) { 172 | logWindow.close(); 173 | } 174 | }); 175 | } 176 | 177 | function registerIPC(handlers) { 178 | const events = Object.keys(handlers); 179 | for (const event of events) { 180 | ipcMain.on(event, (e, ...args) => { 181 | // an wrapper for e.sender.send 182 | const push = (name, ...args2) => { 183 | try { 184 | e.sender.send(name, ...args2); 185 | } catch (err) { 186 | // just ignore any errors 187 | } 188 | }; 189 | handlers[event](push, ...args); 190 | }); 191 | } 192 | } 193 | 194 | /** 195 | * initialize module ipc 196 | */ 197 | function rendererReady({push}) { 198 | const moduleHandlers = Object.assign( 199 | {}, 200 | require('./modules/sys')({sysProxy}), 201 | require('./modules/bs')({ 202 | onStatusChange: (isRunning) => updateContextMenu({ 203 | id: TRAY_MENU_ITEM_APP, 204 | props: { 205 | label: `App Status: ${isRunning ? 'On' : 'Off'}` 206 | } 207 | }) 208 | }), 209 | require('./modules/pac')({ 210 | onStatusChange: (isRunning) => updateContextMenu({ 211 | id: TRAY_MENU_ITEM_PAC, 212 | props: { 213 | label: `PAC Status: ${isRunning ? 'On' : 'Off'}` 214 | } 215 | }) 216 | }), 217 | require('./modules/update')({app}), 218 | require('./modules/log')({bsLogger: bsLogger, bsdLogger: logger}), 219 | require('./modules/qrcode')() 220 | ); 221 | // create directModuleCall method, so that we can call ipcHandlers directly 222 | directModuleCall = (name, ...args) => moduleHandlers[name](push, ...args); 223 | registerIPC(moduleHandlers); 224 | } 225 | 226 | // menu stuff 227 | 228 | const TRAY_MENU_ITEM_SHOW_APPLICATION = 0; 229 | const TRAY_MENU_ITEM_SEPARATOR_1 = 1; 230 | const TRAY_MENU_ITEM_APP = 2; 231 | const TRAY_MENU_ITEM_PAC = 3; 232 | const TRAY_MENU_ITEM_SEPARATOR_2 = 4; 233 | const TRAY_MENU_ITEM_QUIT = 5; 234 | 235 | let menuItems = { 236 | [TRAY_MENU_ITEM_SHOW_APPLICATION]: { 237 | type: 'normal', 238 | label: 'Open Application', 239 | click: onMenuItemShowApplication 240 | }, 241 | [TRAY_MENU_ITEM_SEPARATOR_1]: { 242 | type: 'separator' 243 | }, 244 | [TRAY_MENU_ITEM_APP]: { 245 | type: 'normal', 246 | label: 'App Status: Off', 247 | sublabel: 'blinksocks client', 248 | click: onMenuItemToggleAppService 249 | }, 250 | [TRAY_MENU_ITEM_PAC]: { 251 | type: 'normal', 252 | label: 'PAC Status: Off', 253 | sublabel: 'proxy auto configure service', 254 | click: onMenuItemTogglePacService 255 | }, 256 | [TRAY_MENU_ITEM_SEPARATOR_2]: { 257 | type: 'separator' 258 | }, 259 | [TRAY_MENU_ITEM_QUIT]: { 260 | type: 'normal', 261 | label: 'Quit', 262 | click: onAppClose 263 | } 264 | }; 265 | 266 | function onMenuItemShowApplication() { 267 | if (mainWindow === null) { 268 | createWindow(); 269 | } else { 270 | mainWindow.show(); 271 | } 272 | } 273 | 274 | function onMenuItemToggleAppService() { 275 | const {app_status, pac_status} = config; 276 | const {host, port, bypass} = config; 277 | const url = getPacUrl(); 278 | if (app_status === 0) { 279 | directModuleCall(RENDERER_START_BS, {config}); 280 | if (pac_status === 0) { 281 | directModuleCall(RENDERER_SET_SYS_PROXY, {host, port, bypass}); 282 | } else { 283 | directModuleCall(RENDERER_SET_SYS_PAC, {url}); 284 | } 285 | config.app_status = 1; 286 | saveConfig(config); 287 | } else { 288 | directModuleCall(RENDERER_STOP_BS); 289 | directModuleCall(RENDERER_RESTORE_SYS_PROXY, {host, port, bypass}); 290 | directModuleCall(RENDERER_RESTORE_SYS_PAC, {url}); 291 | config.app_status = 0; 292 | saveConfig(config); 293 | } 294 | } 295 | 296 | function onMenuItemTogglePacService() { 297 | const {app_status, pac_status} = config; 298 | const {host, port, bypass, pac_type, pac_host, pac_port} = config; 299 | if (pac_status === 0) { 300 | directModuleCall(RENDERER_START_PAC, { 301 | type: pac_type, 302 | host: pac_host, 303 | port: pac_port, 304 | proxyHost: config.host, 305 | proxyPort: config.port, 306 | customRules: config.pac_custom_rules 307 | }); 308 | if (app_status === 1) { 309 | directModuleCall(RENDERER_SET_SYS_PAC, {url: getPacUrl()}); 310 | } 311 | config.pac_status = 1; 312 | saveConfig(config); 313 | } else { 314 | directModuleCall(RENDERER_STOP_PAC); 315 | if (app_status === 1) { 316 | directModuleCall(RENDERER_SET_SYS_PROXY, {host, port, bypass}); 317 | } 318 | config.pac_status = 0; 319 | saveConfig(config); 320 | } 321 | } 322 | 323 | function updateContextMenu(updates = [/* {id, props}, ... */]) { 324 | if (tray !== null) { 325 | updates = Array.isArray(updates) ? updates : [updates]; 326 | for (const {id, props} of updates) { 327 | if (typeof menuItems[id] !== 'undefined') { 328 | Object.assign(menuItems[id], props); 329 | } 330 | } 331 | tray.setContextMenu(Menu.buildFromTemplate(Object.values(menuItems))); 332 | } 333 | } 334 | 335 | // This method will be called when Electron has finished 336 | // initialization and is ready to create browser windows. 337 | // Some APIs can only be used after this event occurs. 338 | app.on('ready', async () => { 339 | try { 340 | // 1. initialize then cache 341 | config = loadConfig(); 342 | sysProxy = await createSysProxy(); 343 | 344 | // 2. initialize non-module ipc 345 | registerIPC({ 346 | [RENDERER_INIT]: (push) => { 347 | if (process.platform === 'win32') { 348 | require('readline').createInterface({ 349 | input: process.stdin, 350 | output: process.stdout 351 | }).on('SIGINT', function () { 352 | process.emit('SIGINT'); 353 | }); 354 | } 355 | process.on('SIGINT', onAppClose); 356 | process.on('SIGTERM', onAppClose); 357 | process.on('uncaughtException', (err) => { 358 | try { 359 | push(MAIN_ERROR, err); 360 | } catch (err) { 361 | // fallthrough 362 | } 363 | logger.error(err); 364 | }); 365 | push(MAIN_INIT, { 366 | version: packageJson.version, 367 | config, 368 | pacLastUpdatedAt: fs.lstatSync(DEFAULT_GFWLIST_PATH).mtime.getTime() 369 | }); 370 | rendererReady({push}); 371 | }, 372 | [RENDERER_QUIT]: () => { 373 | onAppClose(); 374 | }, 375 | [RENDERER_SAVE_CONFIG]: (push, json) => { 376 | saveConfig(json); 377 | config = json; // update cached global.config 378 | }, 379 | [RENDERER_PREVIEW_LOGS]: () => { 380 | if (logWindow !== null) { 381 | logWindow.focus(); 382 | } else { 383 | logWindow = new BrowserWindow({ 384 | title: `${packageJson.name} - logs`, 385 | width: 800, 386 | height: 600, 387 | show: false 388 | }); 389 | logWindow.on('closed', () => logWindow = null); 390 | logWindow.on('ready-to-show', () => logWindow.show()); 391 | logWindow.loadURL(APP_LOG_URL); 392 | } 393 | } 394 | }); 395 | 396 | // 3. display window 397 | createWindow(); 398 | 399 | // 4. initialize tray 400 | tray = new Tray(APP_TRAY_ICON); 401 | tray.setToolTip('blinksocks-desktop'); 402 | updateContextMenu(); 403 | } catch (err) { 404 | logger.error(err); 405 | process.exit(-1); 406 | } 407 | }); 408 | 409 | app.on('activate', () => { 410 | // On macOS it's common to re-create a window in the app when the 411 | // dock icon is clicked and there are no other windows open. 412 | if (mainWindow === null) { 413 | createWindow(); 414 | } else { 415 | mainWindow.show(); 416 | } 417 | }); 418 | 419 | app.on('will-quit', (e) => e.preventDefault()); 420 | -------------------------------------------------------------------------------- /src/backend/modules/bs.js: -------------------------------------------------------------------------------- 1 | const {Hub} = require('blinksocks'); 2 | const logger = require('../helpers/logger'); 3 | 4 | const { 5 | MAIN_ERROR, 6 | MAIN_START_BS, 7 | MAIN_STOP_BS, 8 | RENDERER_START_BS, 9 | RENDERER_STOP_BS 10 | } = require('../../defs/events'); 11 | 12 | let bs = null; 13 | 14 | module.exports = function bsModule({onStatusChange}) { 15 | 16 | /** 17 | * start blinksocks client 18 | * @param push 19 | * @param config 20 | */ 21 | function start(push, {config}) { 22 | if (bs) { 23 | bs.terminate(); 24 | } 25 | try { 26 | bs = new Hub(config); 27 | bs.run(() => { 28 | push(MAIN_START_BS); 29 | onStatusChange(true); 30 | }); 31 | } catch (err) { 32 | logger.error(err); 33 | push(MAIN_ERROR, err.message); 34 | } 35 | } 36 | 37 | /** 38 | * stop blinksocks client 39 | * @param push 40 | */ 41 | function stop(push) { 42 | if (bs) { 43 | bs.terminate(); 44 | bs = null; 45 | } 46 | push(MAIN_STOP_BS); 47 | onStatusChange(false); 48 | } 49 | 50 | return { 51 | [RENDERER_START_BS]: start, 52 | [RENDERER_STOP_BS]: stop, 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/backend/modules/log.js: -------------------------------------------------------------------------------- 1 | const { 2 | MAIN_ERROR 3 | } = require('../constants'); 4 | 5 | const { 6 | RENDERER_QUERY_BS_LOG, 7 | RENDERER_QUERY_BSD_LOG, 8 | RENDERER_STREAM_BS_LOG, 9 | RENDERER_STREAM_BSD_LOG, 10 | MAIN_QUERY_BS_LOG, 11 | MAIN_QUERY_BSD_LOG, 12 | MAIN_STREAM_BS_LOG, 13 | MAIN_STREAM_BSD_LOG 14 | } = require('../../defs/events'); 15 | 16 | /** 17 | * query logs from logger instance, use Loggly Search API 18 | * @param logger 19 | * @param options 20 | * @returns {Promise} 21 | */ 22 | function query(logger, options) { 23 | return new Promise((resolve, reject) => { 24 | if (!logger) { 25 | resolve([]); 26 | } else { 27 | const opts = Object.assign({ 28 | from: new Date() - 24 * 60 * 60 * 1000, 29 | until: new Date(), 30 | limit: 9e5, 31 | start: 0, 32 | order: 'desc' 33 | }, options || {}); 34 | logger.query(opts, function (err, results) { 35 | if (err) { 36 | reject(err); 37 | } else { 38 | resolve(results.file); 39 | } 40 | }); 41 | } 42 | }); 43 | } 44 | 45 | module.exports = function logModule({bsLogger, bsdLogger}) { 46 | const bsLoggerStream = bsLogger.stream({start: -1}); 47 | const bsdLoggerStream = bsdLogger.stream({start: -1}); 48 | 49 | async function getBSLog(push, options) { 50 | try { 51 | const logs = await query(bsLogger, options); 52 | push(MAIN_QUERY_BS_LOG, {logs}); 53 | } catch (err) { 54 | push(MAIN_ERROR, err.message); 55 | } 56 | } 57 | 58 | async function getBSDLog(push, options) { 59 | try { 60 | const logs = await query(bsdLogger, options); 61 | push(MAIN_QUERY_BSD_LOG, {logs}); 62 | } catch (err) { 63 | push(MAIN_ERROR, err.message); 64 | } 65 | } 66 | 67 | function streamBSLog(push, isOn) { 68 | if (isOn) { 69 | bsLoggerStream.on('log', function (log) { 70 | push(MAIN_STREAM_BS_LOG, {log}); 71 | }); 72 | } else { 73 | bsLoggerStream.removeAllListeners(); 74 | } 75 | } 76 | 77 | function streamBSDLog(push, isOn) { 78 | if (isOn) { 79 | bsdLoggerStream.on('log', function (log) { 80 | push(MAIN_STREAM_BSD_LOG, {log}); 81 | }); 82 | } else { 83 | bsdLoggerStream.removeAllListeners(); 84 | } 85 | } 86 | 87 | return { 88 | [RENDERER_QUERY_BS_LOG]: getBSLog, 89 | [RENDERER_QUERY_BSD_LOG]: getBSDLog, 90 | [RENDERER_STREAM_BS_LOG]: streamBSLog, 91 | [RENDERER_STREAM_BSD_LOG]: streamBSDLog 92 | }; 93 | }; 94 | -------------------------------------------------------------------------------- /src/backend/modules/pac.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const stream = require('stream'); 3 | const readline = require('readline'); 4 | const http = require('http'); 5 | const os = require('os'); 6 | const path = require('path'); 7 | const logger = require('../helpers/logger'); 8 | 9 | const { 10 | BUILT_IN_ADP_SCRIPTS_PATH, 11 | DEFAULT_GFWLIST_PATH 12 | } = require('../constants'); 13 | 14 | const { 15 | MAIN_START_PAC, 16 | MAIN_STOP_PAC, 17 | RENDERER_START_PAC, 18 | RENDERER_STOP_PAC 19 | } = require('../../defs/events'); 20 | 21 | // cache adp-scripts.js into memory 22 | const adpScripts = fs.readFileSync(BUILT_IN_ADP_SCRIPTS_PATH); 23 | 24 | /** 25 | * A http service which serves pac file 26 | */ 27 | class PacService { 28 | 29 | constructor() { 30 | this._server = null; 31 | } 32 | 33 | isRunning() { 34 | return this._server !== null; 35 | } 36 | 37 | start({host, port, proxyHost, proxyPort, rules}) { 38 | if (!this._server) { 39 | const fileData = this._assemble({host: proxyHost, port: proxyPort, rules}); 40 | this._server = http.createServer((req, res) => { 41 | res.writeHead(200, { 42 | 'Server': 'blinksocks-desktop', 43 | 'Content-Type': 'application/x-ns-proxy-autoconfig', 44 | 'Content-Length': fileData.length, 45 | 'Cache-Control': 'max-age=36000', 46 | 'Date': (new Date).toUTCString(), 47 | 'Connection': 'Close' 48 | }); 49 | res.end(fileData); 50 | }); 51 | this._server.on('clientError', (err, socket) => { 52 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 53 | }); 54 | this._server.listen(port, () => { 55 | logger.info(`started local pac server at http://${host}:${port}`); 56 | }); 57 | } 58 | } 59 | 60 | stop() { 61 | if (this._server) { 62 | this._server.close(); 63 | this._server = null; 64 | logger.info('stopped local pac server'); 65 | } 66 | } 67 | 68 | _assemble({host, port, rules}) { 69 | const _rules = JSON.stringify(rules, null, ' '); 70 | return ` 71 | var proxy = "SOCKS5 ${host}:${port}; SOCKS ${host}:${port}; PROXY ${host}:${port}; DIRECT;"; 72 | var direct = 'DIRECT;'; 73 | var rules = ${_rules}; 74 | 75 | ${adpScripts} 76 | 77 | var defaultMatcher = new CombinedMatcher(); 78 | 79 | for (var i = 0; i < rules.length; i++) { 80 | defaultMatcher.add(Filter.fromText(rules[i])); 81 | } 82 | 83 | function FindProxyForURL(url, host) { 84 | if (defaultMatcher.matchesAny(url, host) instanceof BlockingFilter) { 85 | return proxy; 86 | } 87 | return direct; 88 | } 89 | `; 90 | } 91 | 92 | } 93 | 94 | /** 95 | * parse gfwlist line by line 96 | * @param filePath 97 | * @returns {Promise} 98 | */ 99 | function parseRules(filePath) { 100 | return new Promise((resolve, reject) => { 101 | fs.readFile(filePath, 'utf8', (err, data) => { 102 | if (err) { 103 | reject(err); 104 | } else { 105 | const sr = new stream.PassThrough(); 106 | sr.end(Buffer.from(data, 'base64').toString('ascii')); 107 | const rl = readline.createInterface({input: sr}); 108 | let index = 0; 109 | const domains = []; 110 | rl.on('line', (line) => { 111 | if (!(line.startsWith('!')) && line.length > 0 && index !== 0) { 112 | domains.push(line); 113 | } 114 | index += 1; 115 | }); 116 | rl.on('close', () => resolve(domains)); 117 | } 118 | }); 119 | }); 120 | } 121 | 122 | module.exports = function pacModule({onStatusChange}) { 123 | 124 | let pacService = new PacService(); 125 | 126 | /** 127 | * start local pac service 128 | * @param push 129 | * @param type 130 | * @param host 131 | * @param port 132 | * @param proxyHost 133 | * @param proxyPort 134 | * @param customRules 135 | * @returns {Promise.} 136 | */ 137 | async function start(push, {type, host, port, proxyHost, proxyPort, customRules}) { 138 | try { 139 | // start pac service only in local mode 140 | if (type === 0) { 141 | // parse custom rules 142 | if (Array.isArray(customRules)) { 143 | customRules = customRules.filter((rule) => rule.length > 0); 144 | } else { 145 | customRules = []; 146 | } 147 | // parse gfwlist.txt 148 | const builtInRules = await parseRules(DEFAULT_GFWLIST_PATH); 149 | pacService.start({host, port, proxyHost, proxyPort, rules: customRules.concat(builtInRules)}); 150 | } 151 | push(MAIN_START_PAC); 152 | onStatusChange(true); 153 | } catch (err) { 154 | logger.error(err); 155 | } 156 | } 157 | 158 | /** 159 | * stop local pac service 160 | */ 161 | function stop(push) { 162 | pacService.stop(); 163 | push(MAIN_STOP_PAC); 164 | onStatusChange(false); 165 | } 166 | 167 | return { 168 | [RENDERER_START_PAC]: start, 169 | [RENDERER_STOP_PAC]: stop, 170 | }; 171 | }; 172 | -------------------------------------------------------------------------------- /src/backend/modules/qrcode.js: -------------------------------------------------------------------------------- 1 | const QRCode = require('qrcode'); 2 | const {clipboard, nativeImage} = require('electron'); 3 | 4 | const { 5 | MAIN_ERROR, 6 | MAIN_CREATE_QR_CODE, 7 | MAIN_COPY_QR_CODE_AS_IMAGE, 8 | MAIN_COPY_QR_CODE_AS_TEXT, 9 | RENDERER_CREATE_QR_CODE, 10 | RENDERER_COPY_QR_CODE_AS_IMAGE, 11 | RENDERER_COPY_QR_CODE_AS_TEXT 12 | } = require('../../defs/events'); 13 | 14 | /** 15 | * A promise wrapper to QRCode.toDataURL 16 | * @param qrcode 17 | * @returns {Promise} 18 | * @constructor 19 | */ 20 | function QRCodeToDataURL(qrcode) { 21 | return new Promise((resolve, reject) => { 22 | QRCode.toDataURL(qrcode.instance.segments, (err, url) => { 23 | if (err) { 24 | reject(err); 25 | } else { 26 | resolve(url); 27 | } 28 | }); 29 | }); 30 | } 31 | 32 | module.exports = function qrcodeModule() { 33 | const qrcodes = { 34 | // [name]: {rawText, instance: } 35 | }; 36 | 37 | async function create(push, {name, message}) { 38 | try { 39 | qrcodes[name] = { 40 | rawText: message, 41 | instance: QRCode.create(message) 42 | }; 43 | push(MAIN_CREATE_QR_CODE, {name, dataURL: await QRCodeToDataURL(qrcodes[name])}); 44 | } catch (err) { 45 | push(MAIN_ERROR, err.message); 46 | } 47 | } 48 | 49 | async function copyAsImage(push, {name}) { 50 | if (typeof qrcodes[name] !== 'undefined') { 51 | try { 52 | const dataURL = await QRCodeToDataURL(qrcodes[name]); 53 | const image = nativeImage.createFromDataURL(dataURL); 54 | clipboard.writeImage(image); 55 | push(MAIN_COPY_QR_CODE_AS_IMAGE); 56 | } catch (err) { 57 | push(MAIN_ERROR, err.message); 58 | } 59 | } 60 | } 61 | 62 | async function copyAsText(push, {name}) { 63 | if (typeof qrcodes[name] !== 'undefined') { 64 | try { 65 | const {rawText} = qrcodes[name]; 66 | clipboard.writeText(rawText); 67 | push(MAIN_COPY_QR_CODE_AS_TEXT); 68 | } catch (err) { 69 | push(MAIN_ERROR, err.message); 70 | } 71 | } 72 | } 73 | 74 | return { 75 | [RENDERER_CREATE_QR_CODE]: create, 76 | [RENDERER_COPY_QR_CODE_AS_IMAGE]: copyAsImage, 77 | [RENDERER_COPY_QR_CODE_AS_TEXT]: copyAsText 78 | }; 79 | }; 80 | -------------------------------------------------------------------------------- /src/backend/modules/sys.js: -------------------------------------------------------------------------------- 1 | const { 2 | MAIN_SET_SYS_PAC, 3 | MAIN_SET_SYS_PROXY, 4 | MAIN_RESTORE_SYS_PAC, 5 | MAIN_RESTORE_SYS_PROXY, 6 | RENDERER_SET_SYS_PAC, 7 | RENDERER_SET_SYS_PROXY, 8 | RENDERER_RESTORE_SYS_PAC, 9 | RENDERER_RESTORE_SYS_PROXY 10 | } = require('../../defs/events'); 11 | 12 | module.exports = function sysModule({sysProxy}) { 13 | 14 | async function setGlobal(push, {host, port, bypass}) { 15 | await sysProxy.setGlobal({host, port, bypass}); 16 | push(MAIN_SET_SYS_PROXY); 17 | } 18 | 19 | async function setPac(push, {url}) { 20 | await sysProxy.setPAC({url}); 21 | push(MAIN_SET_SYS_PAC); 22 | } 23 | 24 | async function restoreGlobal(push, {host, port, bypass}) { 25 | await sysProxy.restoreGlobal({host, port, bypass}); 26 | push(MAIN_RESTORE_SYS_PROXY); 27 | } 28 | 29 | async function restorePac(push, {url}) { 30 | await sysProxy.restorePAC({url}); 31 | push(MAIN_RESTORE_SYS_PAC); 32 | } 33 | 34 | return { 35 | [RENDERER_SET_SYS_PROXY]: setGlobal, 36 | [RENDERER_SET_SYS_PAC]: setPac, 37 | [RENDERER_RESTORE_SYS_PROXY]: restoreGlobal, 38 | [RENDERER_RESTORE_SYS_PAC]: restorePac 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/backend/modules/update.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const path = require('path'); 3 | const zlib = require('zlib'); 4 | const fs = require('original-fs'); 5 | const axios = require('axios'); 6 | const isProduction = !require('electron-is-dev'); 7 | const logger = require('../helpers/logger'); 8 | 9 | const { 10 | GFWLIST_URL, 11 | RELEASES_URL, 12 | DEFAULT_GFWLIST_PATH 13 | } = require('../constants'); 14 | 15 | const { 16 | MAIN_UPDATE_PAC, 17 | MAIN_UPDATE_PAC_FAIL, 18 | MAIN_UPDATE_SELF, 19 | MAIN_UPDATE_SELF_PROGRESS, 20 | MAIN_UPDATE_SELF_FAIL, 21 | RENDERER_UPDATE_PAC, 22 | RENDERER_UPDATE_SELF, 23 | RENDERER_UPDATE_SELF_CANCEL 24 | } = require('../../defs/events'); 25 | 26 | function checkPatchHash(patchBuf) { 27 | if (patchBuf.length < 32) { 28 | throw Error('patch is too short'); 29 | } 30 | const hashHead = patchBuf.slice(0, 32); 31 | const sha256 = crypto.createHash('sha256'); 32 | const restBuf = patchBuf.slice(32); 33 | const realHashHead = sha256.update(restBuf).digest(); 34 | if (!hashHead.equals(realHashHead)) { 35 | throw Error(`sha256 mismatch, expect ${hashHead.toString('hex')} but got ${realHashHead.toString('hex')}`); 36 | } 37 | return true; 38 | } 39 | 40 | /** 41 | * a promise wrapper for fs.writeFile 42 | * @param file 43 | * @param data 44 | * @returns {Promise} 45 | */ 46 | function writeFile(file, data) { 47 | return new Promise((resolve, reject) => { 48 | fs.writeFile(file, data, (err) => { 49 | if (err) { 50 | reject(err); 51 | } else { 52 | resolve(); 53 | } 54 | }); 55 | }); 56 | } 57 | 58 | module.exports = function updateModule({app}) { 59 | 60 | let updateSelfRequest = null; 61 | 62 | /** 63 | * update pac 64 | * @param push 65 | * @returns {Promise.} 66 | */ 67 | async function updatePac(push) { 68 | const stat = fs.lstatSync(DEFAULT_GFWLIST_PATH); 69 | const lastModifiedAt = stat.mtime.getTime(); 70 | const now = (new Date()).getTime(); 71 | if (now - lastModifiedAt >= 6 * 60 * 60 * 1e3) { // 6 hours 72 | try { 73 | logger.info(`updating pac from: ${GFWLIST_URL}`); 74 | const response = await axios({ 75 | method: 'get', 76 | url: GFWLIST_URL, 77 | responseType: 'stream' 78 | }); 79 | response.data.pipe(fs.createWriteStream(DEFAULT_GFWLIST_PATH)); 80 | logger.info(`pac updated successfully`); 81 | push(MAIN_UPDATE_PAC, now); 82 | } catch (err) { 83 | logger.error(err); 84 | push(MAIN_UPDATE_PAC_FAIL, err.message); 85 | } 86 | } else { 87 | const message = 'pac had been updated less than 6 hours'; 88 | logger.warn(message); 89 | push(MAIN_UPDATE_PAC_FAIL, message); 90 | } 91 | } 92 | 93 | /** 94 | * preform self-update 95 | * @param push 96 | * @param version 97 | * @returns {Promise.} 98 | */ 99 | async function updateSelf(push, {version}) { 100 | const patchName = `blinksocks-desktop-v${version}`; 101 | const patchUrl = `${RELEASES_URL}/download/v${version}/${patchName}.patch.gz`; 102 | 103 | try { 104 | logger.info(`downloading patch file: ${patchUrl}`); 105 | 106 | // 1. download patch file 107 | updateSelfRequest = axios.CancelToken.source(); 108 | const response = await axios({ 109 | method: 'get', 110 | url: patchUrl, 111 | responseType: 'stream', 112 | cancelToken: updateSelfRequest.token 113 | }); 114 | const stream = response.data; 115 | const contentLength = +stream.headers['content-length']; 116 | 117 | let buffer = Buffer.alloc(0); 118 | stream.on('data', (chunk) => { 119 | buffer = Buffer.concat([buffer, chunk]); 120 | push(MAIN_UPDATE_SELF_PROGRESS, { 121 | totalBytes: contentLength, 122 | receivedBytes: buffer.length, 123 | percentage: contentLength > 0 ? buffer.length / contentLength : 0 124 | }); 125 | }); 126 | stream.on('end', async () => { 127 | if (buffer.length !== contentLength) { 128 | logger.warn(`unexpected patch size=${buffer.length} bytes, want=${contentLength} bytes`); 129 | } else { 130 | logger.info(`downloaded patch file, size=${buffer.length} bytes`); 131 | } 132 | try { 133 | // 2. unzip 134 | buffer = zlib.unzipSync(buffer); 135 | 136 | // 3. check hash 137 | if (!checkPatchHash(buffer)) { 138 | return; 139 | } 140 | 141 | // 4. overwrite the current asar 142 | if (isProduction) { 143 | const asarBuf = buffer.slice(32); 144 | 145 | let appAsarPath; 146 | if (process.platform === 'darwin') { 147 | appAsarPath = path.resolve(path.dirname(process.execPath), '../Resources/app.asar'); 148 | } else { 149 | appAsarPath = path.resolve(path.dirname(process.execPath), 'resources', 'app.asar'); 150 | } 151 | logger.info(`overwriting ${appAsarPath}`); 152 | await writeFile(appAsarPath, asarBuf); 153 | } else { 154 | logger.warn('app.asar will not be replaced in development'); 155 | } 156 | 157 | // 5. restart app 158 | if (isProduction) { 159 | logger.info(`relaunching...`); 160 | app.relaunch(); 161 | app.exit(0); 162 | } else { 163 | logger.warn('app will not restart in development'); 164 | } 165 | 166 | push(MAIN_UPDATE_SELF); 167 | } catch (err) { 168 | logger.error(err.message); 169 | push(MAIN_UPDATE_SELF_FAIL, err.message); 170 | } 171 | }); 172 | } catch (err) { 173 | logger.error(err.message); 174 | push(MAIN_UPDATE_SELF_FAIL, err.message); 175 | } 176 | } 177 | 178 | /** 179 | * cancel self-updating 180 | */ 181 | function cancelUpdateSelf() { 182 | if (updateSelfRequest !== null) { 183 | updateSelfRequest.cancel('Updating Canceled'); 184 | updateSelfRequest = null; 185 | logger.info('self updating canceled'); 186 | } 187 | } 188 | 189 | return { 190 | [RENDERER_UPDATE_PAC]: updatePac, 191 | [RENDERER_UPDATE_SELF]: updateSelf, 192 | [RENDERER_UPDATE_SELF_CANCEL]: cancelUpdateSelf 193 | }; 194 | }; 195 | -------------------------------------------------------------------------------- /src/backend/system/create.js: -------------------------------------------------------------------------------- 1 | /** 2 | * create platform-related SysProxy 3 | */ 4 | function createSysProxy() { 5 | const platform = process.platform; 6 | const ProxyClass = require(`./platforms/${platform}`); 7 | 8 | if (typeof ProxyClass !== 'function') { 9 | throw Error(`ProxyClass not found or invalid on "${platform}"`); 10 | } 11 | 12 | return new Promise((resolve) => { 13 | const instance = new ProxyClass(); 14 | if (instance === null) { 15 | throw Error('fail to create system proxy instance'); 16 | } 17 | if (instance.on) { 18 | instance.on('ready', () => resolve(instance.getSysProxyInstance())); 19 | instance.on('fallback', resolve); 20 | } else { 21 | resolve(instance); 22 | } 23 | }); 24 | } 25 | 26 | module.exports = { 27 | createSysProxy 28 | }; 29 | -------------------------------------------------------------------------------- /src/backend/system/platforms/darwin.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const crypto = require('crypto'); 3 | const dgram = require('dgram'); 4 | const EventEmitter = require('events'); 5 | const sudo = require('sudo-prompt'); 6 | const logger = require('../../helpers/logger'); 7 | const {DARWIN_SUDO_AGENT_PATH, DARWIN_SUDO_AGENT_PORT_FILE} = require('../../constants'); 8 | const ISysProxy = require('./interface'); 9 | 10 | class DarwinSysProxyHelper extends EventEmitter { 11 | 12 | constructor(sender, verifyTag) { 13 | super(); 14 | this._sender = sender; 15 | this._verifyTag = verifyTag; 16 | this._agentPort = 0; 17 | 18 | // watch SUDO_AGENT_PORT_FILE for any changes 19 | fs.watchFile(DARWIN_SUDO_AGENT_PORT_FILE, () => { 20 | this._agentPort = parseInt(fs.readFileSync(DARWIN_SUDO_AGENT_PORT_FILE), 10); 21 | this.emit('ready'); 22 | fs.unwatchFile(DARWIN_SUDO_AGENT_PORT_FILE); 23 | }); 24 | } 25 | 26 | getSysProxyInstance() { 27 | class ProxyClass { 28 | } 29 | // wrap all methods of ISysProxy 30 | const methods = Object.getOwnPropertyNames(ISysProxy.prototype).slice(1); 31 | 32 | // add a method to kill sudo agent if necessary 33 | methods.push('kill'); 34 | 35 | for (const method of methods) { 36 | ProxyClass.prototype[method] = (args) => { 37 | // send request with verify tag 38 | const request = JSON.stringify({ 39 | tag: this._verifyTag, 40 | method, 41 | args 42 | }); 43 | try { 44 | this._sender.send(request, this._agentPort, '127.0.0.1'); 45 | logger.debug(`client request: ${request}`); 46 | } catch (err) { 47 | logger.error(err); 48 | } 49 | }; 50 | } 51 | return new ProxyClass(); 52 | } 53 | 54 | } 55 | 56 | module.exports = function () { 57 | const sender = dgram.createSocket('udp4'); 58 | const SUDO_AGENT_VERIFY_TAG = crypto.randomBytes(16).toString('hex'); 59 | const helper = new DarwinSysProxyHelper(sender, SUDO_AGENT_VERIFY_TAG); 60 | 61 | const fallback = () => { 62 | sender.close(); 63 | helper.emit('fallback', new ISysProxy()); // fallback to manual mode 64 | }; 65 | 66 | const command = [DARWIN_SUDO_AGENT_PATH, `"${SUDO_AGENT_VERIFY_TAG}"`].join(' '); 67 | 68 | logger.debug(command); 69 | 70 | // grant root permission to sudo-agent.js 71 | sudo.exec(command, {name: 'blinksocks desktop'}, function (error/*, stdout, stderr*/) { 72 | if (error) { 73 | logger.warn(error.message); 74 | fallback(); 75 | } 76 | }); 77 | 78 | return helper; 79 | }; 80 | -------------------------------------------------------------------------------- /src/backend/system/platforms/index.js: -------------------------------------------------------------------------------- 1 | export * from './darwin'; 2 | export * from './linux'; 3 | export * from './win32'; 4 | -------------------------------------------------------------------------------- /src/backend/system/platforms/interface.js: -------------------------------------------------------------------------------- 1 | const logger = require('../../helpers/logger'); 2 | 3 | module.exports = class ISysProxy { 4 | 5 | async setGlobal(/* {host, port, bypass} */) { 6 | logger.warn('abstract method ISysProxy.setGlobal() is called, perhaps you should make an implementation.'); 7 | } 8 | 9 | async setPAC(/* {url} */) { 10 | logger.warn('abstract method ISysProxy.setPAC() is called, perhaps you should make an implementation.'); 11 | } 12 | 13 | async restoreGlobal(/* {host, port, bypass} */) { 14 | logger.warn('abstract method ISysProxy.restoreGlobal() is called, perhaps you should make an implementation.'); 15 | } 16 | 17 | async restorePAC(/* {url} */) { 18 | logger.warn('abstract method ISysProxy.restorePAC() is called, perhaps you should make an implementation.'); 19 | } 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /src/backend/system/platforms/linux.js: -------------------------------------------------------------------------------- 1 | const ISysProxy = require('./interface'); 2 | 3 | module.exports = class LinuxSysProxy extends ISysProxy { 4 | 5 | // It's hard to find a system-wide solution to set system proxy on linux desktop(GNOME, KDE, etc.), 6 | // users should set system proxy manually at present, this is recommended. 7 | // 8 | // Documents should point out some workaround, please also see: 9 | // https://justintung.com/2013/04/25/how-to-configure-proxy-settings-in-linux/ 10 | 11 | }; 12 | -------------------------------------------------------------------------------- /src/backend/system/platforms/win32.js: -------------------------------------------------------------------------------- 1 | const ISysProxy = require('./interface'); 2 | const {exec} = require('../../helpers/shell'); 3 | const {WIN32_SYSPROXY_HELPER} = require('../../constants'); 4 | 5 | module.exports = class Win32SysProxy extends ISysProxy { 6 | 7 | constructor() { 8 | super(); 9 | this._agent = WIN32_SYSPROXY_HELPER; 10 | } 11 | 12 | async setGlobal({host, port, bypass}) { 13 | if (host && port) { 14 | await exec(`${this._agent} global ${host}:${port} "${bypass.join(';')}"`); 15 | } 16 | } 17 | 18 | async setPAC({url}) { 19 | if (url) { 20 | await exec(`${this._agent} pac ${url}`); 21 | } 22 | } 23 | 24 | async restorePAC() { 25 | await this._restore(); 26 | } 27 | 28 | async restoreGlobal() { 29 | await this._restore(); 30 | } 31 | 32 | async _restore() { 33 | // const {flags, proxyServer, bypassList, pacUrl} = this._backups; 34 | // await exec(`${this._agent} set ${flags} ${proxyServer} ${bypassList} ${pacUrl}`); 35 | await exec(`${this._agent} set 9`); 36 | } 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/DatePicker/DatePicker.css: -------------------------------------------------------------------------------- 1 | .datepicker { 2 | padding: 5px; 3 | width: 100%; 4 | border: 1px solid #dfdfdf; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/DatePicker/DatePicker.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Flatpickr from 'flatpickr'; 4 | import 'flatpickr/dist/flatpickr.min.css'; 5 | import './DatePicker.css'; 6 | 7 | /** 8 | * A React wrapper for flatpickr 9 | */ 10 | export class DatePicker extends Component { 11 | 12 | static propTypes = { 13 | flatpickrOptions: PropTypes.object, 14 | disabled: PropTypes.bool 15 | }; 16 | 17 | static defaultProps = { 18 | flatpickrOptions: {}, 19 | disabled: false 20 | }; 21 | 22 | $datePicker = null; 23 | 24 | componentDidMount() { 25 | const {flatpickrOptions, onChange} = this.props; 26 | new Flatpickr(this.$datePicker, { 27 | onChange, 28 | ...flatpickrOptions 29 | }); 30 | } 31 | 32 | render() { 33 | const {disabled} = this.props; 34 | return ( 35 | this.$datePicker = dom}/> 36 | ); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/components/DatePicker/index.js: -------------------------------------------------------------------------------- 1 | export * from './DatePicker'; -------------------------------------------------------------------------------- /src/components/PopupDialog/PopupDialog.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Dialog, FlatButton} from 'material-ui'; 4 | 5 | export class PopupDialog extends Component { 6 | 7 | static propTypes = { 8 | isOpen: PropTypes.bool, 9 | title: PropTypes.string, 10 | children: PropTypes.element, 11 | onConfirm: PropTypes.func, 12 | onCancel: PropTypes.func 13 | }; 14 | 15 | static defaultProps = { 16 | isOpen: false, 17 | title: '', 18 | children: null, 19 | onConfirm: null, 20 | onCancel: null 21 | }; 22 | 23 | render() { 24 | const {isOpen, title, children, onConfirm, onCancel} = this.props; 25 | const actions = []; 26 | if (typeof onConfirm === 'function') { 27 | actions.push(); 28 | } 29 | if (typeof onCancel === 'function') { 30 | actions.push(); 31 | } 32 | return ( 33 | {children} 34 | ); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/components/PopupDialog/index.js: -------------------------------------------------------------------------------- 1 | export * from './PopupDialog'; -------------------------------------------------------------------------------- /src/components/PresetItem/PresetItem.css: -------------------------------------------------------------------------------- 1 | .preset-item { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | padding: 5px; 6 | margin-bottom: 5px; 7 | border: 1px solid #e4e4e4; 8 | border-radius: 2px; 9 | cursor: pointer; 10 | transition: all .1s linear; 11 | } 12 | 13 | .preset-item:hover { 14 | background-color: #eee; 15 | } 16 | 17 | .preset-item__info p { 18 | margin: 0; 19 | color: #000; 20 | } 21 | 22 | .preset-item__info em { 23 | font-size: .8rem; 24 | word-break: break-all; 25 | } 26 | 27 | .preset-item__operation { 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/PresetItem/PresetItem.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {NotificationDoNotDisturbOn} from 'material-ui/svg-icons'; 4 | import {red600} from 'material-ui/styles/colors'; 5 | import './PresetItem.css'; 6 | 7 | export class PresetItem extends Component { 8 | 9 | static propTypes = { 10 | preset: PropTypes.object.isRequired, 11 | disabled: PropTypes.bool, 12 | onEdit: PropTypes.func, 13 | onDelete: PropTypes.func 14 | }; 15 | 16 | static defaultProps = { 17 | disabled: false, 18 | onEdit: (/* preset */) => { 19 | }, 20 | onDelete: (/* preset */) => { 21 | } 22 | }; 23 | 24 | render() { 25 | const {preset, disabled} = this.props; 26 | return ( 27 |
  • this.props.onEdit(preset)}> 28 |
    29 |

    {preset.name}

    30 | {Object.values(preset.params).join(' ') || 'no params'} 31 |
    32 | {!disabled && ( 33 |
    34 | { 35 | this.props.onDelete(preset); 36 | e.stopPropagation(); 37 | }}/> 38 |
    39 | )} 40 |
  • 41 | ); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/components/PresetItem/index.js: -------------------------------------------------------------------------------- 1 | export * from './PresetItem'; -------------------------------------------------------------------------------- /src/components/PresetParamItem/PresetParamItem.css: -------------------------------------------------------------------------------- 1 | .preset-param-item { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .preset-param-item__enum { 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | .preset-param-item__enum span { 12 | display: block; 13 | margin: 10px 0 0 0; 14 | font-size: .8rem; 15 | color: rgba(0, 0, 0, 0.3); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/PresetParamItem/PresetParamItem.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { 5 | TextField, 6 | DropDownMenu, 7 | MenuItem 8 | } from 'material-ui'; 9 | 10 | import './PresetParamItem.css'; 11 | 12 | function getValueComponent(def, value, onChange) { 13 | const {type, key} = def; 14 | switch (type) { 15 | case 'string': 16 | return ( 17 | onChange(e.currentTarget.value)} 21 | floatingLabelText={key} 22 | fullWidth 23 | /> 24 | ); 25 | case 'number': { 26 | return ( 27 | onChange(e.currentTarget.value)} 31 | floatingLabelText={key} 32 | fullWidth 33 | /> 34 | ); 35 | } 36 | case 'enum': { 37 | const {values} = def; 38 | return ( 39 |
    40 | {def.key} 41 | onChange(v)}> 42 | {values.map((v, i) => ( 43 | 44 | ))} 45 | 46 |
    47 | ); 48 | } 49 | case 'array': { 50 | return ( 51 | onChange(e.currentTarget.value.split('\n'))} 55 | floatingLabelText={key} 56 | fullWidth 57 | multiLine 58 | /> 59 | ); 60 | } 61 | default: 62 | return ( 63 |
    Unknown Type: "{type}"
    64 | ); 65 | } 66 | } 67 | 68 | export class PresetParamItem extends Component { 69 | 70 | static propTypes = { 71 | def: PropTypes.object.isRequired, 72 | value: PropTypes.any.isRequired, 73 | onChange: PropTypes.func 74 | }; 75 | 76 | static defaultProps = { 77 | onChange: (/* key, value */) => { 78 | } 79 | }; 80 | 81 | render() { 82 | const {def, value} = this.props; 83 | const {key, defaultValue} = def; 84 | return ( 85 |
  • 86 | {getValueComponent( 87 | def, 88 | typeof value === 'undefined' ? defaultValue : value, 89 | (newValue) => this.props.onChange(key, newValue) 90 | )} 91 |
  • 92 | ); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/components/PresetParamItem/index.js: -------------------------------------------------------------------------------- 1 | export * from './PresetParamItem'; -------------------------------------------------------------------------------- /src/components/ScreenMask/SceenMask.css: -------------------------------------------------------------------------------- 1 | .screen-mask { 2 | z-index: 1101; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | bottom: 0; 8 | background-color: rgba(0, 0, 0, 0.5); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ScreenMask/ScreenMask.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './SceenMask.css'; 4 | 5 | export class ScreenMask extends Component { 6 | 7 | static propTypes = { 8 | onTouchTap: PropTypes.func 9 | }; 10 | 11 | static defaultProps = { 12 | onTouchTap: () => { 13 | } 14 | }; 15 | 16 | render() { 17 | return ( 18 |
    19 | ); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ScreenMask/index.js: -------------------------------------------------------------------------------- 1 | export * from './ScreenMask'; -------------------------------------------------------------------------------- /src/components/ServerItem/ServerItem.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { 5 | ListItem, 6 | IconButton, 7 | IconMenu, 8 | MenuItem, 9 | Toggle 10 | } from 'material-ui'; 11 | 12 | import { 13 | ActionDelete, 14 | ActionDashboard, 15 | SocialPublic, 16 | EditorModeEdit, 17 | NavigationMoreVert 18 | } from 'material-ui/svg-icons'; 19 | 20 | import {grey400} from 'material-ui/styles/colors'; 21 | 22 | const iconButtonElement = ( 23 | 24 | 25 | 26 | ); 27 | 28 | export class ServerItem extends Component { 29 | 30 | static propTypes = { 31 | server: PropTypes.object.isRequired, 32 | onToggleEnabled: PropTypes.func, 33 | onEdit: PropTypes.func, 34 | onCreateQRCode: PropTypes.func, 35 | onDelete: PropTypes.func 36 | }; 37 | 38 | static defaultProps = { 39 | onToggleEnabled: () => { 40 | }, 41 | onEdit: () => { 42 | }, 43 | onCreateQRCode: () => { 44 | }, 45 | onDelete: () => { 46 | } 47 | }; 48 | 49 | render() { 50 | const {server} = this.props; 51 | const rightIconMenu = ( 52 |
    53 | 58 | 59 | } onTouchTap={this.props.onEdit}>Edit 60 | } onTouchTap={this.props.onCreateQRCode}>QR code 61 | } onTouchTap={this.props.onDelete}>Delete 62 | 63 |
    64 | ); 65 | return ( 66 | } 68 | primaryText={server.remarks} 69 | secondaryText={`${server.host}:${server.port}`} 70 | rightIconButton={rightIconMenu} 71 | onTouchTap={this.props.onEdit} 72 | /> 73 | ); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/components/ServerItem/index.js: -------------------------------------------------------------------------------- 1 | export * from './ServerItem'; -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from './DatePicker'; 2 | export * from './PopupDialog'; 3 | export * from './PresetItem'; 4 | export * from './PresetParamItem'; 5 | export * from './ServerItem'; 6 | export * from './ScreenMask'; 7 | -------------------------------------------------------------------------------- /src/containers/App/App.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Iceland; 3 | src: url('iceland.woff2') format('woff2') 4 | } 5 | 6 | @keyframes loading { 7 | 0% { 8 | background-color: #eee; 9 | color: #1e88e5; 10 | } 11 | 100% { 12 | background-color: #1e88e5; 13 | color: #eee; 14 | } 15 | } 16 | 17 | .app__loading { 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | position: fixed; 22 | width: 100%; 23 | height: 100%; 24 | background-color: #eee; 25 | font-family: Iceland, 'Times New Roman', serif; 26 | font-size: 4rem; 27 | color: #333; 28 | animation-name: loading; 29 | animation-duration: 2s; 30 | animation-iteration-count: infinite; 31 | animation-direction: alternate; 32 | animation-timing-function: cubic-bezier(0.71, 0.01, 0.26, 0.99); 33 | } 34 | -------------------------------------------------------------------------------- /src/containers/App/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {AppBar, Divider, IconButton, RaisedButton} from 'material-ui'; 3 | import {ActionHistory, ActionDashboard} from 'material-ui/svg-icons'; 4 | 5 | import {DEFAULT_CONFIG_STRUCTURE} from '../../defs/bs-config-template'; 6 | 7 | import { 8 | RENDERER_INIT, 9 | RENDERER_START_BS, 10 | RENDERER_STOP_BS, 11 | RENDERER_START_PAC, 12 | RENDERER_STOP_PAC, 13 | RENDERER_SAVE_CONFIG, 14 | RENDERER_SET_SYS_PAC, 15 | RENDERER_SET_SYS_PROXY, 16 | RENDERER_RESTORE_SYS_PAC, 17 | RENDERER_RESTORE_SYS_PROXY, 18 | RENDERER_PREVIEW_LOGS, 19 | RENDERER_CREATE_QR_CODE, 20 | RENDERER_COPY_QR_CODE_AS_IMAGE, 21 | RENDERER_COPY_QR_CODE_AS_TEXT, 22 | MAIN_INIT, 23 | MAIN_ERROR, 24 | MAIN_START_BS, 25 | MAIN_START_PAC, 26 | MAIN_STOP_BS, 27 | MAIN_STOP_PAC, 28 | MAIN_SET_SYS_PAC, 29 | MAIN_CREATE_QR_CODE, 30 | MAIN_COPY_QR_CODE_AS_IMAGE, 31 | MAIN_COPY_QR_CODE_AS_TEXT 32 | } from '../../defs/events'; 33 | 34 | import {toast, parseQrCodeText} from '../../helpers'; 35 | import {isPresetsCompatibleToSS} from '../../defs/presets'; 36 | import {PopupDialog, ScreenMask, ServerItem} from '../../components'; 37 | import {AppSlider, General, ServerList, ClientEditor, PacEditor, ServerEditor, QRCode} from '../../containers'; 38 | import './App.css'; 39 | 40 | const {ipcRenderer} = window.require('electron'); 41 | 42 | const STATUS_OFF = 0; 43 | const STATUS_RUNNING = 1; 44 | // const STATUS_STARTING = 2; 45 | const STATUS_RESTARTING = 3; 46 | 47 | export class App extends Component { 48 | 49 | state = { 50 | version: '0.0.0', 51 | pacLastUpdatedAt: 0, 52 | config: null, 53 | appStatus: STATUS_OFF, 54 | pacStatus: STATUS_OFF, 55 | serverIndex: -1, 56 | qrcodes: { 57 | // [name]: 58 | }, 59 | isOpenDrawer: false, 60 | isOpenClientDialog: false, 61 | isOpenPacDialog: false, 62 | isOpenServerDialog: false, 63 | isOpenQRCodeDialog: false 64 | }; 65 | 66 | constructor(props) { 67 | super(props); 68 | this.onMenuTouchTap = this.onMenuTouchTap.bind(this); 69 | this.onPreviewHistory = this.onPreviewHistory.bind(this); 70 | this.onBeginAddServer = this.onBeginAddServer.bind(this); 71 | this.onBeginEditServer = this.onBeginEditServer.bind(this); 72 | this.onBeginCreateQRCode = this.onBeginCreateQRCode.bind(this); 73 | this.onBeginEditClient = this.onBeginEditClient.bind(this); 74 | this.onBeginEditPAC = this.onBeginEditPAC.bind(this); 75 | this.onEditedServer = this.onEditedServer.bind(this); 76 | this.onEditedClient = this.onEditedClient.bind(this); 77 | this.onEditedPac = this.onEditedPac.bind(this); 78 | this.onToggleLocalService = this.onToggleLocalService.bind(this); 79 | this.onToggleServer = this.onToggleServer.bind(this); 80 | this.onTogglePac = this.onTogglePac.bind(this); 81 | this.onCopyQRCodeAsText = this.onCopyQRCodeAsText.bind(this); 82 | this.onCopyQRCodeAsImage = this.onCopyQRCodeAsImage.bind(this); 83 | this.onScannedQRCodeFromScreen = this.onScannedQRCodeFromScreen.bind(this); 84 | this.onEditingServer = this.onEditingServer.bind(this); 85 | this.onEditingLocal = this.onEditingLocal.bind(this); 86 | this.onDeleteServer = this.onDeleteServer.bind(this); 87 | this.onStartApp = this.onStartApp.bind(this); 88 | this.onStopApp = this.onStopApp.bind(this); 89 | this.onSave = this.onSave.bind(this); 90 | } 91 | 92 | // react component hooks 93 | 94 | componentDidMount() { 95 | ipcRenderer.send(RENDERER_INIT); 96 | ipcRenderer.on(MAIN_INIT, (event, {version, config, pacLastUpdatedAt}) => { 97 | this.setState({ 98 | version, 99 | pacLastUpdatedAt, 100 | config, 101 | appStatus: config.app_status, 102 | pacStatus: config.pac_status 103 | }); 104 | }); 105 | ipcRenderer.on(MAIN_ERROR, (event, err) => { 106 | if (typeof err === 'object') { 107 | toast(`Error: ${JSON.stringify(err)}`); 108 | } 109 | if (typeof err === 'string') { 110 | toast(`Error: ${err}`); 111 | } 112 | console.warn(err); 113 | }); 114 | ipcRenderer.on(MAIN_START_BS, () => { 115 | const {config} = this.state; 116 | this.setState({ 117 | config: { 118 | ...config, 119 | app_status: STATUS_RUNNING 120 | }, 121 | appStatus: STATUS_RUNNING 122 | }, this.onSave); 123 | }); 124 | ipcRenderer.on(MAIN_START_PAC, () => { 125 | const {config} = this.state; 126 | this.setState({ 127 | config: { 128 | ...config, 129 | pac_status: STATUS_RUNNING 130 | }, 131 | pacStatus: STATUS_RUNNING 132 | }, this.onSave); 133 | }); 134 | ipcRenderer.on(MAIN_STOP_BS, () => { 135 | const {config} = this.state; 136 | this.setState({ 137 | config: { 138 | ...config, 139 | app_status: STATUS_OFF 140 | }, 141 | appStatus: STATUS_OFF 142 | }, this.onSave); 143 | }); 144 | ipcRenderer.on(MAIN_STOP_PAC, () => { 145 | const {config} = this.state; 146 | this.setState({ 147 | config: { 148 | ...config, 149 | pac_status: STATUS_OFF 150 | }, 151 | pacStatus: STATUS_OFF 152 | }, this.onSave); 153 | }); 154 | ipcRenderer.on(MAIN_SET_SYS_PAC, () => { 155 | const {config} = this.state; 156 | this.setState({ 157 | config: { 158 | ...config, 159 | pac_status: STATUS_RUNNING 160 | }, 161 | pacStatus: STATUS_RUNNING 162 | }, this.onSave); 163 | }); 164 | ipcRenderer.on(MAIN_CREATE_QR_CODE, (event, {name, dataURL}) => { 165 | this.setState({qrcodes: {...this.state.qrcodes, ...{[name]: dataURL}}}); 166 | }); 167 | ipcRenderer.on(MAIN_COPY_QR_CODE_AS_TEXT, () => { 168 | toast('QR code copied to clipboard'); 169 | }); 170 | ipcRenderer.on(MAIN_COPY_QR_CODE_AS_IMAGE, () => { 171 | toast('QR code copied to clipboard'); 172 | }); 173 | } 174 | 175 | componentWillUnmount() { 176 | this.onStopApp(); 177 | } 178 | 179 | // left drawer 180 | 181 | onMenuTouchTap() { 182 | this.setState({isOpenDrawer: !this.state.isOpenDrawer}); 183 | } 184 | 185 | // right history 186 | 187 | onPreviewHistory() { 188 | ipcRenderer.send(RENDERER_PREVIEW_LOGS); 189 | } 190 | 191 | // open corresponding dialog 192 | 193 | onBeginAddServer() { 194 | this.setState({isOpenServerDialog: true, serverIndex: -1}); 195 | } 196 | 197 | onBeginEditServer(i) { 198 | this.setState({isOpenServerDialog: true, serverIndex: i}); 199 | } 200 | 201 | onBeginCreateQRCode(i) { 202 | if (i > -1) { 203 | const {host, port, servers} = this.state.config; 204 | const {key, presets} = servers[i]; 205 | 206 | ipcRenderer.send(RENDERER_CREATE_QR_CODE, { 207 | name: 'blinksocks QR code', 208 | message: `bs://${btoa(JSON.stringify(servers[i]))}` 209 | }); 210 | 211 | if (isPresetsCompatibleToSS(presets)) { 212 | const method = presets[1].params.method; 213 | ipcRenderer.send(RENDERER_CREATE_QR_CODE, { 214 | name: 'shadowsocks compatible', 215 | message: `ss://${btoa(`${method}:${key}@${host}:${port}`)}` 216 | }); 217 | } 218 | 219 | this.setState({ 220 | isOpenQRCodeDialog: true, 221 | serverIndex: i, 222 | qrcodes: {} 223 | }); 224 | } 225 | } 226 | 227 | onBeginEditClient() { 228 | this.setState({isOpenClientDialog: true}); 229 | } 230 | 231 | onBeginEditPAC() { 232 | this.setState({isOpenPacDialog: true}); 233 | } 234 | 235 | // once settings confirmed 236 | 237 | onEditedServer() { 238 | this.setState({isOpenServerDialog: false}, this.onRestartApp); 239 | } 240 | 241 | onEditedClient() { 242 | this.setState({isOpenClientDialog: false}, this.onRestartApp); 243 | } 244 | 245 | onEditedPac() { 246 | this.setState({isOpenPacDialog: false}, this.onRestartApp); 247 | } 248 | 249 | onDeleteServer(index) { 250 | const {config} = this.state; 251 | this.setState({ 252 | config: { 253 | ...config, 254 | servers: config.servers.filter((s, i) => i !== index) 255 | } 256 | }, this.onRestartApp); 257 | } 258 | 259 | // toggles 260 | 261 | onToggleLocalService() { 262 | const {appStatus} = this.state; 263 | if (appStatus === STATUS_RUNNING) { 264 | this.onStopApp(); 265 | } else { 266 | this.onStartApp(); 267 | } 268 | } 269 | 270 | onToggleServer(index) { 271 | const {config} = this.state; 272 | this.setState({ 273 | config: { 274 | ...config, 275 | servers: config.servers.map((s, i) => ({ 276 | ...s, 277 | enabled: (i === index) ? !s.enabled : s.enabled 278 | })) 279 | } 280 | }, this.onRestartApp); 281 | } 282 | 283 | onTogglePac() { 284 | const {appStatus, pacStatus} = this.state; 285 | if (pacStatus === STATUS_RUNNING) { 286 | ipcRenderer.send(RENDERER_STOP_PAC); 287 | } else { 288 | const {host, port, pac_type, pac_host, pac_port, pac_custom_rules} = this.state.config; 289 | ipcRenderer.send(RENDERER_START_PAC, { 290 | type: pac_type, 291 | host: pac_host, 292 | port: pac_port, 293 | proxyHost: host, 294 | proxyPort: port, 295 | customRules: pac_custom_rules 296 | }); 297 | } 298 | if (appStatus === STATUS_RUNNING) { 299 | this.onRestartApp(); 300 | } 301 | } 302 | 303 | // qrcode 304 | 305 | onCopyQRCodeAsText(name) { 306 | ipcRenderer.send(RENDERER_COPY_QR_CODE_AS_TEXT, {name}); 307 | } 308 | 309 | onCopyQRCodeAsImage(name) { 310 | ipcRenderer.send(RENDERER_COPY_QR_CODE_AS_IMAGE, {name}); 311 | } 312 | 313 | onScannedQRCodeFromScreen(text) { 314 | const server = parseQrCodeText(text); 315 | if (server !== null) { 316 | const {config} = this.state; 317 | this.setState({ 318 | isOpenServerDialog: true, 319 | serverIndex: config.servers.length, 320 | config: { 321 | ...config, 322 | servers: config.servers.concat(server) 323 | } 324 | }); 325 | } else { 326 | toast('Invalid or unsupported QR code'); 327 | } 328 | } 329 | 330 | // private functions 331 | 332 | onSave() { 333 | const {config, appStatus, pacStatus} = this.state; 334 | if (config !== null) { 335 | ipcRenderer.send(RENDERER_SAVE_CONFIG, { 336 | ...config, 337 | app_status: appStatus, 338 | pac_status: pacStatus 339 | }); 340 | } 341 | } 342 | 343 | onStartApp() { 344 | const {appStatus, pacStatus, config} = this.state; 345 | if (appStatus === STATUS_OFF && appStatus !== STATUS_RESTARTING) { 346 | // 1. set pac or global proxy and bypass 347 | if (pacStatus === STATUS_RUNNING) { 348 | ipcRenderer.send(RENDERER_SET_SYS_PAC, {url: this.getPacUrl()}); 349 | } else { 350 | ipcRenderer.send(RENDERER_SET_SYS_PROXY, { 351 | host: config.host, 352 | port: config.port, 353 | bypass: config.bypass 354 | }); 355 | } 356 | // 2. start blinksocks client 357 | ipcRenderer.send(RENDERER_START_BS, {config}); 358 | } 359 | } 360 | 361 | onStopApp() { 362 | const {appStatus, config} = this.state; 363 | if (appStatus === STATUS_RUNNING) { 364 | // 1. restore all system settings 365 | ipcRenderer.send(RENDERER_RESTORE_SYS_PAC, {url: this.getPacUrl()}); 366 | ipcRenderer.send(RENDERER_RESTORE_SYS_PROXY, { 367 | host: config.host, 368 | port: config.port, 369 | bypass: config.bypass 370 | }); 371 | // 2. terminate blinksocks client 372 | ipcRenderer.send(RENDERER_STOP_BS); 373 | } 374 | } 375 | 376 | onRestartApp() { 377 | const {appStatus} = this.state; 378 | if (appStatus === STATUS_RUNNING) { 379 | this.onStopApp(); 380 | this.setState({appStatus: STATUS_RESTARTING}); 381 | setTimeout(this.onStartApp, 1000); 382 | } else { 383 | this.onSave(); 384 | } 385 | } 386 | 387 | getPacUrl() { 388 | const {pac_type, pac_host, pac_port, pac_remote_url} = this.state.config; 389 | return pac_type === 0 ? `http://${pac_host || 'localhost'}:${pac_port || 1090}` : (pac_remote_url || ''); 390 | } 391 | 392 | // state updater 393 | 394 | onEditingServer(server) { 395 | const {config, serverIndex} = this.state; 396 | if (serverIndex === -1) { 397 | // add a server 398 | this.setState({ 399 | serverIndex: config.servers.length, 400 | config: { 401 | ...config, 402 | servers: config.servers.concat(server) 403 | } 404 | }); 405 | } else { 406 | // edit a server 407 | this.setState({ 408 | config: { 409 | ...config, 410 | servers: config.servers.map((s, i) => (i === serverIndex) ? server : s) 411 | } 412 | }); 413 | } 414 | } 415 | 416 | onEditingLocal(newConfig) { 417 | const {config} = this.state; 418 | this.setState({ 419 | config: { 420 | ...config, 421 | ...newConfig 422 | } 423 | }); 424 | } 425 | 426 | render() { 427 | const {version, pacLastUpdatedAt, config} = this.state; 428 | const {appStatus, pacStatus, serverIndex, qrcodes} = this.state; 429 | const {isOpenDrawer} = this.state; 430 | const {isOpenClientDialog, isOpenServerDialog, isOpenPacDialog, isOpenQRCodeDialog} = this.state; 431 | 432 | if (config === null) { 433 | return ( 434 |
    Loading
    435 | ); 436 | } 437 | 438 | const server = config.servers[serverIndex]; 439 | 440 | return ( 441 |
    442 | {isOpenDrawer && } 443 | } 447 | /> 448 | 449 | 460 | 461 | 462 | {(server, i) => ( 463 | 471 | )} 472 | 473 | this.setState({isOpenClientDialog: false})}> 478 | 479 | 480 | this.setState({isOpenPacDialog: false})}> 485 | 486 | 487 | this.setState({isOpenServerDialog: false})}> 492 |
    493 | 497 | {server && ( 498 | } 501 | onTouchTap={this.onBeginCreateQRCode.bind(this, serverIndex)} 502 | fullWidth 503 | /> 504 | )} 505 |
    506 |
    507 | this.setState({isOpenQRCodeDialog: false})}> 511 | 516 | 517 |
    518 | ); 519 | } 520 | 521 | } 522 | -------------------------------------------------------------------------------- /src/containers/App/iceland.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/a25185736b931dabb50f42ac11c96d41ecfdf8e4/src/containers/App/iceland.woff2 -------------------------------------------------------------------------------- /src/containers/App/index.js: -------------------------------------------------------------------------------- 1 | export * from './App'; -------------------------------------------------------------------------------- /src/containers/AppSlider/AppSlider.css: -------------------------------------------------------------------------------- 1 | .appslider__header { 2 | display: flex; 3 | align-items: center; 4 | padding-left: 5px; 5 | height: 64px; 6 | background-color: #1e88e5; 7 | color: #fff; 8 | font-size: 1.5rem; 9 | } 10 | 11 | .appslider__header__logo { 12 | width: 45px; 13 | height: 45px; 14 | margin-right: 5px; 15 | } 16 | 17 | .appslider__footer { 18 | display: flex; 19 | position: absolute; 20 | bottom: 0; 21 | width: 100%; 22 | font-size: .85rem; 23 | } 24 | 25 | .appslider__footer__item { 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | margin: 0; 30 | padding: 10px 0; 31 | width: 50%; 32 | cursor: pointer; 33 | transition: all .2s linear; 34 | border-top: 2px solid #b2d1ef; 35 | } 36 | 37 | .appslider__footer__item:hover { 38 | border-top: 2px solid #0b61c5; 39 | background-color: #f2f2f2; 40 | } 41 | 42 | .notie-alert { 43 | z-index: 1500; 44 | cursor: pointer; 45 | } 46 | -------------------------------------------------------------------------------- /src/containers/AppSlider/AppSlider.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import marked from 'marked'; 4 | import dateFormat from 'date-fns/format'; 5 | import filesize from 'filesize'; 6 | import 'github-markdown-css'; 7 | 8 | import { 9 | MAIN_UPDATE_PAC, 10 | MAIN_UPDATE_PAC_FAIL, 11 | MAIN_UPDATE_SELF, 12 | MAIN_UPDATE_SELF_PROGRESS, 13 | MAIN_UPDATE_SELF_FAIL, 14 | RENDERER_UPDATE_PAC, 15 | RENDERER_UPDATE_SELF, 16 | RENDERER_UPDATE_SELF_CANCEL, 17 | RENDERER_QUIT 18 | } from '../../defs/events'; 19 | 20 | import { 21 | Dialog, 22 | Drawer, 23 | ListItem, 24 | FlatButton, 25 | Divider 26 | } from 'material-ui'; 27 | 28 | import {ActionHelpOutline, ActionExitToApp} from 'material-ui/svg-icons'; 29 | 30 | import {toast} from '../../helpers'; 31 | 32 | import './AppSlider.css'; 33 | import GithubSvg from './github.svg'; 34 | 35 | const {ipcRenderer} = window.require('electron'); 36 | 37 | const links = [{ 38 | text: 'Quit', 39 | icon: , 40 | onClick: () => ipcRenderer.send(RENDERER_QUIT) 41 | }, { 42 | text: 'Issues', 43 | icon: , 44 | href: 'https://github.com/blinksocks/blinksocks-desktop/issues' 45 | }, { 46 | text: 'Github', 47 | icon: Github, 48 | href: 'https://github.com/blinksocks/blinksocks-desktop' 49 | }]; 50 | 51 | const PACKAGE_JSON_URL = 'https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/master/package.json'; 52 | const CHANGELOG_URL = 'https://raw.githubusercontent.com/blinksocks/blinksocks-desktop/master/CHANGELOG.md'; 53 | const RELEASES_URL = 'https://github.com/blinksocks/blinksocks-desktop/releases'; 54 | 55 | export class AppSlider extends Component { 56 | 57 | static propTypes = { 58 | isOpen: PropTypes.bool.isRequired, 59 | version: PropTypes.string.isRequired, 60 | pacLastUpdatedAt: PropTypes.number.isRequired 61 | }; 62 | 63 | state = { 64 | isCheckingUpdates: false, 65 | isUpdatingPac: false, 66 | isUpdatingSelf: false, 67 | latestPackageJson: null, 68 | latestChangelog: '', 69 | pacLastUpdatedAt: this.props.pacLastUpdatedAt, 70 | selfUpdatingPercentage: 0, 71 | selfUpdatingTotalBytes: 0, 72 | isUpdateDialogShow: false 73 | }; 74 | 75 | constructor(props) { 76 | super(props); 77 | this.onCheckUpdates = this.onCheckUpdates.bind(this); 78 | this.onUpdateAndRestart = this.onUpdateAndRestart.bind(this); 79 | this.onFullDownload = this.onFullDownload.bind(this); 80 | this.onCancelUpdateSelf = this.onCancelUpdateSelf.bind(this); 81 | this.onUpdatePac = this.onUpdatePac.bind(this); 82 | } 83 | 84 | componentDidMount() { 85 | ipcRenderer.on(MAIN_UPDATE_PAC, (e, timestamp) => { 86 | this.setState({isUpdatingPac: false, pacLastUpdatedAt: timestamp}); 87 | }); 88 | ipcRenderer.on(MAIN_UPDATE_PAC_FAIL, (e, message) => { 89 | this.setState({isUpdatingPac: false}); 90 | toast(message, {stay: true}); 91 | }); 92 | ipcRenderer.on(MAIN_UPDATE_SELF_PROGRESS, (e, {percentage, totalBytes}) => { 93 | this.setState({selfUpdatingPercentage: percentage, selfUpdatingTotalBytes: totalBytes}); 94 | }); 95 | ipcRenderer.on(MAIN_UPDATE_SELF, () => { 96 | this.setState({isUpdatingSelf: false, isUpdateDialogShow: false}); 97 | }); 98 | ipcRenderer.on(MAIN_UPDATE_SELF_FAIL, (e, message) => { 99 | this.setState({isUpdatingSelf: false, selfUpdatingPercentage: 0}); 100 | toast(message, {stay: true}); 101 | }); 102 | } 103 | 104 | async onCheckUpdates() { 105 | const {version} = this.props; 106 | const {isCheckingUpdates} = this.state; 107 | 108 | if (!isCheckingUpdates) { 109 | this.setState({isCheckingUpdates: true}); 110 | try { 111 | // 1. fetch package.json 112 | const packageJsonResponse = await fetch(PACKAGE_JSON_URL); 113 | const packageJson = JSON.parse(await packageJsonResponse.text()); 114 | const isUpdateDialogShow = (version !== packageJson.version); 115 | 116 | // 2. fetch CHANGELOG.md 117 | const changelogResponse = await fetch(CHANGELOG_URL); 118 | const changelog = marked(await changelogResponse.text()); 119 | 120 | this.setState({ 121 | isCheckingUpdates: false, 122 | latestPackageJson: packageJson, 123 | latestChangelog: changelog, 124 | isUpdateDialogShow 125 | }); 126 | } catch (err) { 127 | toast(err.message); 128 | this.setState({ 129 | isCheckingUpdates: false, 130 | latestPackageJson: null, 131 | latestChangelog: '', 132 | isUpdateDialogShow: false 133 | }); 134 | } 135 | } 136 | } 137 | 138 | onUpdateAndRestart() { 139 | const {latestPackageJson: {version}} = this.state; 140 | this.setState({isUpdatingSelf: true}); 141 | ipcRenderer.send(RENDERER_UPDATE_SELF, {version}); 142 | } 143 | 144 | onFullDownload() { 145 | window.open(RELEASES_URL); 146 | } 147 | 148 | onCancelUpdateSelf() { 149 | this.setState({isUpdateDialogShow: false, isUpdatingSelf: false}); 150 | ipcRenderer.send(RENDERER_UPDATE_SELF_CANCEL); 151 | } 152 | 153 | onUpdatePac() { 154 | this.setState({isUpdatingPac: true}); 155 | ipcRenderer.send(RENDERER_UPDATE_PAC); 156 | } 157 | 158 | getVersionText() { 159 | const {version} = this.props; 160 | const {isCheckingUpdates, latestPackageJson} = this.state; 161 | let dynamic = ''; 162 | if (isCheckingUpdates) { 163 | dynamic = '? fetching...'; 164 | } else { 165 | if (latestPackageJson !== null) { 166 | if (latestPackageJson.version === version) { 167 | dynamic = `= v${latestPackageJson.version}(latest)`; 168 | } else { 169 | dynamic = `-> v${latestPackageJson.version}(latest)`; 170 | } 171 | } 172 | } 173 | return `Version: v${version} ${dynamic}`; 174 | } 175 | 176 | getPacDateTimeText() { 177 | const {isUpdatingPac, pacLastUpdatedAt} = this.state; 178 | if (isUpdatingPac) { 179 | return 'updating...'; 180 | } 181 | if (pacLastUpdatedAt !== 0) { 182 | return 'Last Updated: ' + dateFormat(pacLastUpdatedAt, 'YYYY/MM/DD HH:mm'); 183 | } 184 | return 'Last Updated: -'; 185 | } 186 | 187 | render() { 188 | const {isOpen} = this.props; 189 | const { 190 | isUpdateDialogShow, 191 | isUpdatingPac, 192 | isUpdatingSelf, 193 | latestPackageJson, 194 | latestChangelog, 195 | selfUpdatingPercentage, 196 | selfUpdatingTotalBytes 197 | } = this.state; 198 | return ( 199 | 200 |
    201 | blinksocks 202 |

    blinksocks

    203 |
    204 | 209 | 215 | 216 |
    217 | {links.map(({text, icon, href, onClick}, i) => ( 218 |

    window.open(href)}> 219 | {icon}{text} 220 |

    221 | ))} 222 |
    223 | , 233 | , 234 | 235 | ]} 236 | autoScrollBodyContent={true} 237 | > 238 |
    243 |
    244 |
    245 | ); 246 | } 247 | 248 | } 249 | -------------------------------------------------------------------------------- /src/containers/AppSlider/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/containers/AppSlider/index.js: -------------------------------------------------------------------------------- 1 | export * from './AppSlider'; -------------------------------------------------------------------------------- /src/containers/ClientEditor/ClientEditor.css: -------------------------------------------------------------------------------- 1 | .client-editor__dropdown { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .client-editor__dropdown span { 7 | display: block; 8 | margin: 10px 0 0 0; 9 | font-size: .8rem; 10 | color: rgba(0, 0, 0, 0.3); 11 | } 12 | -------------------------------------------------------------------------------- /src/containers/ClientEditor/ClientEditor.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { 5 | DropDownMenu, 6 | MenuItem, 7 | Subheader, 8 | TextField 9 | } from 'material-ui'; 10 | 11 | import './ClientEditor.css'; 12 | 13 | const LOG_LEVELS = ['error', 'warn', 'info', 'verbose', 'debug', 'silly']; 14 | 15 | export class ClientEditor extends Component { 16 | 17 | static propTypes = { 18 | config: PropTypes.object.isRequired, 19 | onEdit: PropTypes.func 20 | }; 21 | 22 | static defaultProps = { 23 | onEdit: (/* config */) => { 24 | } 25 | }; 26 | 27 | constructor(props) { 28 | super(props); 29 | this.onTextFieldChange = this.onTextFieldChange.bind(this); 30 | this.onLogLevelChange = this.onLogLevelChange.bind(this); 31 | } 32 | 33 | onTextFieldChange(e) { 34 | const {client} = this.props; 35 | const {name, value} = e.currentTarget; 36 | let _value = value; 37 | 38 | if (['port', 'timeout'].includes(name)) { 39 | _value = (value === '') ? '' : parseInt(value, 10); 40 | } 41 | 42 | if (['bypass', 'dns'].includes(name)) { 43 | _value = value.split('\n'); 44 | } 45 | 46 | this.props.onEdit({ 47 | ...client, 48 | [name]: _value 49 | }); 50 | } 51 | 52 | onLogLevelChange(level) { 53 | const {client} = this.props; 54 | this.props.onEdit({ 55 | ...client, 56 | log_level: level 57 | }); 58 | } 59 | 60 | render() { 61 | const {config} = this.props; 62 | return ( 63 |
    64 | Socks5/Socks4(a)/HTTP Service 65 | 73 | 81 | 90 | 98 |
    99 | Log Level 100 | this.onLogLevelChange(v)}> 101 | {LOG_LEVELS.map((v, i) => ( 102 | 103 | ))} 104 | 105 |
    106 | 115 |
    116 | ); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/containers/ClientEditor/index.js: -------------------------------------------------------------------------------- 1 | export * from './ClientEditor'; -------------------------------------------------------------------------------- /src/containers/General/General.css: -------------------------------------------------------------------------------- 1 | .service__info { 2 | color: rgba(0, 0, 0, 0.541176); 3 | word-break: break-all; 4 | overflow: hidden; 5 | text-overflow: ellipsis; 6 | line-height: 20px; 7 | font-size: .9rem; 8 | } 9 | 10 | .service__info b { 11 | margin-left: 5px; 12 | } 13 | 14 | .service__control { 15 | position: absolute; 16 | top: 30px; 17 | right: 5px; 18 | width: 60px; 19 | } 20 | -------------------------------------------------------------------------------- /src/containers/General/General.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import QrCode from 'qrcode-reader'; 4 | 5 | import { 6 | IconButton, 7 | List, 8 | ListItem, 9 | Toggle, 10 | Subheader 11 | } from 'material-ui'; 12 | 13 | import { 14 | ImageCropFree, 15 | ActionPowerSettingsNew, 16 | ImageTransform, 17 | ContentAdd 18 | } from 'material-ui/svg-icons'; 19 | 20 | import {toast, takeScreenShot} from '../../helpers'; 21 | 22 | import './General.css'; 23 | 24 | const STATUS_OFF = 0; 25 | const STATUS_RUNNING = 1; 26 | const STATUS_STARTING = 2; 27 | const STATUS_RESTARTING = 3; 28 | 29 | const ServiceInfo = ({address, status}) => ( 30 | 31 | ● {address} 32 |
    33 | ● status: 34 | 42 | {status === STATUS_OFF && 'off'} 43 | {status === STATUS_RUNNING && 'running'} 44 | {status === STATUS_STARTING && 'starting'} 45 | {status === STATUS_RESTARTING && 'restarting'} 46 | 47 |
    48 | ); 49 | 50 | const ServiceControl = ({status, onToggle}) => ( 51 |
    52 | 57 |
    58 | ); 59 | 60 | export class General extends Component { 61 | 62 | static propTypes = { 63 | config: PropTypes.object.isRequired, 64 | appStatus: PropTypes.number.isRequired, 65 | pacStatus: PropTypes.number.isRequired, 66 | onOpenClientDialog: PropTypes.func, 67 | onOpenPacDialog: PropTypes.func, 68 | onOpenServerDialog: PropTypes.func, 69 | onToggleClientService: PropTypes.func, 70 | onTogglePacService: PropTypes.func, 71 | onScannedQRCode: PropTypes.func 72 | }; 73 | 74 | static defaultProps = { 75 | onOpenClientDialog: () => { 76 | }, 77 | onOpenPacDialog: () => { 78 | }, 79 | onOpenServerDialog: () => { 80 | }, 81 | onToggleClientService: () => { 82 | }, 83 | onTogglePacService: () => { 84 | }, 85 | onScannedQRCode: (/* text */) => { 86 | } 87 | }; 88 | 89 | state = { 90 | scanning: false 91 | }; 92 | 93 | constructor(props) { 94 | super(props); 95 | this.onScanQRCode = this.onScanQRCode.bind(this); 96 | } 97 | 98 | async onScanQRCode() { 99 | try { 100 | this.setState({scanning: true}); 101 | const reader = new QrCode(); 102 | reader.callback = (err, data) => { 103 | if (err) { 104 | toast('Cannot find QR code!
    Try to zoom-in and put it to the center of the screen.'); 105 | } else { 106 | this.props.onScannedQRCode(data.result); 107 | } 108 | this.setState({scanning: false}); 109 | }; 110 | reader.decode(await takeScreenShot()); 111 | } catch (err) { 112 | toast(err.message); 113 | this.setState({scanning: false}); 114 | } 115 | } 116 | 117 | render() { 118 | const {config, appStatus, pacStatus} = this.props; 119 | const {onOpenClientDialog, onOpenPacDialog, onOpenServerDialog} = this.props; 120 | const {onToggleClientService, onTogglePacService} = this.props; 121 | const {scanning} = this.state; 122 | 123 | // Quick Fix: touch propagation 124 | const onTouchTap = (e, callback) => { 125 | if (e.target.nodeName !== 'INPUT' && typeof callback === 'function') { 126 | callback(); 127 | } 128 | }; 129 | 130 | return ( 131 | 132 | General 133 | } 135 | primaryText="BLINKSOCKS CLIENT" 136 | secondaryText={} 137 | secondaryTextLines={2} 138 | rightIconButton={} 139 | onTouchTap={(e) => onTouchTap(e, onOpenClientDialog)} 140 | /> 141 | } 143 | primaryText="PAC SERVICE" 144 | secondaryText={ 145 | 149 | } 150 | secondaryTextLines={2} 151 | rightIconButton={} 152 | onTouchTap={(e) => onTouchTap(e, onOpenPacDialog)} 153 | /> 154 | } 156 | primaryText="ADD A SERVER" 157 | secondaryText="Add blinksocks/shadowsocks server" 158 | rightIconButton={ 159 | 164 | 165 | 166 | } 167 | onTouchTap={(e) => onTouchTap(e, onOpenServerDialog)} 168 | /> 169 | 170 | ); 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/containers/General/index.js: -------------------------------------------------------------------------------- 1 | export * from './General'; -------------------------------------------------------------------------------- /src/containers/Logs/Logs.css: -------------------------------------------------------------------------------- 1 | .logs { 2 | font-size: .95rem; 3 | } 4 | 5 | /* log toolbox */ 6 | 7 | .logs__toolbox { 8 | position: fixed; 9 | display: flex; 10 | flex-direction: column; 11 | box-shadow: 0 2px 10px #b1b1b1; 12 | width: 100%; 13 | z-index: 1; 14 | } 15 | 16 | .logs__toolbox__line { 17 | display: flex; 18 | flex-wrap: nowrap; 19 | align-items: center; 20 | padding: 0 10px; 21 | } 22 | 23 | .logs__toolbox__line > * { 24 | margin-right: 10px; 25 | white-space: nowrap; 26 | } 27 | 28 | .logs__toolbox__line:last-child { 29 | margin-bottom: 10px; 30 | } 31 | 32 | .logs__toolbox__range { 33 | display: flex; 34 | align-items: center; 35 | width: 260px; 36 | } 37 | 38 | .logs__toolbox__search { 39 | box-sizing: border-box; 40 | padding: 5px; 41 | width: 260px; 42 | border: 1px solid #dfdfdf; 43 | } 44 | 45 | .logs__toolbox__watch input { 46 | margin-right: 5px; 47 | } 48 | 49 | .logs__toolbox__results span { 50 | display: inline-block; 51 | position: relative; 52 | top: -1px; 53 | margin-right: 5px; 54 | border-bottom: 1px dotted #5c5c5c; 55 | font-size: 85%; 56 | color: #5c5c5c; 57 | } 58 | 59 | /* log legends */ 60 | 61 | .logs__toolbox__legends { 62 | display: flex; 63 | list-style: none; 64 | margin: 0 0 5px; 65 | padding: 0; 66 | font-size: .85rem; 67 | } 68 | 69 | .logs__toolbox__legends__item { 70 | display: flex; 71 | flex-direction: column; 72 | justify-content: center; 73 | align-items: center; 74 | } 75 | 76 | .logs__toolbox__legends__item__label { 77 | margin: 10px 5px 2px 0; 78 | padding: 3px; 79 | border: 1px solid #e1e1e1; 80 | border-radius: 3px; 81 | width: 60px; 82 | text-align: center; 83 | cursor: pointer; 84 | } 85 | 86 | .logs__toolbox__legends__item__indicator { 87 | width: 5px; 88 | height: 5px; 89 | border-radius: 50%; 90 | background-color: #4caf50; 91 | } 92 | 93 | .logs__toolbox__legends__item__indicator--transparent { 94 | background-color: transparent; 95 | } 96 | 97 | /* log items */ 98 | 99 | .logs__items { 100 | position: fixed; 101 | top: 135px; 102 | bottom: 0; 103 | width: 100%; 104 | overflow: auto; 105 | } 106 | 107 | .logs__item { 108 | padding: 5px 10px; 109 | } 110 | 111 | .logs__item p { 112 | margin: 3px 0 0; 113 | word-break: break-all; 114 | } 115 | 116 | .logs__item__timestamp { 117 | border-bottom: 1px dotted #777; 118 | font-size: 95%; 119 | text-decoration: none; 120 | } 121 | 122 | .logs__item__message { 123 | color: #1c1c1c; 124 | } 125 | 126 | /* background colors */ 127 | 128 | .logs__item--error { 129 | background-color: #f18565; 130 | } 131 | 132 | .logs__item--warn { 133 | background-color: #ffeaad; 134 | } 135 | 136 | .logs__item--info { 137 | background-color: #f8fbff; 138 | } 139 | 140 | .logs__item--verbose { 141 | background-color: #f8fbff; 142 | } 143 | 144 | .logs__item--debug { 145 | background-color: #93cfff; 146 | } 147 | 148 | .logs__item--silly { 149 | background-color: #f8fbff; 150 | } 151 | -------------------------------------------------------------------------------- /src/containers/Logs/Logs.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PureComponent} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Tabs, Tab} from 'material-ui'; 4 | import formatDate from 'date-fns/format'; 5 | import endOfTomorrow from 'date-fns/end_of_tomorrow'; 6 | import debounce from 'lodash.debounce'; 7 | 8 | import { 9 | RENDERER_QUERY_BS_LOG, 10 | RENDERER_QUERY_BSD_LOG, 11 | RENDERER_STREAM_BS_LOG, 12 | RENDERER_STREAM_BSD_LOG, 13 | MAIN_QUERY_BS_LOG, 14 | MAIN_QUERY_BSD_LOG, 15 | MAIN_STREAM_BS_LOG, 16 | MAIN_STREAM_BSD_LOG 17 | } from '../../defs/events'; 18 | 19 | import {DatePicker} from '../../components'; 20 | 21 | import './Logs.css'; 22 | 23 | const {ipcRenderer} = window.require('electron'); 24 | const TAB_BLINKSOCKS = 0; 25 | const TAB_BLINKSOCKS_DESKTOP = 1; 26 | 27 | class LogLevelFilters extends PureComponent { 28 | 29 | static LOG_LEVELS = ['error', 'warn', 'info', 'verbose', 'debug', 'silly']; 30 | 31 | static propTypes = { 32 | level: PropTypes.string, 33 | onFilter: PropTypes.func 34 | }; 35 | 36 | static defaultProps = { 37 | level: '', 38 | onFilter: () => { 39 | } 40 | }; 41 | 42 | render() { 43 | const levels = LogLevelFilters.LOG_LEVELS; 44 | return ( 45 |
      46 | {levels.map((level, i) => ( 47 |
    • this.props.onFilter(level)}> 48 |
      {level}
      49 |
      55 |
    • 56 | ))} 57 |
    58 | ); 59 | } 60 | 61 | } 62 | 63 | class LogItems extends PureComponent { 64 | 65 | static propTypes = { 66 | logs: PropTypes.array 67 | }; 68 | 69 | static defaultProps = { 70 | logs: [] 71 | }; 72 | 73 | render() { 74 | return ( 75 |
    76 | {this.props.logs.map(({level, message, timestamp}, i) => ( 77 |
    78 | {formatDate(timestamp, 'YYYY-MM-DD HH:mm:ss')} 79 |

    {message}

    80 |
    81 | ))} 82 |
    83 | ); 84 | } 85 | 86 | } 87 | 88 | export class Logs extends Component { 89 | 90 | state = { 91 | tabIndex: TAB_BLINKSOCKS, 92 | orgLogs: [], 93 | logs: [], 94 | filterLevel: '', 95 | searchKeywords: '', 96 | isWatch: false 97 | }; 98 | 99 | constructor(props) { 100 | super(props); 101 | this.onTabChange = this.onTabChange.bind(this); 102 | this.onFilterLevel = this.onFilterLevel.bind(this); 103 | this.onRangeChange = this.onRangeChange.bind(this); 104 | this.onSearch = debounce(this.onSearch, 300); 105 | this.onToggleWatch = this.onToggleWatch.bind(this); 106 | } 107 | 108 | componentDidMount() { 109 | this.onTabChange(this.state.tabIndex); 110 | const setStateThenFilter = ({logs}) => { 111 | this.setState({logs, orgLogs: logs}); 112 | const {filterLevel, searchKeywords} = this.state; 113 | if (filterLevel !== '') { 114 | this.onFilterLevel(filterLevel); 115 | } 116 | if (searchKeywords !== '') { 117 | this.onSearch(searchKeywords); 118 | } 119 | }; 120 | ipcRenderer.on(MAIN_QUERY_BS_LOG, (e, {logs}) => setStateThenFilter({logs})); 121 | ipcRenderer.on(MAIN_QUERY_BSD_LOG, (e, {logs}) => setStateThenFilter({logs})); 122 | ipcRenderer.on(MAIN_STREAM_BS_LOG, (e, {log}) => setStateThenFilter({logs: [log, ...this.state.orgLogs]})); 123 | ipcRenderer.on(MAIN_STREAM_BSD_LOG, (e, {log}) => setStateThenFilter({logs: [log, ...this.state.orgLogs]})); 124 | } 125 | 126 | onTabChange(value) { 127 | switch (value) { 128 | case TAB_BLINKSOCKS: 129 | ipcRenderer.send(RENDERER_QUERY_BS_LOG); 130 | break; 131 | case TAB_BLINKSOCKS_DESKTOP: 132 | ipcRenderer.send(RENDERER_QUERY_BSD_LOG); 133 | break; 134 | default: 135 | break; 136 | } 137 | this.setState({tabIndex: value}); 138 | this.onToggleWatch(false); 139 | } 140 | 141 | onFilterLevel(level) { 142 | const {filterLevel, orgLogs} = this.state; 143 | if (level === filterLevel) { 144 | this.setState({ 145 | logs: orgLogs, 146 | filterLevel: '', 147 | searchKeywords: '' 148 | }); 149 | } else { 150 | this.setState({ 151 | logs: orgLogs.filter((log) => log.level === level), 152 | filterLevel: level, 153 | searchKeywords: '' 154 | }); 155 | } 156 | } 157 | 158 | onRangeChange(selectedDates) { 159 | if (selectedDates.length === 2) { 160 | const type = { 161 | [TAB_BLINKSOCKS]: RENDERER_QUERY_BS_LOG, 162 | [TAB_BLINKSOCKS_DESKTOP]: RENDERER_QUERY_BSD_LOG 163 | }[this.state.tabIndex]; 164 | ipcRenderer.send(type, { 165 | from: formatDate(selectedDates[0]), 166 | until: formatDate(selectedDates[1]) 167 | }); 168 | this.setState({ 169 | filterLevel: '', 170 | searchKeywords: '' 171 | }); 172 | } 173 | } 174 | 175 | onSearch(keywords) { 176 | const {orgLogs} = this.state; 177 | this.setState({ 178 | logs: keywords === '' ? orgLogs : orgLogs.filter(({level, message, timestamp}) => ( 179 | level.indexOf(keywords) !== -1 || 180 | message.indexOf(keywords) !== -1 || 181 | timestamp.indexOf(keywords) !== -1 182 | )), 183 | filterLevel: '', 184 | searchKeywords: keywords 185 | }); 186 | } 187 | 188 | onToggleWatch(isForceWatch) { 189 | this.setState({isWatch: (typeof isForceWatch !== 'undefined') ? isForceWatch : !this.state.isWatch}, () => { 190 | const {tabIndex, isWatch} = this.state; // take care of the state here 191 | const _isWatch = (typeof isForceWatch !== 'undefined') ? isForceWatch : isWatch; 192 | switch (tabIndex) { 193 | case TAB_BLINKSOCKS: 194 | ipcRenderer.send(RENDERER_STREAM_BS_LOG, _isWatch); 195 | break; 196 | case TAB_BLINKSOCKS_DESKTOP: 197 | ipcRenderer.send(RENDERER_STREAM_BSD_LOG, _isWatch); 198 | break; 199 | default: 200 | break; 201 | } 202 | }); 203 | } 204 | 205 | render() { 206 | const {tabIndex, logs, orgLogs, filterLevel, isWatch} = this.state; 207 | return ( 208 |
    209 | 210 | 211 | 212 | 213 |
    214 |
    215 | 216 |
    217 |
    218 |
    219 | 0 ? formatDate(logs[logs.length - 1].timestamp, 'Y-m-d') : 'today', 227 | 'today' 228 | ], 229 | onChange: this.onRangeChange 230 | }} 231 | /> 232 |
    233 | e.target.select()} 238 | onChange={(e) => this.onSearch(e.target.value)} 239 | /> 240 | 244 |
    245 | {logs.length}/{orgLogs.length} 246 | 247 |
    248 |
    249 |
    250 | 251 |
    252 | ); 253 | } 254 | 255 | } 256 | -------------------------------------------------------------------------------- /src/containers/Logs/index.js: -------------------------------------------------------------------------------- 1 | export * from './Logs'; -------------------------------------------------------------------------------- /src/containers/PacEditor/PacEditor.css: -------------------------------------------------------------------------------- 1 | .paceditor { 2 | 3 | } 4 | 5 | .paceditor__type { 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | margin-top: 20px; 10 | } 11 | 12 | .paceditor__type label { 13 | margin-right: 10px; 14 | cursor: pointer; 15 | } 16 | -------------------------------------------------------------------------------- /src/containers/PacEditor/PacEditor.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import {TextField} from 'material-ui'; 5 | 6 | import './PacEditor.css'; 7 | 8 | const PAC_TYPE_LOCAL = 0; 9 | const PAC_TYPE_REMOTE = 1; 10 | 11 | export class PacEditor extends Component { 12 | 13 | static propTypes = { 14 | config: PropTypes.object.isRequired, 15 | onEdit: PropTypes.func 16 | }; 17 | 18 | static defaultProps = { 19 | onEdit: (/* config */) => { 20 | } 21 | }; 22 | 23 | constructor(props) { 24 | super(props); 25 | this.onTextFieldChange = this.onTextFieldChange.bind(this); 26 | } 27 | 28 | onSwitchType(type) { 29 | const {config} = this.props; 30 | this.props.onEdit({ 31 | ...config, 32 | pac_type: type 33 | }); 34 | } 35 | 36 | onTextFieldChange(e) { 37 | const {config} = this.props; 38 | const {name, value} = e.currentTarget; 39 | let _value = e.currentTarget.value; 40 | 41 | if (name === 'pac_custom_rules') { 42 | _value = value.split('\n'); 43 | } 44 | 45 | if (name === 'pac_port') { 46 | _value = parseInt(value || '1090', 10); 47 | } 48 | 49 | this.props.onEdit({ 50 | ...config, 51 | [name]: _value 52 | }); 53 | } 54 | 55 | render() { 56 | const {config} = this.props; 57 | const type = config.pac_type; 58 | return ( 59 |
    60 |
    61 | 70 | 79 |
    80 | {type === PAC_TYPE_LOCAL && ( 81 |
    82 | 90 | 98 | 107 |
    108 | )} 109 | {type === PAC_TYPE_REMOTE && ( 110 |
    111 | 120 |
    121 | )} 122 |
    123 | ); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/containers/PacEditor/index.js: -------------------------------------------------------------------------------- 1 | export * from './PacEditor'; -------------------------------------------------------------------------------- /src/containers/PresetEditor/PresetEditor.css: -------------------------------------------------------------------------------- 1 | .preset-editor { 2 | margin: 10px 0 0 0; 3 | padding: 0; 4 | list-style: none; 5 | } 6 | -------------------------------------------------------------------------------- /src/containers/PresetEditor/PresetEditor.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import {PresetParamItem} from '../../components'; 5 | import './PresetEditor.css'; 6 | 7 | export class PresetEditor extends Component { 8 | 9 | static propTypes = { 10 | preset: PropTypes.object.isRequired, 11 | def: PropTypes.array.isRequired, 12 | onEdit: PropTypes.func 13 | }; 14 | 15 | static defaultProps = { 16 | onEdit: (/* preset */) => { 17 | } 18 | }; 19 | 20 | render() { 21 | const {preset, def} = this.props; 22 | const {name, params} = preset; 23 | return ( 24 |
      25 | {def.map((d, i) => ( 26 | this.props.onEdit({ 31 | ...preset, 32 | params: {...params, [key]: value} 33 | })} 34 | /> 35 | ))} 36 | {def.length === 0 && `no parameters for "${name}" currently.`} 37 |
    38 | ); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/containers/PresetEditor/index.js: -------------------------------------------------------------------------------- 1 | export * from './PresetEditor'; -------------------------------------------------------------------------------- /src/containers/QRCode/QRCode.css: -------------------------------------------------------------------------------- 1 | .qrcodes { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | } 7 | 8 | .qrcodes__qrcode { 9 | margin-bottom: 20px; 10 | } 11 | 12 | .qrcodes__qrcode__name { 13 | text-align: center; 14 | } 15 | 16 | .qrcodes__qrcode__image { 17 | width: 100%; 18 | height: 100%; 19 | } 20 | 21 | .qrcodes__qrcode__tools { 22 | display: flex; 23 | justify-content: center; 24 | } 25 | -------------------------------------------------------------------------------- /src/containers/QRCode/QRCode.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {FlatButton} from 'material-ui'; 4 | import './QRCode.css'; 5 | 6 | export class QRCode extends Component { 7 | 8 | static propTypes = { 9 | qrcodes: PropTypes.object, 10 | onCopyText: PropTypes.func, 11 | onCopyImage: PropTypes.func 12 | }; 13 | 14 | static defaultProps = { 15 | qrcodes: {}, 16 | onCopyText: (/* name */) => { 17 | }, 18 | onCopyImage: (/* name */) => { 19 | } 20 | }; 21 | 22 | render() { 23 | const {qrcodes, onCopyText, onCopyImage} = this.props; 24 | const names = Object.keys(qrcodes); 25 | return ( 26 |
    27 | {names.map((name) => ( 28 |
    29 |
    {name}
    30 | {name} 31 |
    32 | onCopyText(name)} label="Copy Text"/> 33 | onCopyImage(name)} label="Copy Image"/> 34 |
    35 |
    36 | ))} 37 |
    38 | ); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/containers/QRCode/index.js: -------------------------------------------------------------------------------- 1 | export * from './QRCode'; -------------------------------------------------------------------------------- /src/containers/ServerEditor/ServerEditor.css: -------------------------------------------------------------------------------- 1 | .server-editor__label { 2 | display: block; 3 | margin: 10px 0; 4 | font-size: .8rem; 5 | color: rgba(0, 0, 0, 0.3); 6 | } 7 | 8 | .server-editor__presets { 9 | margin: 0; 10 | padding: 0; 11 | list-style: none; 12 | } 13 | 14 | .server-editor__passwitch { 15 | position: relative; 16 | float: right; 17 | top: -36px; 18 | right: 5px; 19 | cursor: pointer; 20 | transform: scale(0.8); 21 | } 22 | -------------------------------------------------------------------------------- /src/containers/ServerEditor/ServerEditor.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | TextField, 5 | FlatButton, 6 | Popover, 7 | Menu, 8 | MenuItem 9 | } from 'material-ui'; 10 | 11 | import {ContentAdd, ActionVisibility, ActionVisibilityOff} from 'material-ui/svg-icons'; 12 | 13 | import {PresetItem, PopupDialog} from '../../components'; 14 | import {PresetEditor} from '../../containers'; 15 | import {defs as PRESET_DEFS} from '../../defs/presets'; 16 | import './ServerEditor.css'; 17 | 18 | export class ServerEditor extends Component { 19 | 20 | static propTypes = { 21 | server: PropTypes.object.isRequired, 22 | onEdit: PropTypes.func 23 | }; 24 | 25 | static defaultProps = { 26 | onEdit: (/* server */) => { 27 | } 28 | }; 29 | 30 | state = { 31 | isDisplayPresetEditor: false, 32 | isDisplayPresetSelector: false, 33 | isDisplayKey: false, 34 | anchorEl: null, 35 | presetIndex: -1 36 | }; 37 | 38 | constructor(props) { 39 | super(props); 40 | this.onBeginEditPreset = this.onBeginEditPreset.bind(this); 41 | this.onBeginAddPreset = this.onBeginAddPreset.bind(this); 42 | this.onAddPreset = this.onAddPreset.bind(this); 43 | this.onEditPreset = this.onEditPreset.bind(this); 44 | this.onDeletePreset = this.onDeletePreset.bind(this); 45 | this.onEditTextField = this.onEditTextField.bind(this); 46 | this.onToggleKeyVisible = this.onToggleKeyVisible.bind(this); 47 | } 48 | 49 | onBeginEditPreset(index) { 50 | this.setState({presetIndex: index, isDisplayPresetEditor: true}); 51 | } 52 | 53 | onBeginAddPreset(e) { 54 | e.preventDefault(); 55 | this.setState({ 56 | isDisplayPresetSelector: true, 57 | anchorEl: e.currentTarget 58 | }); 59 | } 60 | 61 | onEditTextField(e) { 62 | const {server} = this.props; 63 | const {name, value} = e.currentTarget; 64 | 65 | let _value = value; 66 | if (name === 'port') { 67 | _value = (value === '') ? '' : parseInt(value, 10); 68 | } 69 | 70 | this.props.onEdit({ 71 | ...server, 72 | [name]: _value 73 | }); 74 | } 75 | 76 | onAddPreset(name) { 77 | const {server} = this.props; 78 | const def = PRESET_DEFS[name]; 79 | const params = {}; 80 | for (const {key, defaultValue} of def) { 81 | params[key] = defaultValue; 82 | } 83 | this.props.onEdit({ 84 | ...server, 85 | presets: server.presets.concat({ 86 | name, 87 | params 88 | }) 89 | }); 90 | this.setState({isDisplayPresetSelector: false}); 91 | } 92 | 93 | onEditPreset(preset) { 94 | const {server} = this.props; 95 | const {presetIndex} = this.state; 96 | this.props.onEdit({ 97 | ...server, 98 | presets: server.presets.map((p, i) => (i === presetIndex) ? preset : p) 99 | }); 100 | } 101 | 102 | onDeletePreset(index) { 103 | const {server} = this.props; 104 | this.props.onEdit({ 105 | ...server, 106 | presets: server.presets.filter((preset, i) => i !== index) 107 | }); 108 | } 109 | 110 | onToggleKeyVisible() { 111 | this.setState({isDisplayKey: !this.state.isDisplayKey}); 112 | } 113 | 114 | render() { 115 | const {server} = this.props; 116 | const { 117 | isDisplayPresetEditor, 118 | isDisplayPresetSelector, 119 | isDisplayKey, 120 | anchorEl, 121 | presetIndex 122 | } = this.state; 123 | const preset = server.presets[presetIndex] || {}; 124 | return ( 125 |
    126 | 133 | 141 | 149 |
    150 | {isDisplayKey ? : } 151 |
    152 | 153 |
      154 | {server.presets.map((preset, i) => ( 155 | 162 | ))} 163 |
    164 | } 167 | onTouchTap={this.onBeginAddPreset} 168 | secondary 169 | fullWidth 170 | /> 171 | 178 | this.setState({isDisplayPresetSelector: false})}> 184 | 185 | {Object.keys(PRESET_DEFS).map((name, i) => ( 186 | this.onAddPreset(name)} 190 | /> 191 | ))} 192 | 193 | 194 | this.setState({isDisplayPresetEditor: false})}> 198 | {Object.keys(preset).length > 0 ? ( 199 | 204 | ) : null} 205 | 206 |
    207 | ); 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /src/containers/ServerEditor/index.js: -------------------------------------------------------------------------------- 1 | export * from './ServerEditor'; -------------------------------------------------------------------------------- /src/containers/ServerList/ServerList.css: -------------------------------------------------------------------------------- 1 | .serverlist__servers { 2 | position: absolute; 3 | max-height: calc(100% - 420px); 4 | width: 100%; 5 | overflow-y: auto; 6 | } 7 | -------------------------------------------------------------------------------- /src/containers/ServerList/ServerList.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {List, Subheader} from 'material-ui'; 4 | import './ServerList.css'; 5 | 6 | export class ServerList extends Component { 7 | 8 | static propTypes = { 9 | servers: PropTypes.array, 10 | children: PropTypes.func 11 | }; 12 | 13 | static defaultProps = { 14 | servers: [], 15 | children: (/* server, i */) => { 16 | } 17 | }; 18 | 19 | render() { 20 | const {servers, children} = this.props; 21 | return ( 22 | 23 | 24 | Servers({`total: ${servers.length} active: ${servers.filter((s) => s.enabled).length}`}) 25 | 26 |
    27 | {servers.map((server, i) => children(server, i))} 28 |
    29 |
    30 | ); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/containers/ServerList/index.js: -------------------------------------------------------------------------------- 1 | export * from './ServerList'; -------------------------------------------------------------------------------- /src/containers/index.js: -------------------------------------------------------------------------------- 1 | export * from './App'; 2 | export * from './AppSlider'; 3 | export * from './General'; 4 | export * from './ServerList'; 5 | export * from './ClientEditor'; 6 | export * from './ServerEditor'; 7 | export * from './PacEditor'; 8 | export * from './PresetEditor'; 9 | export * from './Logs'; 10 | export * from './QRCode'; 11 | -------------------------------------------------------------------------------- /src/defs/bs-config-template.js: -------------------------------------------------------------------------------- 1 | module.exports.DEFAULT_CONFIG_STRUCTURE = { 2 | host: 'localhost', 3 | port: 1080, 4 | dns: [], 5 | servers: [{ 6 | enabled: false, 7 | remarks: 'Default Server', 8 | transport: 'tcp', 9 | host: 'example.com', 10 | port: 23333, 11 | key: '', 12 | presets: [{ 13 | name: 'ss-base', 14 | params: {} 15 | }] 16 | }], 17 | timeout: 600, 18 | profile: false, 19 | watch: false, 20 | log_level: 'info', 21 | bypass: [ 22 | '127.0.0.1', '', 23 | 'localhost', '127.*', '10.*', 24 | '172.16.*', '172.17.*', '172.18.*', 25 | '172.19.*', '172.20.*', '172.21.*', 26 | '172.22.*', '172.23.*', '172.24.*', 27 | '172.25.*', '172.26.*', '172.27.*', 28 | '172.28.*', '172.29.*', '172.30.*', 29 | '172.31.*', '172.32.*', '192.168.*' 30 | ], 31 | pac_type: 0, 32 | pac_host: 'localhost', 33 | pac_port: 1090, 34 | pac_custom_rules: [], 35 | pac_remote_url: 'http://localhost:1090/proxy.pac', 36 | pac_status: 0, 37 | app_status: 0 38 | }; -------------------------------------------------------------------------------- /src/defs/events.js: -------------------------------------------------------------------------------- 1 | /** 2 | * prefix every event with both "RENDERER_" and "MAIN_" 3 | * @param events 4 | * @returns {{}} 5 | */ 6 | function makePairs(events) { 7 | const map = {}; 8 | for (const e of events) { 9 | const le = e.toLowerCase(); 10 | map[`RENDERER_${e}`] = `renderer/${le}`; 11 | map[`MAIN_${e}`] = `main/${le}`; 12 | } 13 | return map; 14 | } 15 | 16 | module.exports = Object.assign(makePairs([ 17 | 'INIT', 18 | 'ERROR', 19 | 'QUIT', 20 | 'SAVE_CONFIG', 21 | 'START_BS', 22 | 'STOP_BS', 23 | 'START_PAC', 24 | 'STOP_PAC', 25 | 'SET_SYS_PAC', 26 | 'SET_SYS_PROXY', 27 | 'RESTORE_SYS_PAC', 28 | 'RESTORE_SYS_PROXY', 29 | 'UPDATE_PAC', 30 | 'UPDATE_PAC_FAIL', 31 | 'UPDATE_SELF', 32 | 'UPDATE_SELF_PROGRESS', 33 | 'UPDATE_SELF_FAIL', 34 | 'UPDATE_SELF_CANCEL', 35 | 'PREVIEW_LOGS', 36 | 'QUERY_BS_LOG', 37 | 'QUERY_BSD_LOG', 38 | 'STREAM_BS_LOG', 39 | 'STREAM_BSD_LOG', 40 | 'CREATE_QR_CODE', 41 | 'COPY_QR_CODE_AS_IMAGE', 42 | 'COPY_QR_CODE_AS_TEXT' 43 | ])); 44 | -------------------------------------------------------------------------------- /src/defs/presets.js: -------------------------------------------------------------------------------- 1 | export const defs = { 2 | 'ss-base': [], 3 | 'ss-stream-cipher': [{ 4 | key: 'method', 5 | type: 'enum', 6 | values: [ 7 | 'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr', 8 | 'aes-128-cfb', 'aes-192-cfb', 'aes-256-cfb', 9 | 'camellia-128-cfb', 'camellia-192-cfb', 'camellia-256-cfb', 10 | 'aes-128-ofb', 'aes-192-ofb', 'aes-256-ofb', 11 | 'aes-128-cbc', 'aes-192-cbc', 'aes-256-cbc' 12 | ], 13 | defaultValue: 'aes-256-cfb' 14 | }], 15 | 'ss-aead-cipher': [{ 16 | key: 'method', 17 | type: 'enum', 18 | values: [ 19 | 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm' 20 | ], 21 | defaultValue: 'aes-256-gcm' 22 | }, { 23 | key: 'info', 24 | type: 'string', 25 | defaultValue: 'ss-subkey' 26 | }], 27 | 'aead-random-cipher': [{ 28 | key: 'method', 29 | type: 'enum', 30 | values: [ 31 | 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm' 32 | ], 33 | defaultValue: 'aes-256-gcm' 34 | }, { 35 | key: 'info', 36 | type: 'string', 37 | defaultValue: 'bs-subkey' 38 | }, { 39 | key: 'factor', 40 | type: 'number', 41 | defaultValue: 2 42 | }], 43 | 'obfs-http': [{ 44 | key: 'file', 45 | type: 'string', 46 | defaultValue: '' 47 | }], 48 | 'obfs-tls1.2-ticket': [{ 49 | key: 'sni', 50 | type: 'array', 51 | defaultValue: [] 52 | }] 53 | }; 54 | 55 | export function isPresetsCompatibleToSS(presets) { 56 | if (!Array.isArray(presets)) { 57 | return false; 58 | } 59 | if (presets.length !== 2) { 60 | return false; 61 | } 62 | if (presets[0].name !== 'ss-base') { 63 | return false; 64 | } 65 | if (!['ss-stream-cipher', 'ss-aead-cipher'].includes(presets[1].name)) { 66 | return false; 67 | } 68 | if (presets[1].name === 'ss-aead-cipher' && presets[1].params.info !== 'ss-subkey') { 69 | return false; 70 | } 71 | return true; 72 | } 73 | -------------------------------------------------------------------------------- /src/helpers/index.js: -------------------------------------------------------------------------------- 1 | export * from './toast'; 2 | export * from './screenshot'; 3 | export * from './qrcode'; 4 | -------------------------------------------------------------------------------- /src/helpers/qrcode.js: -------------------------------------------------------------------------------- 1 | import {defs} from '../defs/presets'; 2 | 3 | /** 4 | * parse QR code text for shadowsocks 5 | * @param text 6 | * @returns {object} 7 | */ 8 | function parseSS(text) { 9 | // `ss://${method}:${key}@${host}:${port}?xxx=xxx` 10 | const str = atob(text.substr(5)); 11 | 12 | let index = str.lastIndexOf('@'); 13 | if (index === -1) { 14 | return null; 15 | } 16 | 17 | const method_key = str.slice(0, index); 18 | const host_port_query = str.slice(index + 1); 19 | 20 | // method and key 21 | index = method_key.indexOf(':'); 22 | if (index === -1) { 23 | return null; 24 | } 25 | 26 | const [method, key] = [method_key.slice(0, index), method_key.slice(index + 1)]; 27 | if (!method || !key) { 28 | return null; 29 | } 30 | 31 | // presets 32 | let presets = [{name: 'ss-base', params: {}}]; 33 | if (defs['ss-stream-cipher'][0].values.includes(method)) { 34 | presets.push({name: 'ss-stream-cipher', params: {method}}); 35 | } else if (defs['ss-aead-cipher'][0].values.includes(method)) { 36 | presets.push({name: 'ss-aead-cipher', params: {method, info: 'ss-subkey'}}); 37 | } else { 38 | return null; 39 | } 40 | 41 | // drop query 42 | let host_port = host_port_query; 43 | if (host_port.indexOf('?') !== -1) { 44 | host_port = host_port.split('?')[0]; 45 | } 46 | 47 | // host and port 48 | const [host, port] = host_port.split(':'); 49 | if (!host || !port) { 50 | return null; 51 | } 52 | 53 | const _port = +port; 54 | if (!Number.isSafeInteger(_port) || _port < 0 || _port > 65535) { 55 | return null; 56 | } 57 | 58 | const server = { 59 | enabled: false, 60 | host, 61 | port: _port, 62 | transport: 'tcp', 63 | key, 64 | presets, 65 | remarks: `${host}:${port}` 66 | }; 67 | 68 | if (validate(server)) { 69 | return server; 70 | } 71 | return null; 72 | } 73 | 74 | /** 75 | * parse QR code text for blinksocks 76 | * @param text 77 | * @returns {object} 78 | */ 79 | function parseBS(text) { 80 | try { 81 | const server = JSON.parse(atob(text.substr(5))); 82 | if (validate(server)) { 83 | return server; 84 | } 85 | } catch (err) { 86 | // suppress parse error 87 | } 88 | return null; 89 | } 90 | 91 | /** 92 | * validate parse result 93 | * @param server 94 | * @returns {boolean} 95 | */ 96 | function validate(server) { 97 | if (typeof server !== 'object') { 98 | return false; 99 | } 100 | if (server.host.length < 1) { 101 | return false; 102 | } 103 | if (typeof server.port !== 'number') { 104 | return false; 105 | } 106 | if (server.port < 0 || server.port > 65535) { 107 | return false; 108 | } 109 | if (!Array.isArray(server.presets) || server.presets.length < 1) { 110 | return false; 111 | } 112 | if (server.presets.some(({name, params}) => !name || (typeof params !== 'object'))) { 113 | return false; 114 | } 115 | return true; 116 | } 117 | 118 | export function parseQrCodeText(text) { 119 | // parse shadowsocks version 120 | if (text.startsWith('ss://')) { 121 | return parseSS(text); 122 | } 123 | 124 | // parse blinksocks version 125 | if (text.startsWith('bs://')) { 126 | return parseBS(text); 127 | } 128 | 129 | return null; 130 | } 131 | -------------------------------------------------------------------------------- /src/helpers/screenshot.js: -------------------------------------------------------------------------------- 1 | const {desktopCapturer, screen} = window.require('electron'); 2 | 3 | /** 4 | * get current display size 5 | * @returns {{width: number, height: number}} 6 | */ 7 | function getScreenSize() { 8 | // const displays = screen.getAllDisplays(); 9 | const displays = [screen.getPrimaryDisplay()]; 10 | const size = { 11 | width: 0, 12 | height: 0 13 | }; 14 | for (const display of displays) { 15 | const {width, height} = display.size; 16 | size.width += width; 17 | size.height += height; 18 | } 19 | return size; 20 | } 21 | 22 | /** 23 | * a promise wrapper to desktopCapturer.getSources of electron 24 | * @returns {Promise} 25 | */ 26 | function getSources() { 27 | return new Promise((resolve, reject) => { 28 | desktopCapturer.getSources({types: ['window', 'screen']}, (error, sources) => { 29 | if (error) { 30 | reject(error); 31 | } else { 32 | resolve(sources); 33 | } 34 | }); 35 | }); 36 | } 37 | 38 | export async function takeScreenShot() { 39 | const sources = await getSources(); 40 | const source = sources.find((src) => src.name === 'Entire screen'); // Entire screen 41 | 42 | if (!source) { 43 | throw Error('Couldn\'t find "Entire screen"'); 44 | } 45 | 46 | const {width, height} = getScreenSize(); 47 | const stream = await navigator.mediaDevices.getUserMedia({ 48 | audio: false, 49 | video: { 50 | mandatory: { 51 | chromeMediaSource: 'desktop', 52 | chromeMediaSourceId: source.id, 53 | minWidth: width, 54 | maxWidth: width, 55 | minHeight: height, 56 | maxHeight: height 57 | } 58 | } 59 | }); 60 | 61 | const video = document.createElement('video'); 62 | const canvas = document.createElement('canvas'); 63 | 64 | return new Promise((resolve) => { 65 | video.src = URL.createObjectURL(stream); 66 | video.addEventListener('loadedmetadata', () => { 67 | const ratio = video.videoWidth / video.videoHeight; 68 | const w = video.videoWidth; 69 | const h = parseInt(w / ratio, 10); 70 | 71 | canvas.width = w; 72 | canvas.height = h; 73 | 74 | const ctx = canvas.getContext('2d'); 75 | ctx.drawImage(video, 0, 0, w, h); 76 | 77 | resolve(ctx.getImageData(0, 0, w, h)); 78 | stream.getVideoTracks()[0].stop(); 79 | }, false); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /src/helpers/toast.js: -------------------------------------------------------------------------------- 1 | import notie from 'notie'; 2 | 3 | export function toast(message, options = {}) { 4 | notie.alert({text: message, position: 'bottom', stay: false, time: 5, ...options}); 5 | } 6 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: "Segoe UI", Segoe, Tahoma, Arial, Verdana, sans-serif; 5 | overflow: hidden; 6 | } 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {HashRouter, Route} from 'react-router-dom'; 4 | import {MuiThemeProvider, getMuiTheme} from 'material-ui/styles'; 5 | import injectTapEventPlugin from 'react-tap-event-plugin'; 6 | import 'notie'; 7 | import 'notie/dist/notie.min.css'; 8 | 9 | import {App, Logs} from './containers'; 10 | import myTheme from './theme'; 11 | import './index.css'; 12 | 13 | document.addEventListener('DOMContentLoaded', () => { 14 | // Needed for onTouchTap 15 | // http://stackoverflow.com/a/34015469/988941 16 | injectTapEventPlugin(); 17 | 18 | ReactDOM.render( 19 | 20 | 21 |
    22 | 23 | 24 |
    25 |
    26 |
    , 27 | document.getElementById('root') 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | import { 2 | blue600, 3 | cyan500, cyan700, 4 | pinkA200, 5 | grey100, grey300, grey400, grey500, 6 | white, darkBlack, fullBlack 7 | } from 'material-ui/styles/colors'; 8 | 9 | import {fade} from 'material-ui/utils/colorManipulator'; 10 | import {spacing} from 'material-ui/styles'; 11 | 12 | export default { 13 | spacing: spacing, 14 | fontFamily: 'Roboto, sans-serif', 15 | palette: { 16 | primary1Color: blue600, 17 | primary2Color: cyan700, 18 | primary3Color: grey400, 19 | accent1Color: pinkA200, 20 | accent2Color: grey100, 21 | accent3Color: grey500, 22 | textColor: darkBlack, 23 | alternateTextColor: white, 24 | canvasColor: white, 25 | borderColor: grey300, 26 | disabledColor: fade(darkBlack, 0.3), 27 | pickerHeaderColor: cyan500, 28 | clockCircleColor: fade(darkBlack, 0.07), 29 | shadowColor: fullBlack, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/thirdparty/sudo-agent/src/lib/constants.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "os" 5 | "path" 6 | ) 7 | 8 | var HOME_DIR = os.Getenv("HOME") 9 | var BLINKSOCKS_DIR = path.Join(HOME_DIR, ".blinksocks") 10 | var SYSPROXY_PATH = path.Join(BLINKSOCKS_DIR, "proxy_conf_helper") 11 | var SUDO_AGENT_PORT = path.Join(BLINKSOCKS_DIR, ".sudo-agent-port") 12 | -------------------------------------------------------------------------------- /src/thirdparty/sudo-agent/src/lib/executor.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | type DarwinConf struct { 11 | } 12 | 13 | func Exec(args string) { 14 | fmt.Printf("%v %v", SYSPROXY_PATH, args) 15 | _, err := exec.Command(SYSPROXY_PATH, strings.Split(args, " ")...).Output() 16 | if err != nil { 17 | fmt.Println(err) 18 | } 19 | } 20 | 21 | func (*DarwinConf) SetGlobal(port uint16) { 22 | Exec(fmt.Sprintf("--mode global --port %v", port)) 23 | } 24 | 25 | func (*DarwinConf) SetPAC(url string) { 26 | Exec(fmt.Sprintf("--mode auto --pac-url %v", url)) 27 | } 28 | 29 | func (*DarwinConf) RestoreGlobal(port uint16) { 30 | Exec(fmt.Sprintf("--mode off --port %v", port)) 31 | } 32 | 33 | func (*DarwinConf) RestorePAC(url string) { 34 | Exec(fmt.Sprintf("--mode off --pac-url %v", url)) 35 | } 36 | 37 | func (*DarwinConf) Kill() { 38 | process, err := os.FindProcess(os.Getpid()) 39 | if err != nil { 40 | process.Kill() 41 | } else { 42 | os.Exit(0) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/thirdparty/sudo-agent/src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "lib" 7 | "net" 8 | "os" 9 | "os/signal" 10 | "strings" 11 | "syscall" 12 | "io/ioutil" 13 | ) 14 | 15 | var agent lib.DarwinConf 16 | 17 | // udp message structure 18 | type RequestArgs struct { 19 | Url string `json:"url"` // for PAC 20 | Host string `json:"host"` 21 | Port uint16 `json:"port"` 22 | Bypass string `json:"bypass"` 23 | } 24 | type Message struct { 25 | Tag string `json:"tag"` 26 | Method string `json:"method"` 27 | Args RequestArgs `json:"args"` 28 | } 29 | 30 | func onInterrupt(conn *net.UDPConn) { 31 | conn.Close() 32 | os.Exit(0) 33 | } 34 | 35 | func onReceive(msg Message) { 36 | fmt.Printf("<- %+v\n", msg) 37 | 38 | tag := msg.Tag 39 | 40 | // tag verify 41 | if tag != os.Args[1] { 42 | fmt.Printf("unexpected verify tag: \"%v\"\n", tag) 43 | return 44 | } 45 | 46 | method := msg.Method 47 | args := msg.Args 48 | 49 | switch method { 50 | case "setGlobal": 51 | agent.SetGlobal(args.Port) 52 | case "setPAC": 53 | agent.SetPAC(args.Url) 54 | case "restoreGlobal": 55 | agent.RestoreGlobal(args.Port) 56 | case "restorePAC": 57 | agent.RestorePAC(args.Url) 58 | case "kill": 59 | agent.Kill() 60 | os.Remove(lib.SUDO_AGENT_PORT) 61 | } 62 | } 63 | 64 | // $ ./sudo-agent 65 | func main() { 66 | if len(os.Args) < 2 { 67 | fmt.Println("invalid args length") 68 | return 69 | } 70 | 71 | sigs := make(chan os.Signal, 1) 72 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 73 | 74 | // listen unix socket 75 | conn, err := net.ListenUDP("udp4", nil) 76 | if err != nil { 77 | fmt.Println(err) 78 | return 79 | } 80 | 81 | // write listen port to fs 82 | _, port, err := net.SplitHostPort(conn.LocalAddr().String()) 83 | if err != nil { 84 | fmt.Println(err) 85 | return 86 | } 87 | ioutil.WriteFile(lib.SUDO_AGENT_PORT, []byte(string(port)), 0644) 88 | 89 | go func() { 90 | <-sigs // handle SIGINT and SIGTERM 91 | onInterrupt(conn) 92 | }() 93 | 94 | buffer := make([]byte, 512) 95 | for { 96 | // receive 97 | size, _, err := conn.ReadFrom(buffer) 98 | if err != nil { 99 | fmt.Println(err) 100 | continue 101 | } 102 | 103 | // to string 104 | str := string(buffer[0:size]) 105 | 106 | // parse json 107 | var msg Message 108 | dec := json.NewDecoder(strings.NewReader(str)) 109 | err1 := dec.Decode(&msg) 110 | if err1 != nil { 111 | fmt.Printf("cannot parse (\"%v\") due to: %v", str, err1) 112 | continue 113 | } 114 | onReceive(msg) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tasks/compress.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ ! $1 ] ; then 4 | echo "you must specify an input path" 5 | exit 1 6 | fi 7 | 8 | if [ ! $2 ] ; then 9 | echo "you must specify an output path" 10 | exit 1 11 | fi 12 | 13 | tar -zcf $2 $1 14 | -------------------------------------------------------------------------------- /tasks/create-patch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | command -v openssl > /dev/null 2>&1 4 | 5 | if [ $? != 0 ]; then 6 | echo "openssl is required to be installed" 7 | exit 1 8 | fi 9 | 10 | if [ ! $1 ] ; then 11 | echo "you must specify a file as the second parameter" 12 | exit 1 13 | fi 14 | 15 | patch="$1.patch" 16 | 17 | if [ $2 ]; then 18 | patch=$2 19 | fi 20 | 21 | cat $1 | openssl dgst -sha256 -binary > ${patch} 22 | cat $1 >> ${patch} 23 | gzip ${patch} 24 | -------------------------------------------------------------------------------- /tasks/github-upload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | HELP="$ ./github-upload.sh " 4 | 5 | if [ ! $3 ] ; then 6 | echo ${HELP} 7 | exit 1 8 | fi 9 | 10 | if command -v github-release > /dev/null; then 11 | if [[ -z ${GITHUB_TOKEN} ]]; then 12 | echo "You must set \$GITHUB_TOKEN first" 13 | exit 1 14 | fi 15 | github-release upload --user blinksocks --repo blinksocks-desktop --tag ${1} --name ${2} --file ${3} 16 | else 17 | echo "You should install github-release first, see https://github.com/aktau/github-release" 18 | fi 19 | -------------------------------------------------------------------------------- /tasks/release.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const child_process = require('child_process'); 5 | const packager = require('electron-packager'); 6 | const filesize = require('filesize'); 7 | const packageJson = require('../package.json'); 8 | 9 | const name = packageJson.name; 10 | const version = packageJson.version; 11 | const root = path.join.bind(path, path.resolve(__dirname, '..')); 12 | 13 | const options = { 14 | dir: root(), 15 | out: root('releases'), 16 | appVersion: version, 17 | name: name, 18 | appCopyright: 'Copyright 2017 blinksocks', 19 | platform: 'linux,win32,darwin', 20 | arch: 'ia32,x64', 21 | asar: true, 22 | packageManager: 'yarn', 23 | overwrite: true, 24 | prune: true, 25 | quiet: false, 26 | win32metadata: { 27 | CompanyName: '', 28 | FileDescription: 'Cross-platform desktop GUI for blinksocks', 29 | OriginalFilename: name, 30 | ProductName: name, 31 | InternalName: name 32 | }, 33 | // If the file extension is omitted, it is auto-completed to the correct extension based on the platform 34 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#icon 35 | icon: path.resolve(__dirname, '..', 'resources', 'icon') 36 | }; 37 | 38 | const execFileSync = function (sh, args = []) { 39 | if (typeof sh === 'string') { 40 | return child_process.execFileSync(sh, args, {cwd: root('releases')}); 41 | } 42 | }; 43 | 44 | packager(options, function done(err, appPaths) { 45 | if (err) { 46 | console.error(err); 47 | } else { 48 | const targzFiles = [/* {name, hash, size}, ... */]; 49 | let isPatchGenerated = false; 50 | 51 | // post-process to each generated dir 52 | for (const dir of appPaths) { 53 | const newDir = `${dir}-v${version}`; 54 | try { 55 | // 1. append version 56 | fs.renameSync(dir, newDir); 57 | 58 | // 2. generate patch 59 | if (!isPatchGenerated) { 60 | let appAsar; 61 | if (newDir.indexOf('darwin') !== -1) { 62 | appAsar = path.join(newDir, `${name}.app/Contents/Resources/app.asar`); 63 | } else { 64 | appAsar = path.join(newDir, 'resources', 'app.asar'); 65 | } 66 | execFileSync(root('tasks', 'create-patch.sh'), [appAsar, `${name}-v${version}.patch`]); 67 | const sha256 = execFileSync(root('tasks', 'sha256sum.sh'), [`${name}-v${version}.patch.gz`]); 68 | fs.appendFileSync(root('releases', 'sha256sum.txt'), sha256); 69 | isPatchGenerated = true; 70 | } 71 | 72 | const fname = path.basename(newDir); 73 | const fnameWithExt = `${fname}.tar.gz`; 74 | const fullFilePath = `${newDir}.tar.gz`; 75 | 76 | // 3. compress into .tar.gz 77 | execFileSync(root('tasks', 'compress.sh'), [fname, fullFilePath]); 78 | 79 | // 4. append hash to sha256sum.txt 80 | const stdout = execFileSync(root('tasks', 'sha256sum.sh'), [fnameWithExt]); 81 | const sha256 = stdout.toString().split(' ')[1].trim(); 82 | fs.appendFileSync(root('releases', 'sha256sum.txt'), stdout); 83 | 84 | targzFiles.push({ 85 | name: fnameWithExt, 86 | hash: sha256, 87 | size: fs.lstatSync(fullFilePath).size 88 | }); 89 | } catch (err) { 90 | console.error(err); 91 | process.exit(1); 92 | } 93 | } 94 | 95 | // generate RELEASE.md 96 | targzFiles.sort((a, b) => a.name < b.name); 97 | const dlItems = targzFiles.map(({name, hash, size}) => `| [${name}] | ${hash} | ${filesize(size, {exponent: 2})} |`); 98 | const dlLinks = targzFiles.map(({name}) => `[${name}]: https://github.com/blinksocks/blinksocks-desktop/releases/download/v${version}/${name}`); 99 | const markdown = `[//]: # (THIS IS AN AUTO-GENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.) 100 | 101 | # Release 102 | 103 | | NAME | SHA256 | SIZE | 104 | | :--- | :----- | :--- | 105 | ${dlItems.join('\n')} 106 | 107 | Looking for old versions? Please visit [releases](https://github.com/blinksocks/blinksocks-desktop/releases). 108 | 109 | ${dlLinks.join('\n')} 110 | `; 111 | fs.writeFileSync(root('RELEASE.md'), markdown); 112 | } 113 | }); 114 | -------------------------------------------------------------------------------- /tasks/sha256sum.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | command -v openssl > /dev/null 2>&1 4 | 5 | if [ $? != 0 ]; then 6 | echo "openssl is required to be installed" 7 | exit 1 8 | fi 9 | 10 | if [ ! $1 ] ; then 11 | echo "you must specify a file as the second parameter" 12 | exit 1 13 | fi 14 | 15 | openssl dgst -sha256 -hex $1 16 | -------------------------------------------------------------------------------- /tasks/thirdparty-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function build_sudo_agent() { 4 | command -v go > /dev/null 2>&1 5 | 6 | if [ $? != 0 ]; then 7 | echo "go is required to be installed" 8 | exit 1 9 | fi 10 | 11 | # GOPATH 12 | DIR=`pwd`/src/thirdparty/sudo-agent 13 | export GOPATH=${DIR} 14 | 15 | # build for darwin 16 | ENTRY_FILE=./src/thirdparty/sudo-agent/src/main.go 17 | OUT_PATH=./resources/sudo-agent_darwin_x64 18 | GOOS=darwin GOARCH=amd64 go build -ldflags "-w" -o ${OUT_PATH} ${ENTRY_FILE} 19 | 20 | # gzip 21 | if [ $? == 0 ]; then 22 | gzip ${OUT_PATH} 23 | fi 24 | } 25 | 26 | build_sudo_agent 27 | --------------------------------------------------------------------------------