├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── LICENSE ├── README.md ├── core ├── .gitignore ├── bin │ ├── bootstrap.js │ └── start.js ├── package-lock.json ├── package.json ├── pkg │ └── postpkg.js ├── public │ ├── asset-manifest.json │ ├── favicon.ico │ ├── fonts │ │ └── TitilliumWeb-Light.ttf │ ├── images │ │ └── landing.jpeg │ ├── index.html │ ├── locales │ │ ├── en │ │ │ └── translation.json │ │ ├── zh-CN │ │ │ └── translation.json │ │ └── zh │ │ │ └── translation.json │ ├── manifest.json │ ├── service-worker.js │ └── static │ │ ├── css │ │ └── main.6ee5fc8a.css │ │ ├── js │ │ └── main.098d86a0.js │ │ └── media │ │ ├── icons-16.76957eb0.ttf │ │ ├── icons-16.c22f3806.eot │ │ ├── icons-16.d13388be.woff │ │ ├── icons-20.062208ea.eot │ │ ├── icons-20.6d716ba4.ttf │ │ └── icons-20.f56d7d34.woff ├── src │ ├── constants.js │ ├── core │ │ ├── router.js │ │ ├── server.js │ │ └── ws.js │ ├── lives │ │ ├── live_connections.js │ │ ├── live_log.js │ │ ├── live_services.js │ │ └── live_usage.js │ ├── main.js │ ├── methods │ │ ├── _keepalive_server_push.js │ │ ├── _register_server_push.js │ │ ├── _unregister_server_push.js │ │ ├── add_setting.js │ │ ├── add_user.js │ │ ├── copy_setting.js │ │ ├── delete_setting.js │ │ ├── delete_user.js │ │ ├── get_auto_start_services.js │ │ ├── get_config.js │ │ ├── get_env.js │ │ ├── get_geoip.js │ │ ├── get_preset_defs.js │ │ ├── get_service.js │ │ ├── get_service_logs.js │ │ ├── get_service_metrics.js │ │ ├── get_services.js │ │ ├── get_system_pac.js │ │ ├── get_system_proxy.js │ │ ├── get_users.js │ │ ├── restart_service.js │ │ ├── save_setting.js │ │ ├── save_user.js │ │ ├── set_service_auto_start.js │ │ ├── set_system_pac.js │ │ ├── set_system_proxy.js │ │ ├── start_service.js │ │ ├── stop_service.js │ │ ├── unset_service_auto_start.js │ │ └── update_remarks.js │ └── utils │ │ ├── _fork.js │ │ ├── db.js │ │ ├── geoip.js │ │ ├── hash.js │ │ ├── hash_password.js │ │ ├── import_dir.js │ │ ├── index.js │ │ ├── logger.js │ │ └── service_manager.js └── vendor │ ├── pac-cmd │ ├── .gitignore │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── binaries │ │ ├── darwin │ │ │ └── pac │ │ ├── linux_386 │ │ │ └── pac │ │ ├── linux_amd64 │ │ │ └── pac │ │ └── windows │ │ │ ├── pac_386.exe │ │ │ └── pac_amd64.exe │ ├── common.h │ ├── darwin.c │ ├── linux.c │ ├── main.c │ └── windows.c │ └── sysproxy-cmd │ ├── .gitignore │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── binaries │ ├── darwin │ │ └── sysproxy │ ├── linux_386 │ │ └── sysproxy │ ├── linux_amd64 │ │ └── sysproxy │ └── windows │ │ ├── sysproxy_386.exe │ │ └── sysproxy_amd64.exe │ ├── common.h │ ├── darwin.c │ ├── linux.c │ ├── main.c │ └── windows.c ├── screenshot-0.png ├── screenshot-1.png └── ui ├── .env.development ├── .env.production ├── .gitignore ├── config-overrides.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── fonts │ └── TitilliumWeb-Light.ttf ├── images │ └── landing.jpeg ├── index.html ├── locales │ ├── en │ │ └── translation.json │ ├── zh-CN │ │ └── translation.json │ └── zh │ │ └── translation.json └── manifest.json ├── scripts └── post-build.js └── src ├── components ├── Github │ ├── Github.js │ └── Github.module.scss ├── MenuRouter │ ├── MenuRouter.js │ ├── MenuRouter.module.scss │ └── index.js ├── NoMatch │ ├── NoMatch.js │ └── NoMatch.module.scss └── Title │ └── Title.js ├── constants.js ├── containers ├── App │ ├── App.js │ ├── App.module.scss │ └── NetworkTips │ │ ├── NetworkTips.js │ │ └── NetworkTips.module.scss ├── Dashboard │ ├── Dashboard.js │ ├── Dashboard.module.scss │ └── Service │ │ ├── Service.js │ │ └── Service.module.scss ├── Home │ ├── Home.js │ ├── Home.module.scss │ └── Usage │ │ ├── Usage.js │ │ └── Usage.module.scss ├── Info │ ├── Info.js │ └── Info.module.scss ├── Landing │ ├── Landing.js │ └── Landing.module.scss ├── Plugins │ ├── Plugins.js │ ├── Plugins.module.scss │ └── SystemProxy │ │ ├── SystemProxy.js │ │ └── SystemProxy.module.scss ├── Services │ ├── Add │ │ └── Add.js │ ├── Graphs │ │ ├── Graphs.js │ │ └── Graphs.module.scss │ ├── Log │ │ ├── GoogleMap │ │ │ ├── GoogleMap.js │ │ │ ├── GoogleMap.module.scss │ │ │ └── destination.png │ │ ├── Log.js │ │ └── Log.module.scss │ ├── Services.js │ ├── Services.module.scss │ └── Setting │ │ ├── AddressEditor │ │ ├── AddressEditor.js │ │ └── AddressEditor.module.scss │ │ ├── ClientEditor.js │ │ ├── Editor.module.scss │ │ ├── ProtocolStatckEditor │ │ ├── ProtocolStackEditor.js │ │ └── ProtocolStackEditor.module.scss │ │ ├── ServerEditor.js │ │ ├── Setting.js │ │ ├── Setting.module.scss │ │ └── ToolTip │ │ └── ToolTip.js └── Settings │ ├── Settings.js │ ├── Settings.module.scss │ └── UserItem │ ├── UserItem.js │ └── UserItem.module.scss ├── i18n └── index.js ├── index.js ├── index.scss ├── registerServiceWorker.js └── utils ├── hash.js ├── index.js ├── live.js ├── rpc.js ├── store.js ├── toast.js └── ws.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "core/vendor/pac-cmd"] 2 | path = core/vendor/pac-cmd 3 | url = https://github.com/getlantern/pac-cmd 4 | [submodule "core/vendor/sysproxy-cmd"] 5 | path = core/vendor/sysproxy-cmd 6 | url = https://github.com/getlantern/sysproxy-cmd 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | ## 0.3.1 (2018-11-24) 4 | 5 | ### :bug: Bug 修复: 6 | 7 | - 修复子进程(blinksocks)启动失败导致 CPU 占满的问题 8 | 9 | ### 从 0.3.0 更新到 0.3.1 10 | 11 | ``` 12 | $ npm install -g blinksocks-gui@0.3.1 blinksocks@3.x 13 | ``` 14 | 15 | ## 0.3.0 (2018-05-14) 16 | 17 | ### :rocket: 新功能和改进 18 | 19 | - 日志查看器新增地图(基于 Google Map)模式 :sparkles: 20 | - 将 blinksocks 依赖改为 peerDependency,可以选择使用不同的版本 21 | - 图表改为自动刷新 22 | - 选项卡标题增加动态内容 23 | 24 | ### :bug: Bug 修复: 25 | 26 | - 修复子进程(blinksocks)报错退出时主进程(blinksocks-gui)崩溃的问题 27 | - 修复免安装版本无法再 Windows 平台下使用的问题 28 | - 修复 server 模式下无法修改密钥的问题 29 | - 修复无法通过强制刷新进入次级页面的问题 30 | - 修复日志查看器没有显示目标地址的问题 31 | 32 | ### 从 0.2.1 更新到 0.3.0 33 | 34 | ``` 35 | $ npm install -g blinksocks-gui@0.3.0 blinksocks@3.x 36 | ``` 37 | 38 | ## 0.2.1 (2018-04-11) 39 | 40 | ### :bug: Bug 修复: 41 | 42 | - 修复保存系统用户设置时的报错 43 | 44 | ### 从 0.2.0 更新到 0.2.1 45 | 46 | ``` 47 | $ npm install -g blinksocks-gui@0.2.1 48 | ``` 49 | 50 | ## 0.2.0 (2018-04-11) 51 | 52 | ### :rocket: 新功能和改进 53 | 54 | - 服务日志查看器 55 | - 实时查看每个连接的源地址、目的地址、存活时间和对应状态 56 | - 历史日志下载 57 | - 服务运行日志统一存放在 runtime/logs 下 58 | - 调整 RPC 超时时间从 10s 到 30s 59 | - 服务名称修改功能现集成到 card 菜单中 60 | - 重新安排了配置项的展示顺序和高级分类 61 | - 增加配置服务自动重启的功能 62 | 63 | ### :bug: Bug 修复: 64 | 65 | - 修改服务名称判空处理 66 | - 登录密码非明文存储 67 | 68 | ### 从 0.1.0 更新到 0.2.0 69 | 70 | ``` 71 | $ npm install -g blinksocks-gui@0.2.0 72 | ``` 73 | 74 | ## 0.1.0 (2018-04-04) 75 | 76 | ### :rocket: 特性 77 | 78 | - 三大平台支持(Windows、Linux、macOS) 79 | - 双端图形化界面 80 | - 单机服务多开 81 | - 远程服务配置、启动/停止 82 | - 实时监控图表(CPU、内存、上下行速度、网络连接数、网络流量) 83 | - 日志查看和搜索 84 | 85 | ### 安装 86 | 87 | ``` 88 | $ npm install -g blinksocks-gui@0.1.0 89 | ``` 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blinksocks-gui 2 | 3 | [![version](https://img.shields.io/npm/v/blinksocks-gui.svg)](https://www.npmjs.com/package/blinksocks-gui) 4 | [![downloads](https://img.shields.io/npm/dt/blinksocks-gui.svg)](https://www.npmjs.com/package/blinksocks-gui) 5 | [![license](https://img.shields.io/npm/l/blinksocks-gui.svg)](https://github.com/blinksocks/blinksocks-gui/blob/master/LICENSE) 6 | [![%e2%9d%a4](https://img.shields.io/badge/made%20with-%e2%9d%a4-ff69b4.svg)](https://github.com/blinksocks/blinksocks-gui) 7 | 8 | > 为 [blinksocks](https://github.com/blinksocks/blinksocks) 封装的 WEB 图形化界面。 9 | 10 | ![](screenshot-0.png) 11 | 12 | ![](screenshot-1.png) 13 | 14 | ## 特性 15 | 16 | - 三大平台支持(Windows、Linux、macOS) 17 | - 双端图形化界面 18 | - 单机服务多开 19 | - 远程服务配置、启动/停止 20 | - 实时监控图表(CPU、内存、上下行速度、网络连接数、网络流量) 21 | - 日志查看和搜索 22 | 23 | ## 安装 24 | 25 | ### 使用 npm 安装或升级(推荐) 26 | 27 | 在此之前,请先安装 [Node.js](https://nodejs.org/en/),Node.js 自带 npm 包管理器。 28 | 29 | > Tips: 如果你是在服务端(一般是 Linux)上使用,可以使用官方提供的安装脚本: 30 | > https://nodejs.org/en/download/package-manager/#installing-node-js-via-package-manager 31 | 32 | 然后执行下面的命令安装 blinksocks 和 blinksocks-gui: 33 | 34 | ``` 35 | $ npm install -g blinksocks blinksocks-gui 36 | ``` 37 | 38 | 需要升级时重新执行上面的命令即可。 39 | 40 | ## 启动 41 | 42 | ### 交互式启动 43 | 44 | ``` 45 | $ NODE_ENV=production blinksocks-gui 46 | ``` 47 | 48 | 根据提示选择启动类型(客户端或者服务端): 49 | 50 | ``` 51 | ? Please choose run type › - Use arrow-keys. Return to submit. 52 | ❯ Client 53 | Server 54 | ``` 55 | 56 | 选择一个端口号用于远程访问图形界面: 57 | 58 | ``` 59 | ✔ Please choose run type › Client 60 | ? Please choose a port(1 ~ 65535) for web ui: › 3000 61 | ``` 62 | 63 | 完成后在**浏览器**中打开提示链接即可: 64 | 65 | ``` 66 | ✔ Please choose run type › Client 67 | ✔ Please choose a port(1 ~ 65535) for web ui: … 3000 68 | info: blinksocks gui client is running at 3000. 69 | info: You can now open blinksocks-gui in browser: 70 | 71 | http://localhost:3000/ 72 | 73 | ``` 74 | 75 | ### 一行命令启动 76 | 77 | ``` 78 | $ blinksocks-gui --client --port 3000 79 | ``` 80 | 81 | > Tips: 第一次启动时,程序会自动创建一个 `root` 用户,初始密码为 `root`,在 `/landing` 页面输入初始密码后登录系统。 82 | 83 | ## 修改初始登录密码 84 | 85 | 转到 `/settings` 页面或点击左侧 `Settings` 菜单进入系统配置面板修改相关配置并保存。 86 | 87 | ## 开发指引 88 | 89 | ### 拉取仓库并初始化 90 | 91 | ``` 92 | $ git clone https://github.com/blinksocks/blinksocks-gui 93 | $ cd blinksocks-gui 94 | $ git submodule update --init 95 | ``` 96 | 97 | ### 安装依赖 98 | 99 | **core 模块安装** 100 | 101 | ``` 102 | $ cd core 103 | $ npm install 104 | $ npm install --no-save blinksocks 105 | ``` 106 | 107 | **ui 模块安装** 108 | 109 | ``` 110 | $ cd ui 111 | $ npm install 112 | ``` 113 | 114 | ### 启动调试 115 | 116 | 启动本地 HTTP/WebSocket 服务: 117 | 118 | ``` 119 | $ cd core && npm run start:client 120 | ``` 121 | 122 | 启动前端开发服务器: 123 | 124 | ``` 125 | $ cd ui && npm start 126 | ``` 127 | 128 | 根据提示打开链接开始调试。 129 | 130 | ### 编译和打包 131 | 132 | 只需要编译打包前端代码,完成后会自动替换 `core/public` 里的内容: 133 | 134 | ``` 135 | $ cd ui && npm run build 136 | ``` 137 | 138 | ### 发布 139 | 140 | 只需发布 `core/` 里的内容到 npm registry 即可: 141 | 142 | ``` 143 | $ cd core 144 | $ npm publish 145 | ``` 146 | 147 | ## 更新日志 148 | 149 | [CHANGELOG.md](CHANGELOG.md) 150 | 151 | ## License 152 | 153 | Apache License 2.0 154 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # pkg 20 | pkg/blinksocks-*.gz 21 | pkg/sha256sum.txt 22 | 23 | # runtime 24 | /runtime 25 | 26 | *.log* 27 | .audit.json 28 | 29 | -------------------------------------------------------------------------------- /core/bin/bootstrap.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../src/main'); -------------------------------------------------------------------------------- /core/bin/start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const os = require('os'); 3 | const prompts = require('prompts'); 4 | const chalk = require('chalk'); 5 | const bootstrap = require('./bootstrap'); 6 | const version = require('../package.json').version; 7 | 8 | const RUN_TYPE_CLIENT = 0; 9 | const RUN_TYPE_SERVER = 1; 10 | 11 | const examples = [ 12 | ['Start web ui interactively', '$ blinksocks-gui'], 13 | ['Start web ui at 3000 as client', '$ blinksocks-gui --client --port 3000'], 14 | ]; 15 | 16 | const usage = ` 17 | ${chalk.bold.underline(`blinksocks-gui v${version}`)} 18 | 19 | Usage: blinksocks-gui [options] ... 20 | 21 | Options: 22 | 23 | -h, --help output usage information 24 | -v, --version output blinksocks-gui version 25 | -c, --client start web ui as client 26 | -s, --server start web ui as server 27 | -p, --port web ui listening port 28 | 29 | Examples: 30 | 31 | ${examples.map(([description, example]) => ` ${chalk.gray('-')} ${description}${os.EOL} ${chalk.blue(example)}`).join(os.EOL)} 32 | 33 | About & Help: ${chalk.underline('https://github.com/blinksocks/blinksocks-gui')} 34 | `; 35 | 36 | const argv = process.argv; 37 | const options = argv.slice(2); 38 | 39 | function hasOption(opt) { 40 | return options.indexOf(opt) !== -1; 41 | } 42 | 43 | function getOptionValue(opt) { 44 | const index = options.indexOf(opt); 45 | if (index !== -1) { 46 | return options[index + 1]; 47 | } 48 | } 49 | 50 | async function main() { 51 | if (argv.length < 2) { 52 | return console.log(usage); 53 | } 54 | 55 | // parse options 56 | 57 | if (hasOption('-h') || hasOption('--help')) { 58 | return console.log(usage); 59 | } 60 | 61 | if (hasOption('-v') || hasOption('--version')) { 62 | console.log('blinksocks: v' + require('blinksocks/package.json').version); 63 | console.log('blinksocks-gui: v' + version); 64 | return; 65 | } 66 | 67 | let runType, port; 68 | 69 | // ask for runType when necessary 70 | if (hasOption('-c') || hasOption('--client')) { 71 | runType = RUN_TYPE_CLIENT; 72 | } 73 | if (hasOption('-s') || hasOption('--server')) { 74 | runType = RUN_TYPE_SERVER; 75 | } 76 | 77 | if (typeof runType === 'undefined') { 78 | const answer = await prompts({ 79 | type: 'select', 80 | name: 'value', 81 | message: 'Please choose run type', 82 | choices: [ 83 | { title: 'Client', value: RUN_TYPE_CLIENT }, 84 | { title: 'Server', value: RUN_TYPE_SERVER }, 85 | ], 86 | initial: 0, 87 | }); 88 | runType = answer.value; 89 | } 90 | 91 | // ask for port when necessary 92 | port = getOptionValue('-p') || getOptionValue('--port'); 93 | 94 | if (!port || port === '0') { 95 | const answer = await prompts({ 96 | type: 'number', 97 | name: 'value', 98 | message: 'Please choose a port(1 ~ 65535) for web ui:', 99 | initial: 3000, 100 | style: 'default', 101 | min: 1, 102 | max: 65535, 103 | }); 104 | port = answer.value; 105 | } 106 | 107 | bootstrap({ runType, port }); 108 | } 109 | 110 | main(); 111 | -------------------------------------------------------------------------------- /core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blinksocks-gui", 3 | "version": "0.3.1", 4 | "description": "A web based GUI wrapper for blinksocks", 5 | "author": "Micooz", 6 | "files": [ 7 | "bin", 8 | "src", 9 | "public" 10 | ], 11 | "bin": { 12 | "blinksocks-gui": "bin/start.js" 13 | }, 14 | "scripts": { 15 | "start:client": "cross-env NODE_ENV=development nodemon bin/start.js --client --port 3000", 16 | "start:server": "cross-env NODE_ENV=development nodemon bin/start.js --server --port 3000", 17 | "debug:client": "cross-env NODE_ENV=development node --inspect --inspect-port=9400 bin/start.js --client --port 3000", 18 | "debug:server": "cross-env NODE_ENV=development node --inspect --inspect-port=9401 bin/start.js --server --port 3000", 19 | "prepkg": "rimraf pkg/blinksocks-gui-* pkg/sha256sum.txt", 20 | "pkg": "pkg --out-path pkg/ --targets node8.9.0-linux-x64,node8.9.0-macos-x64,node8.9.0-win-x64 .", 21 | "postpkg": "node pkg/postpkg.js" 22 | }, 23 | "dependencies": { 24 | "chalk": "^2.4.1", 25 | "date-fns": "^1.29.0", 26 | "fs-extra": "^6.0.1", 27 | "jssha": "^2.3.1", 28 | "koa": "^2.5.1", 29 | "koa-bodyparser": "^4.2.1", 30 | "koa-favicon": "^2.0.1", 31 | "koa-router": "^7.4.0", 32 | "koa-static-cache": "^5.1.2", 33 | "lodash": "^4.17.10", 34 | "lodash-id": "^0.14.0", 35 | "lowdb": "^1.0.0", 36 | "node-ipc-call": "0.0.3", 37 | "pidusage": "^2.0.6", 38 | "prompts": "^0.1.8", 39 | "socket.io": "^2.1.1", 40 | "sudo-prompt": "^8.2.0", 41 | "winston": "^3.0.0-rc4" 42 | }, 43 | "devDependencies": { 44 | "cross-env": "^5.1.6", 45 | "nodemon": "^1.17.5", 46 | "pkg": "^4.3.4", 47 | "rimraf": "^2.6.2" 48 | }, 49 | "peerDependencies": { 50 | "blinksocks": "3.x" 51 | }, 52 | "repository": { 53 | "url": "https://github.com/blinksocks/blinksocks-gui", 54 | "type": "git" 55 | }, 56 | "bugs": { 57 | "url": "https://github.com/blinksocks/blinksocks-gui/issues" 58 | }, 59 | "nodemonConfig": { 60 | "ignore": [ 61 | "test/*", 62 | "docs/*", 63 | "vendor/*", 64 | "public/*", 65 | "runtime/*" 66 | ] 67 | }, 68 | "pkg": { 69 | "assets": [ 70 | "public/*", 71 | "vendor/pac-cmd/binaries/**/*", 72 | "vendor/sysproxy-cmd/binaries/**/*" 73 | ], 74 | "scripts": [ 75 | "src/utils/_fork.js", 76 | "src/lives/*", 77 | "src/methods/*" 78 | ] 79 | }, 80 | "engines": { 81 | "node": "8" 82 | }, 83 | "license": "Apache-2.0" 84 | } 85 | -------------------------------------------------------------------------------- /core/pkg/postpkg.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const utils = require('util'); 5 | const zlib = require('zlib'); 6 | 7 | const { version } = require('../package.json'); 8 | 9 | const readdir = utils.promisify(fs.readdir); 10 | const remove = utils.promisify(fs.unlink); 11 | const appendFile = utils.promisify(fs.appendFile); 12 | const readFile = utils.promisify(fs.readFile); 13 | 14 | async function sha256sum(file) { 15 | const sha256 = crypto.createHash('sha256'); 16 | const input = await readFile(file); 17 | return sha256.update(input).digest('hex'); 18 | } 19 | 20 | (async function main() { 21 | try { 22 | let files = await readdir(path.resolve(__dirname)); 23 | files = files.filter((f) => f.startsWith('blinksocks-gui-')); 24 | for (const file of files) { 25 | const name = path.basename(file, '.exe'); 26 | const ext = path.extname(file); 27 | const newName = `${name}-x64-v${version}${ext}.gz`; 28 | 29 | const input = path.join(__dirname, file); 30 | const output = path.join(__dirname, newName); 31 | const hashFile = path.join(__dirname, 'sha256sum.txt'); 32 | 33 | // compress into .gz 34 | const stream = fs.createReadStream(input).pipe(zlib.createGzip()).pipe(fs.createWriteStream(output)); 35 | 36 | stream.on('finish', async () => { 37 | // calc sha256sum 38 | await appendFile(hashFile, `${path.basename(output)} ${await sha256sum(output)}\n`); 39 | // remove original 40 | await remove(input); 41 | }); 42 | } 43 | } catch (err) { 44 | console.error(err.message); 45 | } 46 | })(); 47 | -------------------------------------------------------------------------------- /core/public/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "main.css": "/static/css/main.6ee5fc8a.css", 3 | "main.js": "/static/js/main.098d86a0.js", 4 | "static/media/icons-16.eot": "/static/media/icons-16.c22f3806.eot", 5 | "static/media/icons-16.ttf": "/static/media/icons-16.76957eb0.ttf", 6 | "static/media/icons-16.woff": "/static/media/icons-16.d13388be.woff", 7 | "static/media/icons-20.eot": "/static/media/icons-20.062208ea.eot", 8 | "static/media/icons-20.ttf": "/static/media/icons-20.6d716ba4.ttf", 9 | "static/media/icons-20.woff": "/static/media/icons-20.f56d7d34.woff" 10 | } -------------------------------------------------------------------------------- /core/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/public/favicon.ico -------------------------------------------------------------------------------- /core/public/fonts/TitilliumWeb-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/public/fonts/TitilliumWeb-Light.ttf -------------------------------------------------------------------------------- /core/public/images/landing.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/public/images/landing.jpeg -------------------------------------------------------------------------------- /core/public/index.html: -------------------------------------------------------------------------------- 1 | blinksocks-gui
-------------------------------------------------------------------------------- /core/public/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /core/public/locales/zh-CN/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /core/public/locales/zh/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /core/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "blinksocks gui", 3 | "name": "friendly gui management tools for blinksocks", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /core/public/service-worker.js: -------------------------------------------------------------------------------- 1 | "use strict";var precacheConfig=[["/index.html","c5c00edfd30949e19849e0c039ceea0e"],["/static/css/main.6ee5fc8a.css","6ee5fc8a0dc96eaa71e24d462b71b443"],["/static/js/main.098d86a0.js","3e3ff8c5287074fc1b57cb25c4b95a0b"],["/static/media/icons-16.76957eb0.ttf","76957eb0b3f1dd52766872b7dd6c04d6"],["/static/media/icons-16.c22f3806.eot","c22f3806638273bd9e764fea0a812c51"],["/static/media/icons-16.d13388be.woff","d13388bee1190a551bb5d9c9f953a779"],["/static/media/icons-20.062208ea.eot","062208ea882dd2a7e2fa8552c6f32e3b"],["/static/media/icons-20.6d716ba4.ttf","6d716ba49663310796d6cf4d6d8b65b2"],["/static/media/icons-20.f56d7d34.woff","f56d7d3470cf9d60593812561aff63c4"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(t){return t.redirected?("body"in t?Promise.resolve(t.body):t.blob()).then(function(e){return new Response(e,{headers:t.headers,status:t.status,statusText:t.statusText})}):Promise.resolve(t)},createCacheKey=function(e,t,n,r){var a=new URL(e);return r&&a.pathname.match(r)||(a.search+=(a.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,n){var t=new URL(e);return t.hash="",t.search=t.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(t){return n.every(function(e){return!e.test(t[0])})}).map(function(e){return e.join("=")}).join("&"),t.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),a=createCacheKey(r,hashParamName,n,/\.\w{8}\./);return[r.toString(),a]}));function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(r){return setOfCachedUrls(r).then(function(n){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(t){if(!n.has(t)){var e=new Request(t,{credentials:"same-origin"});return fetch(e).then(function(e){if(!e.ok)throw new Error("Request for "+t+" returned a response with status "+e.status);return cleanResponse(e).then(function(e){return r.put(t,e)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var n=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(t){return t.keys().then(function(e){return Promise.all(e.map(function(e){if(!n.has(e.url))return t.delete(e)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(t){if("GET"===t.request.method){var e,n=stripIgnoredUrlParameters(t.request.url,ignoreUrlParametersMatching),r="index.html";(e=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,r),e=urlsToCacheKeys.has(n));0,e&&t.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(e){return console.warn('Couldn\'t serve response for "%s" from cache: %O',t.request.url,e),fetch(t.request)}))}}); -------------------------------------------------------------------------------- /core/public/static/media/icons-16.76957eb0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/public/static/media/icons-16.76957eb0.ttf -------------------------------------------------------------------------------- /core/public/static/media/icons-16.c22f3806.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/public/static/media/icons-16.c22f3806.eot -------------------------------------------------------------------------------- /core/public/static/media/icons-16.d13388be.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/public/static/media/icons-16.d13388be.woff -------------------------------------------------------------------------------- /core/public/static/media/icons-20.062208ea.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/public/static/media/icons-20.062208ea.eot -------------------------------------------------------------------------------- /core/public/static/media/icons-20.6d716ba4.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/public/static/media/icons-20.6d716ba4.ttf -------------------------------------------------------------------------------- /core/public/static/media/icons-20.f56d7d34.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/public/static/media/icons-20.f56d7d34.woff -------------------------------------------------------------------------------- /core/src/constants.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const cwd = process.cwd(); 3 | 4 | exports.RUNTIME_PATH = path.join(cwd, 'runtime'); 5 | exports.RUNTIME_LOG_PATH = path.join(cwd, 'runtime/logs/'); 6 | exports.DATABASE_PATH = path.join(cwd, 'runtime/db.json'); 7 | exports.RUNTIME_HELPERS_PAC_PATH = path.join(cwd, 'runtime/helpers/pac'); 8 | exports.RUNTIME_HELPERS_SYSPROXY_PATH = path.join(cwd, 'runtime/helpers/sysproxy'); 9 | 10 | exports.HASH_SALT = 'blinksocks'; 11 | exports.DESENSITIZE_PLACEHOLDER = '********'; 12 | 13 | exports.RUN_TYPE_CLIENT = 0; 14 | exports.RUN_TYPE_SERVER = 1; 15 | 16 | exports.SERVER_PUSH_REGISTER_SUCCESS = 0; 17 | exports.SERVER_PUSH_REGISTER_ERROR = 1; 18 | exports.SERVER_PUSH_DISPOSE_TIMEOUT = 6e4; // 1min 19 | 20 | exports.SERVICE_STATUS_INIT = -1; 21 | exports.SERVICE_STATUS_RUNNING = 0; 22 | exports.SERVICE_STATUS_STOPPED = 1; 23 | -------------------------------------------------------------------------------- /core/src/core/router.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { import_dir } = require('../utils'); 3 | 4 | const methods = import_dir(path.join(__dirname, '..', 'methods')); 5 | 6 | async function dispatch(method, args, extra) { 7 | const func = methods[method]; 8 | if (typeof func === 'function') { 9 | // check methods 10 | if (this.getDisallowedMethods().includes(method)) { 11 | throw Error(`you don't have privileges to call: "${method}"`); 12 | } 13 | const result = await func.call(this, args || {}, extra || {}); 14 | if (typeof result === 'undefined') { 15 | return null; 16 | } else { 17 | return await result; 18 | } 19 | } else { 20 | throw Error(`method "${method}" is not implemented or not registered`); 21 | } 22 | } 23 | 24 | module.exports = { 25 | dispatch, 26 | }; 27 | -------------------------------------------------------------------------------- /core/src/core/server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const http = require('http'); 4 | const utils = require('util'); 5 | const Koa = require('koa'); 6 | const KoaRouter = require('koa-router'); 7 | const staticCache = require('koa-static-cache'); 8 | const favicon = require('koa-favicon'); 9 | const bodyParser = require('koa-bodyparser'); 10 | const setupWebsocket = require('./ws'); 11 | 12 | const { logger, db } = require('../utils'); 13 | const { RUNTIME_LOG_PATH, RUN_TYPE_SERVER } = require('../constants'); 14 | 15 | const readdir = utils.promisify(fs.readdir); 16 | 17 | // http interfaces 18 | 19 | async function onPostVerify(ctx) { 20 | const { token } = ctx.request.body; 21 | if (!db.get('users').find({ password: token }).value()) { 22 | return ctx.throw(403, 'authentication error'); 23 | } 24 | ctx.status = 200; 25 | } 26 | 27 | async function onGetLog(ctx) { 28 | const { file } = ctx.params; 29 | if (!/^[0-9a-z\-]{36}\.log\.\d{4}-\d{2}-\d{2}$/.test(file)) { 30 | return ctx.throw(400, 'invalid parameter'); 31 | } 32 | const files = await readdir(RUNTIME_LOG_PATH); 33 | const name = files.find(name => name === file); 34 | if (!name) { 35 | return ctx.throw(404); 36 | } 37 | ctx.set('content-type', 'text/plain'); 38 | ctx.body = fs.createReadStream(path.join(RUNTIME_LOG_PATH, name)); 39 | } 40 | 41 | module.exports = async function startServer(args) { 42 | const { runType, port } = args; 43 | 44 | // start koa server 45 | const app = new Koa(); 46 | const router = new KoaRouter(); 47 | const server = http.createServer(app.callback()); 48 | 49 | // websocket setup 50 | setupWebsocket(server, args); 51 | 52 | // standalone http interfaces 53 | router.post('/verify', onPostVerify); 54 | router.get('/logs/:file', onGetLog); 55 | 56 | const publicPath = path.join(__dirname, '../../public'); 57 | app.use(favicon(path.join(publicPath, 'favicon.ico'))); 58 | app.use(staticCache(publicPath)); 59 | app.use(bodyParser()); 60 | app.use(router.routes()); 61 | app.use(router.allowedMethods()); 62 | // others are fallback to index.html 63 | app.use((ctx) => { 64 | ctx.set('content-type', 'text/html'); 65 | ctx.body = fs.createReadStream(path.join(publicPath, 'index.html')); 66 | }); 67 | 68 | return new Promise((resolve, reject) => { 69 | const _port = port || 3000; 70 | server.on('error', reject); 71 | server.listen(_port, () => { 72 | logger.info(`blinksocks gui ${runType === RUN_TYPE_SERVER ? 'server' : 'client'} is running at ${_port}.`); 73 | logger.info('You can now open blinksocks-gui in browser:'); 74 | console.log(''); 75 | console.log(` http://localhost:${_port}/`); 76 | console.log(''); 77 | resolve(); 78 | }); 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /core/src/core/ws.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const _ = require('lodash'); 3 | const Router = require('./router'); 4 | const { RUN_TYPE_CLIENT, RUN_TYPE_SERVER } = require('../constants'); 5 | const { logger, db, import_dir } = require('../utils'); 6 | 7 | const ALL_METHODS = Object.assign( 8 | {}, 9 | import_dir(path.resolve(__dirname, '../methods')), 10 | import_dir(path.resolve(__dirname, '../lives')), 11 | ); 12 | 13 | // create "this" for each method 14 | function createThisArg({ socket, runType }) { 15 | const { user, address } = socket.handshake; 16 | 17 | function extendDB(db) { 18 | db.getConfigs = () => { 19 | const key = { 20 | [RUN_TYPE_CLIENT]: 'client_configs', 21 | [RUN_TYPE_SERVER]: 'server_configs', 22 | }[runType]; 23 | return db.get(key); 24 | }; 25 | return db; 26 | } 27 | 28 | const _this = { 29 | ctx: { 30 | runType, 31 | push_handlers: {}, // used by _xxx_server_push(). 32 | }, 33 | user: user || null, 34 | db: extendDB(db), 35 | getConfigurableMethods() { 36 | const methods = _.transform(ALL_METHODS, (result, _, key) => result.push(key), []); 37 | return methods.filter((name) => name[0] !== '_'); 38 | }, 39 | getDisallowedMethods() { 40 | return user['disallowed_methods'] || []; 41 | }, 42 | push(event, data) { 43 | logger.info(`[${address}] [PUSH] ${JSON.stringify(data)}`); 44 | socket.emit(event, data); 45 | }, 46 | invoke(method, args, extra) { 47 | return Router.dispatch.call(_this, method, args, extra); 48 | }, 49 | }; 50 | return _this; 51 | } 52 | 53 | // once websocket connection established 54 | function onConnection(socket, { runType }) { 55 | const { address } = socket.handshake; 56 | logger.verbose(`[${address}] connected`); 57 | 58 | const thisArg = createThisArg({ socket, runType }); 59 | 60 | // handle client requests 61 | socket.on('request', async function (req, send) { 62 | const reqStr = JSON.stringify(req); 63 | logger.info(`[${address}] request => ${reqStr}`); 64 | const { method, args } = req; 65 | try { 66 | const result = await Router.dispatch.call(thisArg, method, args); 67 | const response = { code: 0 }; 68 | if (result !== null) { 69 | response.data = result; 70 | } 71 | logger.info(`[${address}] response => ${JSON.stringify(response)}`); 72 | send(response); 73 | } catch (err) { 74 | logger.error(`[${address}] cannot process the request: ${reqStr}, %s`, err.stack); 75 | send({ code: -1, message: err.message }); 76 | } 77 | }); 78 | 79 | socket.on('disconnect', async function () { 80 | logger.verbose(`[${address}] disconnected`); 81 | try { 82 | const { push_handlers } = thisArg.ctx; 83 | for (const key of Object.keys(push_handlers)) { 84 | await push_handlers[key].dispose(); 85 | } 86 | } catch (err) { 87 | // ignore any errors 88 | // console.log(err); 89 | } 90 | }); 91 | } 92 | 93 | module.exports = function setup(server, args) { 94 | const io = require('socket.io')(server); 95 | 96 | // ws authentication middleware 97 | io.use((socket, next) => { 98 | const { query: { token } } = socket.handshake; 99 | const user = db.get('users').find({ password: token }).value(); 100 | if (user) { 101 | // NOTE: put user to socket.handshake so that 102 | // we can access it again in onConnection(). 103 | socket.handshake.user = user; 104 | return next(); 105 | } 106 | return next(new Error('authentication error')); 107 | }); 108 | 109 | // handle ws connections 110 | io.on('connection', (socket) => onConnection(socket, args)); 111 | }; 112 | -------------------------------------------------------------------------------- /core/src/lives/live_connections.js: -------------------------------------------------------------------------------- 1 | const dateFns = require('date-fns'); 2 | const { ServiceManager } = require('../utils'); 3 | 4 | module.exports = async function live_connections({ id }) { 5 | this.pushInterval(async () => { 6 | const statuses = await ServiceManager.getServiceConnStatuses(id); 7 | return statuses.reverse().map((conn) => { 8 | if (conn.startTime && conn.endTime) { 9 | conn.elapsed = conn.endTime - conn.startTime; // ms 10 | } 11 | if (conn.startTime) { 12 | conn.startTime = dateFns.format(conn.startTime, 'HH:mm:ss'); 13 | } 14 | if (conn.endTime) { 15 | conn.endTime = dateFns.format(conn.endTime, 'HH:mm:ss'); 16 | } 17 | return conn; 18 | }); 19 | }, 5e3); 20 | }; 21 | -------------------------------------------------------------------------------- /core/src/lives/live_log.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const utils = require('util'); 4 | const readline = require('readline'); 5 | const { tailFile: tail } = require('winston/lib/winston/common'); 6 | 7 | const readdir = utils.promisify(fs.readdir); 8 | const TAIL_FROM = 100; 9 | 10 | async function getTotalLines(file) { 11 | let totalLines = 0; 12 | const reader = readline.createInterface({ 13 | input: fs.createReadStream(file), 14 | }); 15 | return new Promise((resolve) => { 16 | reader.on('line', () => totalLines += 1); 17 | reader.on('close', () => resolve(totalLines)); 18 | }); 19 | } 20 | 21 | module.exports = async function live_log({ id }) { 22 | const { log_path } = await this.invoke('get_config', { id }); 23 | 24 | const logFilePath = path.resolve(process.cwd(), log_path || 'blinksocks'); 25 | const logFileName = path.basename(logFilePath); 26 | const logFileDir = path.dirname(logFilePath); 27 | 28 | // find the most recently created log file 29 | const files = await readdir(logFileDir); 30 | const logFiles = files 31 | .filter(name => name.startsWith(logFileName)) 32 | .sort() 33 | .map(name => path.join(logFileDir, name)); 34 | 35 | const logFile = logFiles[0] || ''; 36 | 37 | // count total lines 38 | let totalLines = 0; 39 | if (logFile) { 40 | totalLines = await getTotalLines(logFile); 41 | } 42 | 43 | let firstTime = true; 44 | let counter = 0; 45 | let lines = []; 46 | 47 | const start = totalLines > TAIL_FROM ? totalLines - TAIL_FROM - 1 : -1; 48 | const destroy = tail({ file: logFile, start: start }, (err, line) => { 49 | if (err) { 50 | return; 51 | } 52 | // instead of push many times at the first time, 53 | // we can make a cache here and do an one-time push. 54 | if (firstTime) { 55 | const end = start === -1 ? totalLines : TAIL_FROM; 56 | if (counter < end - 1) { 57 | lines.push(line); 58 | counter++; 59 | } else { 60 | firstTime = false; 61 | lines.push(line); 62 | this.push(lines); 63 | lines = null; 64 | } 65 | } else { 66 | this.push(line); 67 | } 68 | }); 69 | return async function unregister() { 70 | destroy(); 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /core/src/lives/live_services.js: -------------------------------------------------------------------------------- 1 | const { ServiceManager } = require('../utils'); 2 | 3 | module.exports = async function live_services() { 4 | this.pushInterval(async () => ({ 5 | services: await ServiceManager.getServices() 6 | }), 5e3); 7 | }; 8 | -------------------------------------------------------------------------------- /core/src/lives/live_usage.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const pidusage = require('pidusage'); 3 | 4 | module.exports = async function live_usage() { 5 | this.pushInterval(async () => { 6 | const stats = await pidusage(process.pid); 7 | return { 8 | cpuUsage: stats.cpu / os.cpus().length, 9 | memoryUsage: stats.memory, 10 | }; 11 | }, 5e3); 12 | }; 13 | -------------------------------------------------------------------------------- /core/src/main.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const fs = require('fs'); 3 | const net = require('net'); 4 | const utils = require('util'); 5 | const path = require('path'); 6 | const chalk = require('chalk'); 7 | const http = require('http'); 8 | const fsExtra = require('fs-extra'); 9 | const bsInit = require('blinksocks/bin/init'); 10 | const { Config } = require('blinksocks'); 11 | 12 | const runServer = require('./core/server'); 13 | const { ServiceManager, logger } = require('./utils'); 14 | 15 | const { 16 | RUN_TYPE_CLIENT, 17 | RUN_TYPE_SERVER, 18 | RUNTIME_HELPERS_PAC_PATH, 19 | RUNTIME_HELPERS_SYSPROXY_PATH, 20 | } = require('./constants'); 21 | 22 | const { db, hash_password } = require('./utils'); 23 | 24 | const chmod = utils.promisify(fs.chmod); 25 | 26 | function getSysArch() { 27 | const arch = os.arch(); 28 | switch (arch) { 29 | case'x32': 30 | return '386'; 31 | case 'x64': 32 | return 'amd64'; 33 | default: 34 | throw Error('unsupported architecture: ' + arch); 35 | } 36 | } 37 | 38 | async function copy(source, target) { 39 | if (process.pkg) { 40 | // use stream pipe to reduce memory usage 41 | // when load a large file into memory. 42 | fs.createReadStream(source).pipe(fs.createWriteStream(target)); 43 | } else { 44 | await fsExtra.copy(source, target); 45 | } 46 | } 47 | 48 | async function extractHelpers() { 49 | // copy system-related helper tools to runtime/helpers 50 | const platform = os.platform(); 51 | const arch = getSysArch(); 52 | 53 | const pac_cmd_binaries_path = path.resolve(__dirname, '../vendor/pac-cmd/binaries'); 54 | const sysproxy_binaries_path = path.resolve(__dirname, '../vendor/sysproxy-cmd/binaries'); 55 | 56 | switch (platform) { 57 | case 'darwin': 58 | await copy(path.join(pac_cmd_binaries_path, 'darwin/pac'), RUNTIME_HELPERS_PAC_PATH); 59 | await copy(path.join(sysproxy_binaries_path, 'darwin/sysproxy'), RUNTIME_HELPERS_SYSPROXY_PATH); 60 | break; 61 | case 'linux': 62 | await copy(path.join(pac_cmd_binaries_path, `linux_${arch}/pac`), RUNTIME_HELPERS_PAC_PATH); 63 | await copy(path.join(sysproxy_binaries_path, `linux_${arch}/sysproxy`), RUNTIME_HELPERS_SYSPROXY_PATH); 64 | break; 65 | case 'win32': 66 | await copy(path.join(pac_cmd_binaries_path, `windows/pac_${arch}`), RUNTIME_HELPERS_PAC_PATH); 67 | await copy(path.join(sysproxy_binaries_path, `windows/sysproxy_${arch}`), RUNTIME_HELPERS_SYSPROXY_PATH); 68 | break; 69 | default: 70 | throw Error('unsupported platform: ' + platform); 71 | } 72 | 73 | // grant execute permission 74 | await chmod(RUNTIME_HELPERS_PAC_PATH, 0o774); 75 | await chmod(RUNTIME_HELPERS_SYSPROXY_PATH, 0o774); 76 | } 77 | 78 | async function getPublicIP() { 79 | return new Promise((resolve, reject) => { 80 | http.get('http://api.ipify.org', function (res) { 81 | res.on('data', function (ipbuf) { 82 | const ip = ipbuf.toString(); 83 | if (net.isIP(ip)) { 84 | resolve(ip); 85 | } else { 86 | reject(Error('response is not an ip')); 87 | } 88 | }); 89 | res.on('error', reject); 90 | }); 91 | }); 92 | } 93 | 94 | module.exports = async function main(args) { 95 | const { runType } = args; 96 | try { 97 | // create runtime directory 98 | await fsExtra.mkdirp('runtime/logs'); 99 | 100 | let configs = null; 101 | 102 | // keep at least one config in database 103 | const { clientJson, serverJson } = bsInit({ isMinimal: false, isDryRun: true }); 104 | if (runType === RUN_TYPE_CLIENT) { 105 | configs = db.get('client_configs'); 106 | if (configs.size().value() < 1) { 107 | clientJson.remarks = 'Default'; 108 | configs.insert(clientJson).write(); 109 | } 110 | // await extractHelpers(); 111 | } 112 | if (runType === RUN_TYPE_SERVER) { 113 | configs = db.get('server_configs'); 114 | if (configs.size().value() < 1) { 115 | serverJson.remarks = 'Default'; 116 | configs.insert(serverJson).write(); 117 | } 118 | } 119 | 120 | // add a default user if no users set 121 | const users = db.get('users'); 122 | if (users.value().length < 1) { 123 | users.insert({ 124 | 'name': 'root', 125 | 'password': hash_password('root'), 126 | 'disallowed_methods': [], 127 | }).write(); 128 | } 129 | 130 | // start server 131 | await runServer(args); 132 | 133 | // start services in "auto_start_services" 134 | const ids = db.get('auto_start_services').value(); 135 | for (const id of ids) { 136 | const config = configs.find({ id }).value(); 137 | if (config) { 138 | try { 139 | Config.test(config); 140 | await ServiceManager.start(id, config); 141 | logger.info(`auto started "${config.remarks}" => ${config.service}`); 142 | } catch (err) { 143 | logger.error(`cannot auto start "${config.remarks}": %s`, err.stack); 144 | } 145 | } 146 | } 147 | 148 | // get server ip address 149 | if (process.env.NODE_ENV === 'production') { 150 | logger.info('retrieving public ip address of this machine.'); 151 | try { 152 | const ip = await getPublicIP(); 153 | logger.info(`public ip address is: ${ip}`); 154 | db.set('runtime.ip', ip).write(); 155 | } catch (err) { 156 | logger.error('cannot get public ip address of this machine: %s', err.stack); 157 | } 158 | } 159 | 160 | } catch (err) { 161 | console.error(chalk.red('[main] cannot start gui server:')); 162 | console.error(err.stack); 163 | process.exit(1); 164 | } 165 | }; 166 | -------------------------------------------------------------------------------- /core/src/methods/_keepalive_server_push.js: -------------------------------------------------------------------------------- 1 | module.exports = async function keepalive_server_push({ method }) { 2 | const handler = this.ctx.push_handlers[method]; 3 | if (handler) { 4 | handler.keepalive(); 5 | } else { 6 | throw Error(`method "${method}" is not found or was unregistered`); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /core/src/methods/_register_server_push.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { import_dir } = require('../utils'); 3 | 4 | const { 5 | SERVER_PUSH_REGISTER_SUCCESS, 6 | SERVER_PUSH_REGISTER_ERROR, 7 | SERVER_PUSH_DISPOSE_TIMEOUT, 8 | } = require('../constants'); 9 | 10 | const lives = import_dir(path.join(__dirname, '..', 'lives')); 11 | 12 | module.exports = async function register_server_push({ method, args }) { 13 | const { push_handlers } = this.ctx; 14 | if (push_handlers[method]) { 15 | return { code: SERVER_PUSH_REGISTER_ERROR, message: `method "${method}" is already registered` }; 16 | } 17 | 18 | const func = lives[method]; 19 | if (typeof func !== 'function') { 20 | throw Error(`live method "${method}" is not implemented or not registered`); 21 | } 22 | 23 | // keepalive timer 24 | let timer = null; 25 | 26 | // interval timers 27 | let interval_timers = []; 28 | 29 | // prepare a new thisArgs for live methods 30 | const push = this.push; 31 | const thisArgs = Object.assign({}, this, { 32 | push(data) { 33 | // keepalive(); 34 | push(method, data); 35 | }, 36 | pushInterval(getData, interval) { 37 | if (typeof getData !== 'function') { 38 | throw Error('"getData" must be a function'); 39 | } 40 | const tick = async () => { 41 | try { 42 | this.push(await getData()); 43 | } catch (err) { 44 | console.error(err); 45 | } 46 | }; 47 | tick(); 48 | const tm = setInterval(tick, interval); 49 | interval_timers.push(tm); 50 | }, 51 | }); 52 | const unregister = await func.call(thisArgs, args, {}); 53 | 54 | // set a timeout here in case the client doesn't call _unregister_server_push() 55 | timer = setTimeout(dispose, SERVER_PUSH_DISPOSE_TIMEOUT); 56 | 57 | // remember to remove this handler from ctx.push_handlers after unregister() 58 | async function dispose() { 59 | clearTimeout(timer); 60 | interval_timers.forEach(clearTimeout); 61 | if (typeof unregister === 'function') { 62 | await unregister(); 63 | } 64 | delete push_handlers[method]; 65 | } 66 | 67 | // reset timeout timer 68 | function keepalive() { 69 | if (timer) { 70 | clearTimeout(timer); 71 | timer = setTimeout(dispose, SERVER_PUSH_DISPOSE_TIMEOUT); 72 | } 73 | } 74 | 75 | // put it to ctx so that it can be accessed from 76 | // _keepalive_server_push() and _unregister_server_push() 77 | push_handlers[method] = { 78 | // this function should be called in _keepalive_server_push() 79 | keepalive, 80 | // this function should be called in _unregister_server_push() 81 | dispose, 82 | }; 83 | 84 | return { code: SERVER_PUSH_REGISTER_SUCCESS }; 85 | }; 86 | -------------------------------------------------------------------------------- /core/src/methods/_unregister_server_push.js: -------------------------------------------------------------------------------- 1 | module.exports = async function unregister_server_push({ method }) { 2 | const handler = this.ctx.push_handlers[method]; 3 | if (handler) { 4 | await handler.dispose(); 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /core/src/methods/add_setting.js: -------------------------------------------------------------------------------- 1 | const bsInit = require('blinksocks/bin/init'); 2 | const { RUN_TYPE_CLIENT, RUN_TYPE_SERVER } = require('../constants'); 3 | 4 | module.exports = async function add_setting({ remarks }) { 5 | const { runType } = this.ctx; 6 | const { clientJson, serverJson } = bsInit({ isMinimal: false, isDryRun: true }); 7 | const configs = this.db.getConfigs(); 8 | if (runType === RUN_TYPE_CLIENT) { 9 | return configs.insert(Object.assign({}, clientJson, { remarks })).write().id; 10 | } 11 | if (runType === RUN_TYPE_SERVER) { 12 | return configs.insert(Object.assign({}, serverJson, { remarks })).write().id; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /core/src/methods/add_user.js: -------------------------------------------------------------------------------- 1 | const { hash_password } = require('../utils'); 2 | 3 | module.exports = async function add_user({ user }) { 4 | if (typeof user !== 'object') { 5 | throw Error('invalid parameter'); 6 | } 7 | 8 | const { name, password } = user; 9 | if (typeof name !== 'string' || name.length < 1) { 10 | throw Error('username is invalid'); 11 | } 12 | if (typeof password !== 'string' || password.length < 1) { 13 | throw Error('password is invalid'); 14 | } 15 | 16 | if (this.db.get('users').find({ name }).value()) { 17 | throw Error('user is already exists'); 18 | } 19 | 20 | this.db.get('users').insert({ 21 | name: name, 22 | password: hash_password(password), 23 | disallowed_methods: [], 24 | }).write(); 25 | }; 26 | -------------------------------------------------------------------------------- /core/src/methods/copy_setting.js: -------------------------------------------------------------------------------- 1 | module.exports = async function copy_setting({ id }) { 2 | const configs = this.db.getConfigs(); 3 | const config = configs.find({ id }).cloneDeep().value(); 4 | delete config.id; 5 | return configs.insert(config).write(); 6 | }; 7 | -------------------------------------------------------------------------------- /core/src/methods/delete_setting.js: -------------------------------------------------------------------------------- 1 | module.exports = async function delete_setting({ id }) { 2 | return this.db.getConfigs().remove({ id }).write(); 3 | }; 4 | -------------------------------------------------------------------------------- /core/src/methods/delete_user.js: -------------------------------------------------------------------------------- 1 | module.exports = async function delete_user({ id }) { 2 | this.db.get('users').remove({ id }).write(); 3 | }; 4 | -------------------------------------------------------------------------------- /core/src/methods/get_auto_start_services.js: -------------------------------------------------------------------------------- 1 | module.exports = async function get_auto_start_services() { 2 | return this.db.get('auto_start_services'); 3 | }; 4 | -------------------------------------------------------------------------------- /core/src/methods/get_config.js: -------------------------------------------------------------------------------- 1 | const { RUN_TYPE_CLIENT, RUN_TYPE_SERVER, DESENSITIZE_PLACEHOLDER } = require('../constants'); 2 | 3 | module.exports = async function get_config({ id }, { desensitize = true }) { 4 | const { runType } = this.ctx; 5 | const config = this.db.getConfigs().find({ id }).cloneDeep().value(); 6 | if (!config) { 7 | throw Error('config is not found'); 8 | } 9 | if (desensitize) { 10 | if (runType === RUN_TYPE_CLIENT) { 11 | config.server.key = DESENSITIZE_PLACEHOLDER; 12 | } 13 | if (runType === RUN_TYPE_SERVER) { 14 | config.key = DESENSITIZE_PLACEHOLDER; 15 | } 16 | } 17 | return config; 18 | }; 19 | -------------------------------------------------------------------------------- /core/src/methods/get_env.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | 3 | /** 4 | * return os information and Node.js versions 5 | */ 6 | module.exports = async function get_env() { 7 | const osParams = [ 8 | ['cpu', os.cpus()[0].model], 9 | ['cores', os.cpus().length], 10 | ['memory', os.totalmem()], 11 | ['type', os.type()], 12 | ['platform', os.platform()], 13 | ['arch', os.arch()], 14 | ['release', os.release()] 15 | ]; 16 | const nodeVersions = []; 17 | for (const [key, value] of Object.entries(process.versions)) { 18 | nodeVersions.push([key, value]); 19 | } 20 | return { 21 | version: require('../../package').version, 22 | blinksocksVersion: require('blinksocks/package').version, 23 | runType: this.ctx.runType, 24 | os: osParams, 25 | node: nodeVersions, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /core/src/methods/get_geoip.js: -------------------------------------------------------------------------------- 1 | const { ServiceManager } = require('../utils'); 2 | 3 | module.exports = async function get_geoip({ id }) { 4 | return ServiceManager.getServiceGeoIPs(id); 5 | }; 6 | -------------------------------------------------------------------------------- /core/src/methods/get_preset_defs.js: -------------------------------------------------------------------------------- 1 | module.exports = async function get_preset_defs() { 2 | return [ 3 | { 4 | "name": "ss-base", 5 | "params": [], 6 | "isAddressing": true 7 | }, 8 | { 9 | "name": "ss-stream-cipher", 10 | "params": [{ 11 | "key": "method", 12 | "type": "enum", 13 | "values": [ 14 | "aes-128-ctr", 15 | "aes-192-ctr", 16 | "aes-256-ctr", 17 | "aes-128-cfb", 18 | "aes-192-cfb", 19 | "aes-256-cfb", 20 | "camellia-128-cfb", 21 | "camellia-192-cfb", 22 | "camellia-256-cfb", 23 | "rc4-md5", 24 | "rc4-md5-6", 25 | "none" 26 | ], 27 | "defaultValue": "aes-128-ctr", 28 | "description": "encryption/decryption algorithm", 29 | "optional": true 30 | }] 31 | }, 32 | { 33 | "name": "ss-aead-cipher", 34 | "params": [{ 35 | "key": "method", 36 | "type": "enum", 37 | "values": [ 38 | "aes-128-gcm", 39 | "aes-192-gcm", 40 | "aes-256-gcm", 41 | "chacha20-poly1305", 42 | "chacha20-ietf-poly1305", 43 | "xchacha20-ietf-poly1305" 44 | ], 45 | "defaultValue": "aes-128-gcm", 46 | "description": "encryption/decryption algorithm", 47 | "optional": true 48 | }] 49 | }, 50 | { 51 | "name": "ssr-auth-aes128-md5", 52 | "params": [] 53 | }, 54 | { 55 | "name": "ssr-auth-aes128-sha1", 56 | "params": [] 57 | }, 58 | { 59 | "name": "ssr-auth-chain-a", 60 | "params": [] 61 | }, 62 | { 63 | "name": "ssr-auth-chain-b", 64 | "params": [] 65 | }, 66 | { 67 | "name": "v2ray-vmess", 68 | "params": [{ 69 | "key": "id", 70 | "type": "string", 71 | "defaultValue": "", 72 | "description": "uuid", 73 | "optional": false 74 | }, { 75 | "key": "security", 76 | "type": "enum", 77 | "values": [ 78 | "aes-128-gcm", 79 | "chacha20-poly1305", 80 | "none" 81 | ], 82 | "defaultValue": "aes-128-gcm", 83 | "description": "encryption/decryption algorithm", 84 | "optional": true 85 | }] 86 | }, 87 | { 88 | "name": "obfs-random-padding", 89 | "params": [] 90 | }, 91 | // { 92 | // "name": "obfs-http", 93 | // "params": [ 94 | // { 95 | // "key": "file", 96 | // "type": "string", 97 | // "defaultValue": "" 98 | // } 99 | // ] 100 | // }, 101 | { 102 | "name": "obfs-tls1.2-ticket", 103 | "params": [{ 104 | "key": "sni", 105 | "type": "array", 106 | "defaultValue": [], 107 | "description": "server name indication", 108 | "optional": false 109 | }] 110 | }, 111 | { 112 | "name": "base-auth", 113 | "params": [{ 114 | "key": "method", 115 | "type": "enum", 116 | "values": [ 117 | "md5", 118 | "sha1", 119 | "sha256" 120 | ], 121 | "defaultValue": "sha1", 122 | "description": "hash algorithm", 123 | "optional": true 124 | }], 125 | "isAddressing": true 126 | }, 127 | { 128 | "name": "aead-random-cipher", 129 | "params": [{ 130 | "key": "method", 131 | "type": "enum", 132 | "values": [ 133 | "aes-128-gcm", 134 | "aes-192-gcm", 135 | "aes-256-gcm" 136 | ], 137 | "defaultValue": "aes-128-gcm", 138 | "description": "encryption/decryption algorithm", 139 | "optional": true 140 | }, { 141 | "key": "info", 142 | "type": "string", 143 | "defaultValue": "bs-subkey", 144 | "description": "", 145 | "optional": true 146 | }, { 147 | "key": "factor", 148 | "type": "number", 149 | "defaultValue": 2, 150 | "description": "", 151 | "optional": true 152 | }] 153 | }, 154 | // { 155 | // "name": "auto-conf", 156 | // "params": [] 157 | // }, 158 | ]; 159 | }; 160 | -------------------------------------------------------------------------------- /core/src/methods/get_service.js: -------------------------------------------------------------------------------- 1 | module.exports = async function get_service({ id }) { 2 | return this.db.getConfigs().find({ id }).value(); 3 | }; 4 | -------------------------------------------------------------------------------- /core/src/methods/get_service_logs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const utils = require('util'); 3 | const { RUNTIME_LOG_PATH } = require('../constants'); 4 | 5 | const readdir = utils.promisify(fs.readdir); 6 | 7 | module.exports = async function get_service_logs({ id }) { 8 | return (await readdir(RUNTIME_LOG_PATH)) 9 | .filter(name => name.startsWith(id + '.log')) 10 | .sort(); 11 | }; 12 | -------------------------------------------------------------------------------- /core/src/methods/get_service_metrics.js: -------------------------------------------------------------------------------- 1 | const { ServiceManager } = require('../utils'); 2 | 3 | module.exports = async function get_service_metrics({ id }) { 4 | const [cpu_metrics, memory_metrics, speed_metrics, connections_metrics, traffic_metrics] = await Promise.all([ 5 | ServiceManager.getMetrics(id, 'cpu'), 6 | ServiceManager.getMetrics(id, 'memory'), 7 | ServiceManager.getMetrics(id, 'speed'), 8 | ServiceManager.getMetrics(id, 'connections'), 9 | ServiceManager.getMetrics(id, 'traffic'), 10 | ]); 11 | return { 12 | cpu_metrics, 13 | memory_metrics, 14 | speed_metrics, 15 | connections_metrics, 16 | traffic_metrics, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /core/src/methods/get_services.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | 3 | module.exports = async function get_services() { 4 | return this.db 5 | .getConfigs() 6 | .map(({ id, service, log_path, remarks }) => { 7 | const { protocol, hostname, port } = url.parse(service); 8 | return { 9 | id: id, 10 | protocol: protocol ? protocol.slice(0, -1) : '-', 11 | address: `${hostname}:${port}`, 12 | remarks: remarks || '', 13 | log_path: log_path || '-', 14 | }; 15 | }) 16 | .value(); 17 | }; 18 | -------------------------------------------------------------------------------- /core/src/methods/get_system_pac.js: -------------------------------------------------------------------------------- 1 | const child_process = require('child_process'); 2 | const { RUNTIME_HELPERS_PAC_PATH } = require('../constants'); 3 | 4 | module.exports = async function get_system_pac() { 5 | return new Promise((resolve, reject) => { 6 | child_process.exec(RUNTIME_HELPERS_PAC_PATH + ' show', { encoding: 'utf-8' }, (err, result) => { 7 | if (err) { 8 | reject(err); 9 | } else { 10 | resolve(result); 11 | } 12 | }); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /core/src/methods/get_system_proxy.js: -------------------------------------------------------------------------------- 1 | const child_process = require('child_process'); 2 | const sudo = require('sudo-prompt'); 3 | const { RUNTIME_HELPERS_SYSPROXY_PATH } = require('../constants'); 4 | 5 | module.exports = async function get_system_proxy() { 6 | const command = RUNTIME_HELPERS_SYSPROXY_PATH + ' show'; 7 | return new Promise((resolve, reject) => { 8 | if (process.platform === 'darwin') { 9 | sudo.exec(command, { name: 'blinksocksGUI' }, (err, stdout) => { 10 | if (err) { 11 | reject(err); 12 | } else { 13 | resolve(stdout); 14 | } 15 | }); 16 | } else { 17 | child_process.exec(command, { encoding: 'utf-8' }, (err, result) => { 18 | if (err) { 19 | reject(err); 20 | } else { 21 | resolve(result); 22 | } 23 | }); 24 | } 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /core/src/methods/get_users.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const { DESENSITIZE_PLACEHOLDER } = require('../constants'); 3 | 4 | function merge(method_mapping, disallowed_methods) { 5 | for (const name of disallowed_methods) { 6 | method_mapping[name] = false; 7 | } 8 | return _.transform(method_mapping, (result, value, key) => result.push({ 9 | name: key, 10 | active: !!value, 11 | }), []); 12 | } 13 | 14 | module.exports = async function get_users() { 15 | return this.db 16 | .get('users') 17 | .map(({ id, name, password, disallowed_methods }) => { 18 | const method_mapping = _.transform(this.getConfigurableMethods(), (result, name) => result[name] = true, {}); 19 | return { 20 | id, 21 | name, 22 | password: DESENSITIZE_PLACEHOLDER, 23 | methods: merge(method_mapping, disallowed_methods || []) 24 | .sort((a, b) => a.name.localeCompare(b.name)), 25 | }; 26 | }) 27 | .value(); 28 | }; 29 | -------------------------------------------------------------------------------- /core/src/methods/restart_service.js: -------------------------------------------------------------------------------- 1 | const { ServiceManager } = require('../utils'); 2 | 3 | module.exports = async function restart_service({ id }) { 4 | await this.invoke('stop_service', { id }); 5 | await this.invoke('start_service', { id }); 6 | return ServiceManager.getServiceStatus(id); 7 | }; 8 | -------------------------------------------------------------------------------- /core/src/methods/save_setting.js: -------------------------------------------------------------------------------- 1 | const { Config } = require('blinksocks'); 2 | const { RUN_TYPE_CLIENT, RUN_TYPE_SERVER, DESENSITIZE_PLACEHOLDER } = require('../constants'); 3 | 4 | module.exports = async function save_setting({ id, config }) { 5 | const { runType } = this.ctx; 6 | const configs = this.db.getConfigs(); 7 | const prevConfig = configs.find({ id }).value(); 8 | switch (runType) { 9 | case RUN_TYPE_CLIENT: 10 | Config.testOnClient(config); 11 | if (config.server.key === DESENSITIZE_PLACEHOLDER) { 12 | config.server.key = prevConfig.server.key; 13 | } 14 | break; 15 | case RUN_TYPE_SERVER: 16 | Config.testOnServer(config); 17 | if (config.key === DESENSITIZE_PLACEHOLDER) { 18 | config.key = prevConfig.key; 19 | } 20 | break; 21 | } 22 | configs.find({ id }).assign(config).write(); 23 | }; 24 | -------------------------------------------------------------------------------- /core/src/methods/save_user.js: -------------------------------------------------------------------------------- 1 | const { hash_password } = require('../utils'); 2 | const { DESENSITIZE_PLACEHOLDER } = require('../constants'); 3 | 4 | module.exports = async function save_user({ user }) { 5 | if (!user || typeof user !== 'object') { 6 | throw Error('invalid parameter'); 7 | } 8 | 9 | // strict check 10 | const { id, name, password, methods } = user; 11 | if (typeof name !== 'string' || name.length < 1) { 12 | throw Error('name is invalid'); 13 | } 14 | if (typeof password !== 'string' || password.length < 1) { 15 | throw Error('password is invalid'); 16 | } 17 | if (!Array.isArray(methods)) { 18 | throw Error('methods is invalid'); 19 | } 20 | 21 | // all exist method names 22 | const method_names = this.getConfigurableMethods(); 23 | for (const method of methods) { 24 | const { name, active } = method; 25 | if (!method_names.includes(name)) { 26 | throw Error(`method: "${name}" is not allowed here`); 27 | } 28 | if (typeof active !== 'boolean') { 29 | throw Error('active is invalid'); 30 | } 31 | } 32 | 33 | const _user = this.db.get('users').find({ id }).value(); 34 | if (!_user) { 35 | throw Error(`user "${name}" is not exist`); 36 | } 37 | 38 | this.db.get('users').find({ id }).assign({ 39 | name: name, 40 | password: password === DESENSITIZE_PLACEHOLDER ? _user.password : hash_password(password), 41 | disallowed_methods: methods.filter(({ active }) => !active).map(({ name }) => name), 42 | }).write(); 43 | }; 44 | -------------------------------------------------------------------------------- /core/src/methods/set_service_auto_start.js: -------------------------------------------------------------------------------- 1 | module.exports = async function set_service_auto_start({ id }) { 2 | const configs = this.db.getConfigs(); 3 | const services = this.db.get('auto_start_services'); 4 | 5 | if (!configs.find({ id }).value()) { 6 | throw Error('unknown service'); 7 | } 8 | 9 | if (services.value().indexOf(id) < 0) { 10 | services.insert(id).write(); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /core/src/methods/set_system_pac.js: -------------------------------------------------------------------------------- 1 | module.exports = async function set_system_pac({ sysPac }) { 2 | 3 | }; 4 | -------------------------------------------------------------------------------- /core/src/methods/set_system_proxy.js: -------------------------------------------------------------------------------- 1 | module.exports = async function set_system_proxy({ sysProxy }) { 2 | 3 | }; 4 | -------------------------------------------------------------------------------- /core/src/methods/start_service.js: -------------------------------------------------------------------------------- 1 | const { Config } = require('blinksocks'); 2 | const { ServiceManager } = require('../utils'); 3 | 4 | module.exports = async function start_service({ id }) { 5 | const config = await this.invoke('get_config', { id }, { desensitize: false }); 6 | Config.test(config); 7 | await ServiceManager.start(id, config); 8 | return ServiceManager.getServiceStatus(id); 9 | }; 10 | -------------------------------------------------------------------------------- /core/src/methods/stop_service.js: -------------------------------------------------------------------------------- 1 | const { ServiceManager } = require('../utils'); 2 | 3 | module.exports = async function stop_service({ id }) { 4 | await ServiceManager.stop(id); 5 | return ServiceManager.getServiceStatus(id); 6 | }; 7 | -------------------------------------------------------------------------------- /core/src/methods/unset_service_auto_start.js: -------------------------------------------------------------------------------- 1 | module.exports = async function unset_service_auto_start({ id }) { 2 | const configs = this.db.getConfigs(); 3 | const services = this.db.get('auto_start_services'); 4 | 5 | if (!configs.find({ id }).value()) { 6 | throw Error('unknown service'); 7 | } 8 | 9 | services.pullAll([id]).write(); 10 | }; 11 | -------------------------------------------------------------------------------- /core/src/methods/update_remarks.js: -------------------------------------------------------------------------------- 1 | module.exports = async function update_remarks({ id, remarks }) { 2 | if (typeof remarks !== 'string' || remarks.length < 1) { 3 | throw Error('invalid parameter'); 4 | } 5 | this.db.getConfigs().find({ id }).assign({ remarks }).write(); 6 | }; 7 | -------------------------------------------------------------------------------- /core/src/utils/_fork.js: -------------------------------------------------------------------------------- 1 | const pidusage = require('pidusage'); 2 | const dateFns = require('date-fns'); 3 | const { Callee } = require('node-ipc-call'); 4 | 5 | const { Hub } = require('blinksocks'); 6 | 7 | let hub = null; 8 | 9 | async function getUsage() { 10 | const stats = await pidusage(process.pid); 11 | return { 12 | cpuUsage: stats.cpu, 13 | memoryUsage: stats.memory, 14 | }; 15 | } 16 | 17 | const QUEUE_SIZE = 300; // 5min 18 | 19 | // metrics collector 20 | const Monitor = { 21 | 22 | _timer: null, 23 | 24 | _cpu_metrics: [ 25 | // timestamp, cpu_percentage 26 | ], 27 | 28 | _memory_metrics: [ 29 | // timestamp, memory_usage 30 | ], 31 | 32 | _upload_speed_metrics: [ 33 | // timestamp, memory_usage 34 | ], 35 | 36 | _download_speed_metrics: [ 37 | // timestamp, memory_usage 38 | ], 39 | 40 | _connections_metrics: [ 41 | // timestamp, connections 42 | ], 43 | 44 | _upload_traffic_metrics: [ 45 | // timestamp, total_bytes 46 | ], 47 | 48 | _download_traffic_metrics: [ 49 | // timestamp, total_bytes 50 | ], 51 | 52 | getCPUMetrics() { 53 | return this._cpu_metrics; 54 | }, 55 | 56 | getMemoryMetrics() { 57 | return this._memory_metrics; 58 | }, 59 | 60 | getUploadSpeedMetrics() { 61 | return this._upload_speed_metrics; 62 | }, 63 | 64 | getDownloadSpeedMetrics() { 65 | return this._download_speed_metrics; 66 | }, 67 | 68 | getConnectionsMetrics() { 69 | return this._connections_metrics; 70 | }, 71 | 72 | getUploadTrafficMetrics() { 73 | return this._upload_traffic_metrics; 74 | }, 75 | 76 | getDownloadTrafficMetrics() { 77 | return this._download_traffic_metrics; 78 | }, 79 | 80 | start() { 81 | this._timer = setInterval(this._sample.bind(this), 1e3); 82 | }, 83 | 84 | stop() { 85 | clearInterval(this._timer); 86 | }, 87 | 88 | async _sample() { 89 | const { _cpu_metrics, _memory_metrics } = this; 90 | const { _connections_metrics, _upload_speed_metrics, _download_speed_metrics } = this; 91 | const { _upload_traffic_metrics, _download_traffic_metrics } = this; 92 | try { 93 | const { cpuUsage, memoryUsage } = await getUsage(); 94 | const dateStr = dateFns.format(Date.now(), 'HH:mm:ss'); 95 | 96 | _cpu_metrics.push([dateStr, cpuUsage > 1 ? 1 : cpuUsage]); 97 | _memory_metrics.push([dateStr, memoryUsage]); 98 | 99 | if (hub) { 100 | const connections = await hub.getConnections(); 101 | _upload_speed_metrics.push([dateStr, hub.getUploadSpeed()]); 102 | _download_speed_metrics.push([dateStr, hub.getDownloadSpeed()]); 103 | _connections_metrics.push([dateStr, connections]); 104 | _upload_traffic_metrics.push([dateStr, hub.getTotalWritten()]); 105 | _download_traffic_metrics.push([dateStr, hub.getTotalRead()]); 106 | } 107 | 108 | const metricsCollection = [ 109 | _cpu_metrics, 110 | _memory_metrics, 111 | _upload_speed_metrics, 112 | _download_speed_metrics, 113 | _connections_metrics, 114 | _upload_traffic_metrics, 115 | _download_traffic_metrics, 116 | ]; 117 | 118 | metricsCollection.forEach((metrics) => { 119 | if (metrics.length > QUEUE_SIZE) { 120 | metrics.shift(); 121 | } 122 | }); 123 | } catch (err) { 124 | console.error(err); 125 | } 126 | } 127 | 128 | }; 129 | 130 | const callee = new Callee(); 131 | 132 | // process methods mapping 133 | callee.register({ 134 | // start hub 135 | 'start': async function start(config) { 136 | if (!hub) { 137 | hub = new Hub(config); 138 | Monitor.start(); 139 | return hub.run(); 140 | } 141 | }, 142 | // stop hub 143 | 'stop': async function stop() { 144 | if (hub) { 145 | Monitor.stop(); 146 | return hub.terminate(); 147 | } 148 | }, 149 | // get status from hub 150 | 'getStatus': async function getStatus() { 151 | if (hub) { 152 | return { 153 | connections: await hub.getConnections(), 154 | download_speed: hub.getDownloadSpeed(), 155 | upload_speed: hub.getUploadSpeed(), 156 | }; 157 | } 158 | }, 159 | // get connection statuses from hub 160 | 'getConnStatuses': async function getConnStatuses() { 161 | if (hub) { 162 | return hub.getConnStatuses(); 163 | } 164 | }, 165 | // get current process cpu metrics 166 | 'getCPUMetrics': () => Monitor.getCPUMetrics(), 167 | // get current process memory metrics 168 | 'getMemoryMetrics': () => Monitor.getMemoryMetrics(), 169 | // get current process upload speed and download speed metrics 170 | 'getSpeedMetrics': () => [Monitor.getUploadSpeedMetrics(), Monitor.getDownloadSpeedMetrics()], 171 | // get current process connections number 172 | 'getConnectionsMetrics': () => Monitor.getConnectionsMetrics(), 173 | // get current process upload traffic and download traffic 174 | 'getTrafficMetrics': () => [Monitor.getUploadTrafficMetrics(), Monitor.getDownloadTrafficMetrics()], 175 | }); 176 | 177 | callee.listen(); 178 | -------------------------------------------------------------------------------- /core/src/utils/db.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fsExtra = require('fs-extra'); 3 | const lodashId = require('lodash-id'); 4 | const lowdb = require('lowdb'); 5 | const FileSync = require('lowdb/adapters/FileSync'); 6 | 7 | const { DATABASE_PATH } = require('../constants'); 8 | 9 | const DATABASE_SCHEMA = { 10 | "client_configs": [], 11 | "server_configs": [], 12 | "users": [], 13 | "auto_start_services": [], 14 | "runtime": { 15 | "ip": "", 16 | }, 17 | }; 18 | 19 | fsExtra.mkdirpSync(path.dirname(DATABASE_PATH)); 20 | 21 | const db = lowdb(new FileSync(DATABASE_PATH, { 22 | defaultValue: DATABASE_SCHEMA, 23 | serialize: (data) => JSON.stringify(data, null, 2), 24 | deserialize: JSON.parse, 25 | })); 26 | 27 | db.defaults(DATABASE_SCHEMA).write(); 28 | db._.mixin(lodashId); 29 | 30 | module.exports = db; 31 | -------------------------------------------------------------------------------- /core/src/utils/geoip.js: -------------------------------------------------------------------------------- 1 | const dns = require('dns'); 2 | const net = require('net'); 3 | const http = require('http'); 4 | const _ = require('lodash'); 5 | 6 | const logger = require('./logger'); 7 | 8 | async function lookup(hostname) { 9 | return new Promise((resolve, reject) => { 10 | dns.lookup(hostname, function (err, address) { 11 | if (err) { 12 | reject(err); 13 | } else { 14 | resolve(address); 15 | } 16 | }); 17 | }); 18 | } 19 | 20 | async function getGeoInfo(ip) { 21 | return new Promise((resolve, reject) => { 22 | const request = http.get('http://ip-api.com/json/' + ip, (res) => { 23 | res.setEncoding('utf-8'); 24 | res.on('data', (data) => { 25 | try { 26 | const json = JSON.parse(data); 27 | if (json.status === 'success') { 28 | resolve(json); 29 | } 30 | } catch (err) { 31 | return reject(err); 32 | } 33 | reject(Error('couldn\'t get geo info of ' + ip)); 34 | }); 35 | res.on('error', reject); 36 | }); 37 | request.on('error', reject); 38 | }); 39 | } 40 | 41 | function isPrivateIP(ip) { 42 | return ( 43 | /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(ip) || 44 | /^(::f{4}:)?192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(ip) || 45 | /^(::f{4}:)?172\.(1[6-9]|2\d|30|31)\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(ip) || 46 | /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(ip) || 47 | /^(::f{4}:)?169\.254\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(ip) || 48 | /^f[cd][0-9a-f]{2}:/i.test(ip) || 49 | /^fe80:/i.test(ip) || 50 | /^::1$/.test(ip) || 51 | /^::$/.test(ip) 52 | ); 53 | } 54 | 55 | const MAX_QUEUE_SIZE = 500; 56 | 57 | class GeoIP { 58 | 59 | constructor() { 60 | this._uniqueKeys = new Set(); 61 | this._store = []; 62 | } 63 | 64 | // lookup ip address to geo location 65 | async put(arg, extra = {}) { 66 | if (typeof arg !== 'string' || !arg) { 67 | return; 68 | } 69 | 70 | if (this._uniqueKeys.has(arg)) { 71 | return; 72 | } else { 73 | this._uniqueKeys.add(arg); 74 | } 75 | 76 | let ip; 77 | try { 78 | ip = net.isIP(arg) ? arg : await lookup(arg); 79 | } catch (err) { 80 | return; 81 | } 82 | 83 | // ignore private ip 84 | if (isPrivateIP(ip)) { 85 | return; 86 | } 87 | 88 | try { 89 | const geoInfo = await getGeoInfo(ip); 90 | const { lat, lon: lng, query } = geoInfo; 91 | 92 | // merge hostname and ip by the same geo location 93 | const item = this._store.find((v) => v.lat === lat && v.lng === lng); 94 | if (item) { 95 | item.ips.push(query); 96 | if (item.ips.length > 20) { 97 | item.ips.shift(); 98 | } 99 | if (item.hostname && extra.hostname) { 100 | item.hostname.push(extra.hostname); 101 | if (item.hostname.length > 20) { 102 | item.hostname.shift(); 103 | } 104 | } 105 | } else { 106 | const obj = _.pick(geoInfo, ['as', 'city', 'country', 'lat', 'org', 'regionName']); 107 | obj['ips'] = [query]; 108 | obj['lng'] = lng; 109 | this._store.push(Object.assign({}, obj, extra)); 110 | if (this._store.length > MAX_QUEUE_SIZE) { 111 | this._store.shift(); 112 | } 113 | } 114 | } catch (err) { 115 | logger.error(err.message); 116 | this._uniqueKeys.delete(arg); 117 | } 118 | } 119 | 120 | // return all resolved ip and its geo location 121 | getStore() { 122 | return this._store; 123 | } 124 | 125 | clear() { 126 | this._uniqueKeys.clear(); 127 | this._store = []; 128 | } 129 | 130 | } 131 | 132 | module.exports = GeoIP; 133 | -------------------------------------------------------------------------------- /core/src/utils/hash.js: -------------------------------------------------------------------------------- 1 | const jsSHA = require('jssha'); 2 | 3 | module.exports = function hash(algorithm, message) { 4 | const shaObj = new jsSHA(algorithm, 'TEXT'); 5 | shaObj.update(message); 6 | return shaObj.getHash('HEX'); 7 | }; 8 | -------------------------------------------------------------------------------- /core/src/utils/hash_password.js: -------------------------------------------------------------------------------- 1 | const hash = require('./hash'); 2 | const { HASH_SALT } = require('../constants'); 3 | 4 | module.exports = function hash_password(plaintext) { 5 | return hash('SHA-256', plaintext + HASH_SALT); 6 | }; 7 | -------------------------------------------------------------------------------- /core/src/utils/import_dir.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const cache = {}; 5 | 6 | module.exports = function import_dir(dir) { 7 | if (cache[dir]) { 8 | return cache[dir]; 9 | } 10 | const files = fs.readdirSync(dir); 11 | const modules = {}; 12 | for (const file of files) { 13 | const name = path.basename(file, '.js'); 14 | if (name && name[0] !== '.') { 15 | modules[name] = require(path.resolve(dir, name)); 16 | } 17 | } 18 | return cache[dir] = modules; 19 | }; 20 | -------------------------------------------------------------------------------- /core/src/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | db: require('./db'), 3 | hash: require('./hash'), 4 | hash_password: require('./hash_password'), 5 | import_dir: require('./import_dir'), 6 | logger: require('./logger'), 7 | ServiceManager: require('./service_manager'), 8 | GeoIP: require('./geoip'), 9 | }; 10 | -------------------------------------------------------------------------------- /core/src/utils/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | 3 | const { transports, format: { printf, combine, colorize, timestamp, splat, prettyPrint } } = winston; 4 | 5 | module.exports = winston.createLogger({ 6 | level: 'silly', 7 | format: combine( 8 | timestamp(), 9 | splat(), 10 | colorize(), 11 | prettyPrint(), 12 | printf((info) => `${info.timestamp} - ${info.level}: ${info.message}`) 13 | ), 14 | transports: [ 15 | new transports.Console(), 16 | ] 17 | }); 18 | -------------------------------------------------------------------------------- /core/src/utils/service_manager.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const _ = require('lodash'); 3 | const { Caller } = require('node-ipc-call'); 4 | 5 | const db = require('./db'); 6 | const GeoIP = require('./geoip'); 7 | 8 | const { 9 | RUNTIME_PATH, 10 | SERVICE_STATUS_INIT, 11 | SERVICE_STATUS_RUNNING, 12 | SERVICE_STATUS_STOPPED, 13 | } = require('../constants'); 14 | 15 | const FORK_SCRIPT = path.resolve(__dirname, '_fork.js'); 16 | 17 | module.exports = { 18 | 19 | _subprocesses: new Map(/* : */), 20 | 21 | async start(id, config) { 22 | let sub = this._subprocesses.get(id); 23 | if (!sub) { 24 | sub = Caller.fork(FORK_SCRIPT, [], { 25 | cwd: RUNTIME_PATH, 26 | silent: process.env.NODE_ENV === 'production', 27 | }); 28 | sub.geoip = new GeoIP(); 29 | sub.geoip.put(db.get('runtime.ip').value(), { self: true }); 30 | this._subprocesses.set(id, sub); 31 | } 32 | try { 33 | // force logs to put into runtime/logs/ 34 | const configCopy = _.cloneDeep(config); 35 | configCopy.log_path = `logs/${id}.log`; 36 | await sub.invoke('start', configCopy); 37 | } catch (err) { 38 | this._subprocesses.delete(id); 39 | throw err; 40 | } 41 | }, 42 | 43 | async stop(id) { 44 | const sub = this._subprocesses.get(id); 45 | if (sub) { 46 | await sub.invoke('stop'); 47 | sub.destroy(); 48 | sub.geoip.clear(); 49 | this._subprocesses.set(id, null); 50 | } else { 51 | throw Error(`service(${id}) is not found`); 52 | } 53 | }, 54 | 55 | async getServices() { 56 | const services = {}; 57 | for (const [id] of this._subprocesses) { 58 | services[id] = await this.getServiceStatus(id); 59 | } 60 | return services; 61 | }, 62 | 63 | async getServiceStatus(id) { 64 | const sub = this._subprocesses.get(id); 65 | if (sub) { 66 | return { 67 | status: SERVICE_STATUS_RUNNING, 68 | ...(await sub.invoke('getStatus')), 69 | }; 70 | } else { 71 | let status; 72 | if (sub === undefined) { 73 | status = SERVICE_STATUS_INIT; 74 | } 75 | if (sub === null) { 76 | status = SERVICE_STATUS_STOPPED; 77 | } 78 | return { status }; 79 | } 80 | }, 81 | 82 | async getServiceConnStatuses(id) { 83 | const sub = this._subprocesses.get(id); 84 | if (sub) { 85 | const conns = await sub.invoke('getConnStatuses') || []; 86 | for (const { sourceHost, targetHost } of conns) { 87 | if (sourceHost) { 88 | sub.geoip.put(sourceHost, { hostname: [sourceHost], inbound: true }); 89 | } 90 | if (targetHost) { 91 | sub.geoip.put(targetHost, { hostname: [targetHost] }); 92 | } 93 | } 94 | return conns; 95 | } 96 | return []; 97 | }, 98 | 99 | async getMetrics(id, type) { 100 | const sub = this._subprocesses.get(id); 101 | if (sub) { 102 | switch (type) { 103 | case 'cpu': 104 | return await sub.invoke('getCPUMetrics'); 105 | case 'memory': 106 | return await sub.invoke('getMemoryMetrics'); 107 | case 'speed': 108 | return await sub.invoke('getSpeedMetrics'); 109 | case 'connections': 110 | return await sub.invoke('getConnectionsMetrics'); 111 | case 'traffic': 112 | return await sub.invoke('getTrafficMetrics'); 113 | default: 114 | break; 115 | } 116 | } else { 117 | return []; 118 | } 119 | }, 120 | 121 | getServiceGeoIPs(id) { 122 | const sub = this._subprocesses.get(id); 123 | if (sub) { 124 | return sub.geoip.getStore(); 125 | } else { 126 | return []; 127 | } 128 | }, 129 | 130 | }; 131 | -------------------------------------------------------------------------------- /core/vendor/pac-cmd/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *~ 3 | *.gch 4 | -------------------------------------------------------------------------------- /core/vendor/pac-cmd/Makefile: -------------------------------------------------------------------------------- 1 | # This Makefile is GNU make compatible. You can get GNU Make from 2 | # http://gnuwin32.sourceforge.net/packages/make.htm 3 | 4 | CCFLAGS = -Wall -c 5 | 6 | ifeq ($(OS),Windows_NT) 7 | os = windows 8 | CCFLAGS += -D WIN32 9 | # 32 bit `make` utility over 64 bit OS 10 | ifeq ($(PROCESSOR_ARCHITEW6432),AMD64) 11 | CCFLAGS += -D AMD64 12 | BIN = binaries/windows/pac_amd64 13 | else 14 | ifeq ($(PROCESSOR_ARCHITECTURE),AMD64) 15 | CCFLAGS += -D AMD64 16 | BIN = binaries/windows/pac_amd64 17 | endif 18 | ifeq ($(PROCESSOR_ARCHITECTURE),x86) 19 | CCFLAGS += -D IA32 20 | BIN = binaries/windows/pac_386 21 | endif 22 | endif 23 | LDFLAGS += -l rasapi32 -l wininet -Wl,--subsystem,windows 24 | else 25 | UNAME_S := $(shell uname -s) 26 | ifeq ($(UNAME_S),Linux) 27 | os = linux 28 | CCFLAGS += -D LINUX $(shell pkg-config --cflags gio-2.0) 29 | LDFLAGS += $(shell pkg-config --libs gio-2.0) 30 | UNAME_P := $(shell uname -p) 31 | ifeq ($(UNAME_P),x86_64) 32 | CCFLAGS += -D AMD64 33 | BIN = binaries/linux_amd64/pac 34 | endif 35 | ifneq ($(filter %86,$(UNAME_P)),) 36 | CCFLAGS += -D IA32 37 | BIN = binaries/linux_386/pac 38 | endif 39 | ifneq ($(filter arm%,$(UNAME_P)),) 40 | CCFLAGS += -D ARM 41 | BIN = binaries/linux_arm/pac 42 | endif 43 | endif 44 | ifeq ($(UNAME_S),Darwin) 45 | os = darwin 46 | CCFLAGS += -D DARWIN -D AMD64 -x objective-c 47 | LDFLAGS += -framework Cocoa -framework SystemConfiguration -framework Security 48 | BIN = binaries/darwin/pac 49 | endif 50 | endif 51 | 52 | CC=gcc 53 | 54 | all: $(BIN) 55 | main.o: main.c common.h 56 | $(CC) $(CCFLAGS) $^ 57 | $(os).o: $(os).c common.h 58 | $(CC) $(CCFLAGS) $^ 59 | $(BIN): $(os).o main.o 60 | $(CC) -o $@ $^ $(LDFLAGS) 61 | 62 | clean: 63 | rm *.o 64 | -------------------------------------------------------------------------------- /core/vendor/pac-cmd/README.md: -------------------------------------------------------------------------------- 1 | # pac-cmd 2 | 3 | A command line tool to change proxy auto-config settings of operation system. 4 | 5 | Binaries included in repo. Simply `make` to build it again. 6 | 7 | Note - you will need to run make separately on each platform. 8 | 9 | # Usage 10 | 11 | ```sh 12 | pac show 13 | pac on 14 | pac off [old-pac-url-prefix] 15 | ``` 16 | 17 | `pac off` with `old-pac-url` will turn off pac setting only if the existing pac url is prefixed with `old-pac-url-prefix`. 18 | 19 | #Notes 20 | 21 | * **Mac** 22 | 23 | Setting pac is an privileged action on Mac OS. `sudo` or elevate it as below. 24 | 25 | There's an additional option to chown itself to root:wheel and add setuid bit. 26 | 27 | ```sh 28 | pac setuid 29 | ``` 30 | 31 | * **Windows** 32 | 33 | Install [MinGW-W64](http://sourceforge.net/projects/mingw-w64) to build pac, as it has up to date SDK headers we require. 34 | 35 | To avoid bringing up console window, it doesn't show anything directly to console. Piping the result to other utilities should work. 36 | ``` 37 | pac show | cat 38 | ``` 39 | 40 | * **Linux** 41 | 42 | `sudo apt-get install libgtk2.0-dev` 43 | -------------------------------------------------------------------------------- /core/vendor/pac-cmd/binaries/darwin/pac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/vendor/pac-cmd/binaries/darwin/pac -------------------------------------------------------------------------------- /core/vendor/pac-cmd/binaries/linux_386/pac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/vendor/pac-cmd/binaries/linux_386/pac -------------------------------------------------------------------------------- /core/vendor/pac-cmd/binaries/linux_amd64/pac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/vendor/pac-cmd/binaries/linux_amd64/pac -------------------------------------------------------------------------------- /core/vendor/pac-cmd/binaries/windows/pac_386.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/vendor/pac-cmd/binaries/windows/pac_386.exe -------------------------------------------------------------------------------- /core/vendor/pac-cmd/binaries/windows/pac_amd64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/vendor/pac-cmd/binaries/windows/pac_amd64.exe -------------------------------------------------------------------------------- /core/vendor/pac-cmd/common.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #ifdef DARWIN 5 | int setUid(); 6 | int elevate(char *path, char *prompt, char *iconPath); 7 | #endif 8 | 9 | int show(); 10 | int togglePac(bool turnOn, const char* pacUrl); 11 | 12 | enum RET_ERRORS { 13 | RET_NO_ERROR = 0, 14 | INVALID_FORMAT = 1, 15 | NO_PERMISSION = 2, 16 | SYSCALL_FAILED = 3, 17 | NO_MEMORY = 4, 18 | PAC_URL_CONVERSION_ERROR = 5, 19 | }; 20 | -------------------------------------------------------------------------------- /core/vendor/pac-cmd/darwin.c: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | 6 | #include 7 | #include 8 | #include 9 | #include "common.h" 10 | 11 | /* === implement details === */ 12 | 13 | typedef Boolean (*visitor) (SCNetworkProtocolRef proxyProtocolRef, NSDictionary* oldPreferences, bool turnOn, const char* pacUrl); 14 | 15 | Boolean showAction(SCNetworkProtocolRef proxyProtocolRef/*unused*/, NSDictionary* oldPreferences, bool turnOn/*unused*/, const char* pacUrl/*unused*/) 16 | { 17 | NSNumber* on = [oldPreferences valueForKey:(NSString*)kSCPropNetProxiesProxyAutoConfigEnable]; 18 | NSString* nsOldPacUrl = [oldPreferences valueForKey:(NSString*)kSCPropNetProxiesProxyAutoConfigURLString]; 19 | if ([on intValue] == 1) { 20 | printf("%s\n", [nsOldPacUrl UTF8String]); 21 | } 22 | return TRUE; 23 | } 24 | 25 | Boolean toggleAction(SCNetworkProtocolRef proxyProtocolRef, NSDictionary* oldPreferences, bool turnOn, const char* pacUrl) 26 | { 27 | NSString* nsPacUrl = [[NSString alloc] initWithCString: pacUrl encoding:NSUTF8StringEncoding]; 28 | NSString* nsOldPacUrl; 29 | NSMutableDictionary *newPreferences = [NSMutableDictionary dictionaryWithDictionary: oldPreferences]; 30 | Boolean success; 31 | 32 | if(turnOn == true) { 33 | [newPreferences setValue:[NSNumber numberWithInt:1] forKey:(NSString*)kSCPropNetProxiesProxyAutoConfigEnable]; 34 | [newPreferences setValue:nsPacUrl forKey:(NSString*)kSCPropNetProxiesProxyAutoConfigURLString]; 35 | } else { 36 | nsOldPacUrl = [oldPreferences valueForKey:(NSString*)kSCPropNetProxiesProxyAutoConfigURLString]; 37 | // we turn pac off only if the option is set and pac url has the provided 38 | // prefix. 39 | if (nsPacUrl.length == 0 || [nsOldPacUrl hasPrefix:nsPacUrl]) { 40 | [newPreferences setValue:[NSNumber numberWithInt:0] forKey:(NSString*)kSCPropNetProxiesProxyAutoConfigEnable]; 41 | [newPreferences setValue:@"" forKey:(NSString*)kSCPropNetProxiesProxyAutoConfigURLString]; 42 | } 43 | } 44 | 45 | success = SCNetworkProtocolSetConfiguration(proxyProtocolRef, (__bridge CFDictionaryRef)newPreferences); 46 | if(!success) { 47 | NSLog(@"Failed to set Protocol Configuration"); 48 | } 49 | return success; 50 | } 51 | 52 | int visit(visitor v, bool persist, bool turnOn, const char* pacUrl) 53 | { 54 | int ret = RET_NO_ERROR; 55 | Boolean success; 56 | 57 | SCNetworkSetRef networkSetRef; 58 | CFArrayRef networkServicesArrayRef; 59 | SCNetworkServiceRef networkServiceRef; 60 | SCNetworkProtocolRef proxyProtocolRef; 61 | NSDictionary *oldPreferences; 62 | 63 | // Get System Preferences Lock 64 | SCPreferencesRef prefsRef = SCPreferencesCreate(NULL, CFSTR("org.getlantern.lantern"), NULL); 65 | 66 | if(prefsRef==NULL) { 67 | NSLog(@"Fail to obtain Preferences Ref"); 68 | ret = NO_PERMISSION; 69 | goto freePrefsRef; 70 | } 71 | 72 | success = SCPreferencesLock(prefsRef, true); 73 | if (!success) { 74 | NSLog(@"Fail to obtain PreferencesLock"); 75 | ret = NO_PERMISSION; 76 | goto freePrefsRef; 77 | } 78 | 79 | // Get available network services 80 | networkSetRef = SCNetworkSetCopyCurrent(prefsRef); 81 | if(networkSetRef == NULL) { 82 | NSLog(@"Fail to get available network services"); 83 | ret = SYSCALL_FAILED; 84 | goto freeNetworkSetRef; 85 | } 86 | 87 | //Look up interface entry 88 | networkServicesArrayRef = SCNetworkSetCopyServices(networkSetRef); 89 | networkServiceRef = NULL; 90 | for (long i = 0; i < CFArrayGetCount(networkServicesArrayRef); i++) { 91 | networkServiceRef = CFArrayGetValueAtIndex(networkServicesArrayRef, i); 92 | 93 | // Get proxy protocol 94 | proxyProtocolRef = SCNetworkServiceCopyProtocol(networkServiceRef, kSCNetworkProtocolTypeProxies); 95 | if(proxyProtocolRef == NULL) { 96 | NSLog(@"Couldn't acquire copy of proxyProtocol"); 97 | ret = SYSCALL_FAILED; 98 | goto freeProxyProtocolRef; 99 | } 100 | 101 | oldPreferences = (__bridge NSDictionary*)SCNetworkProtocolGetConfiguration(proxyProtocolRef); 102 | if (!v(proxyProtocolRef, oldPreferences, turnOn, pacUrl)) { 103 | ret = SYSCALL_FAILED; 104 | } 105 | 106 | freeProxyProtocolRef: 107 | CFRelease(proxyProtocolRef); 108 | } 109 | 110 | if (persist) { 111 | success = SCPreferencesCommitChanges(prefsRef); 112 | if(!success) { 113 | NSLog(@"Failed to Commit Changes"); 114 | ret = SYSCALL_FAILED; 115 | goto freeNetworkServicesArrayRef; 116 | } 117 | 118 | success = SCPreferencesApplyChanges(prefsRef); 119 | if(!success) { 120 | NSLog(@"Failed to Apply Changes"); 121 | ret = SYSCALL_FAILED; 122 | goto freeNetworkServicesArrayRef; 123 | } 124 | } 125 | 126 | //Free Resources 127 | freeNetworkServicesArrayRef: 128 | CFRelease(networkServicesArrayRef); 129 | freeNetworkSetRef: 130 | CFRelease(networkSetRef); 131 | freePrefsRef: 132 | SCPreferencesUnlock(prefsRef); 133 | CFRelease(prefsRef); 134 | 135 | return ret; 136 | } 137 | 138 | /* === public functions === */ 139 | int setUid() 140 | { 141 | char exeFullPath [PATH_MAX]; 142 | uint32_t size = PATH_MAX; 143 | if (_NSGetExecutablePath(exeFullPath, &size) != 0) 144 | { 145 | printf("Path longer than %d, should not occur!!!!!", size); 146 | return SYSCALL_FAILED; 147 | } 148 | if (chown(exeFullPath, 0, 0) != 0) // root:wheel 149 | { 150 | puts("Error chown"); 151 | return NO_PERMISSION; 152 | } 153 | if (chmod(exeFullPath, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH | S_ISUID) != 0) 154 | { 155 | puts("Error chmod"); 156 | return NO_PERMISSION; 157 | } 158 | return RET_NO_ERROR; 159 | } 160 | 161 | int show() 162 | { 163 | return visit(&showAction, false, false /*unused*/, "" /*unused*/); 164 | } 165 | 166 | int togglePac(bool turnOn, const char* pacUrl) 167 | { 168 | return visit(&toggleAction, true, turnOn, pacUrl); 169 | } 170 | -------------------------------------------------------------------------------- /core/vendor/pac-cmd/linux.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "common.h" 5 | 6 | void init() { 7 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" 8 | // deprecated since version 2.36, must leave here or prior glib will crash 9 | g_type_init(); 10 | #pragma GCC diagnostic warning "-Wdeprecated-declarations" 11 | } 12 | 13 | int show() 14 | { 15 | init(); 16 | GSettings* setting = g_settings_new("org.gnome.system.proxy"); 17 | char* old_mode = g_settings_get_string(setting, "mode"); 18 | char* old_pac_url = g_settings_get_string(setting, "autoconfig-url"); 19 | if (strcmp(old_mode, "auto") == 0) { 20 | printf("%s\n", old_pac_url); 21 | } 22 | return RET_NO_ERROR; 23 | } 24 | 25 | int togglePac(bool turnOn, const char* pacUrl) 26 | { 27 | int ret = RET_NO_ERROR; 28 | init(); 29 | GSettings* setting = g_settings_new("org.gnome.system.proxy"); 30 | if (turnOn == true) { 31 | gboolean success = g_settings_set_string(setting, "mode", "auto"); 32 | if (!success) { 33 | fprintf(stderr, "error setting mode to auto\n"); 34 | ret = SYSCALL_FAILED; 35 | goto cleanup; 36 | } 37 | success = g_settings_set_string(setting, "autoconfig-url", pacUrl); 38 | if (!success) { 39 | fprintf(stderr, "error setting autoconfig-url to %s\n", pacUrl); 40 | ret = SYSCALL_FAILED; 41 | goto cleanup; 42 | } 43 | } 44 | else { 45 | if (strlen(pacUrl) != 0) { 46 | char* old_mode = g_settings_get_string(setting, "mode"); 47 | char* old_pac_url = g_settings_get_string(setting, "autoconfig-url"); 48 | // we turn pac off only if the option is set and pac url has the provided 49 | // prefix. 50 | if (strcmp(old_mode, "auto") != 0 51 | || strncmp(old_pac_url, pacUrl, strlen(pacUrl)) != 0 ) { 52 | fprintf(stderr, "current pac url setting is not %s, skipping\n", pacUrl); 53 | goto cleanup; 54 | } 55 | } 56 | g_settings_reset(setting, "autoconfig-url"); 57 | gboolean success = g_settings_set_string(setting, "mode", "none"); 58 | if (!success) { 59 | fprintf(stderr, "error setting mode to none\n"); 60 | ret = SYSCALL_FAILED; 61 | goto cleanup; 62 | } 63 | } 64 | cleanup: 65 | g_settings_sync(); 66 | g_object_unref(setting); 67 | 68 | return ret; 69 | } 70 | -------------------------------------------------------------------------------- /core/vendor/pac-cmd/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "common.h" 5 | 6 | void usage(const char* binName) 7 | { 8 | printf("Usage: %s [show | on | off [prefix-of-old-pac-url]]\n", binName); 9 | exit(INVALID_FORMAT); 10 | } 11 | 12 | int main(int argc, char* argv[]) { 13 | if (argc < 2) { 14 | usage(argv[0]); 15 | } 16 | 17 | #ifdef DARWIN 18 | if (strcmp(argv[1], "setuid") == 0) { 19 | return setUid(); 20 | } 21 | #endif 22 | 23 | if (strcmp(argv[1], "show") == 0) { 24 | return show(); 25 | } else if (strcmp(argv[1], "on") == 0) { 26 | if (argc < 3) { 27 | usage(argv[0]); 28 | } 29 | return togglePac(true, argv[2]); 30 | } else if (strcmp(argv[1], "off") == 0) { 31 | return togglePac(false, argc < 3 ? "" : argv[2]); 32 | } else { 33 | usage(argv[0]); 34 | } 35 | // code never reaches here, just avoids compiler from complaining. 36 | return RET_NO_ERROR; 37 | } 38 | -------------------------------------------------------------------------------- /core/vendor/pac-cmd/windows.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "common.h" 8 | 9 | void reportWindowsError(const char* action) { 10 | LPTSTR pErrMsg = NULL; 11 | DWORD errCode = GetLastError(); 12 | FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER| 13 | FORMAT_MESSAGE_FROM_SYSTEM| 14 | FORMAT_MESSAGE_ARGUMENT_ARRAY, 15 | NULL, 16 | errCode, 17 | LANG_NEUTRAL, 18 | pErrMsg, 19 | 0, 20 | NULL); 21 | fprintf(stderr, "Error %s: %lu %s\n", action, errCode, pErrMsg); 22 | } 23 | 24 | // Stolen from https://github.com/getlantern/winproxy 25 | // Figure out which Dial-Up or VPN connection is active; in a normal LAN connection, this should 26 | // return NULL. NOTE: For some reason this method fails when compiled in Debug mode but works 27 | // every time in Release mode. 28 | LPTSTR findActiveConnection() { 29 | DWORD dwCb = sizeof(RASCONN); 30 | DWORD dwErr = ERROR_SUCCESS; 31 | DWORD dwRetries = 5; 32 | DWORD dwConnections = 0; 33 | RASCONN* lpRasConn = NULL; 34 | RASCONNSTATUS rasconnstatus; 35 | rasconnstatus.dwSize = sizeof(RASCONNSTATUS); 36 | 37 | // 38 | // Loop through in case the information from RAS changes between calls. 39 | // 40 | while (dwRetries--) { 41 | // If the memory is allocated, free it. 42 | if (NULL != lpRasConn) { 43 | HeapFree(GetProcessHeap(), 0, lpRasConn); 44 | lpRasConn = NULL; 45 | } 46 | 47 | // Allocate the size needed for the RAS structure. 48 | lpRasConn = (RASCONN*)HeapAlloc(GetProcessHeap(), 0, dwCb); 49 | if (NULL == lpRasConn) { 50 | dwErr = ERROR_NOT_ENOUGH_MEMORY; 51 | break; 52 | } 53 | 54 | // Set the structure size for version checking purposes. 55 | lpRasConn->dwSize = sizeof(RASCONN); 56 | 57 | // Call the RAS API then exit the loop if we are successful or an unknown 58 | // error occurs. 59 | dwErr = RasEnumConnections(lpRasConn, &dwCb, &dwConnections); 60 | if (ERROR_INSUFFICIENT_BUFFER != dwErr) { 61 | break; 62 | } 63 | } 64 | // 65 | // In the success case, print the names of the connections. 66 | // 67 | if (ERROR_SUCCESS == dwErr) { 68 | DWORD i; 69 | for (i = 0; i < dwConnections; i++) { 70 | RasGetConnectStatus(lpRasConn[i].hrasconn, &rasconnstatus); 71 | if (rasconnstatus.rasconnstate == RASCS_Connected){ 72 | return lpRasConn[i].szEntryName; 73 | } 74 | 75 | } 76 | } 77 | return NULL; // Couldn't find an active dial-up/VPN connection; return NULL 78 | } 79 | 80 | int initialize(INTERNET_PER_CONN_OPTION_LIST* options) { 81 | DWORD dwBufferSize = sizeof(INTERNET_PER_CONN_OPTION_LIST); 82 | options->dwSize = dwBufferSize; 83 | options->pszConnection = findActiveConnection(); 84 | 85 | options->dwOptionCount = 2; 86 | options->dwOptionError = 0; 87 | options->pOptions = (INTERNET_PER_CONN_OPTION*)calloc(2, sizeof(INTERNET_PER_CONN_OPTION)); 88 | if(!options->pOptions) { 89 | return NO_MEMORY; 90 | } 91 | options->pOptions[0].dwOption = INTERNET_PER_CONN_FLAGS; 92 | options->pOptions[1].dwOption = INTERNET_PER_CONN_AUTOCONFIG_URL; 93 | return RET_NO_ERROR; 94 | } 95 | 96 | int query(INTERNET_PER_CONN_OPTION_LIST* options) { 97 | DWORD dwBufferSize = sizeof(INTERNET_PER_CONN_OPTION_LIST); 98 | if(!InternetQueryOption(NULL, INTERNET_OPTION_PER_CONNECTION_OPTION, options, &dwBufferSize)) { 99 | reportWindowsError("Querying options"); 100 | return SYSCALL_FAILED; 101 | } 102 | return RET_NO_ERROR; 103 | } 104 | 105 | int show() 106 | { 107 | INTERNET_PER_CONN_OPTION_LIST options; 108 | int ret = initialize(&options); 109 | if (ret != RET_NO_ERROR) { 110 | return ret; 111 | } 112 | ret = query(&options); 113 | if (ret != RET_NO_ERROR) { 114 | return ret; 115 | } 116 | if ((options.pOptions[0].Value.dwValue & PROXY_TYPE_AUTO_PROXY_URL) > 0) { 117 | if (options.pOptions[1].Value.pszValue != NULL) { 118 | printf("%s\n", options.pOptions[1].Value.pszValue); 119 | } 120 | } 121 | return ret; 122 | } 123 | 124 | int togglePac(bool turnOn, const char* pacUrl) 125 | { 126 | INTERNET_PER_CONN_OPTION_LIST options; 127 | int ret = initialize(&options); 128 | if (ret != RET_NO_ERROR) { 129 | return ret; 130 | } 131 | if (turnOn) { 132 | options.pOptions[0].Value.dwValue = PROXY_TYPE_AUTO_PROXY_URL; 133 | options.pOptions[1].Value.pszValue = (char*)pacUrl; 134 | } 135 | else { 136 | if (strlen(pacUrl) == 0) { 137 | goto turnOff; 138 | } 139 | ret = query(&options); 140 | if (ret != RET_NO_ERROR) { 141 | goto cleanup; 142 | } 143 | // we turn pac off only if the option is set and pac url has the provided 144 | // prefix. 145 | if ((options.pOptions[0].Value.dwValue & PROXY_TYPE_AUTO_PROXY_URL) == 0 146 | || options.pOptions[1].Value.pszValue == NULL 147 | || strncmp(pacUrl, options.pOptions[1].Value.pszValue, strlen(pacUrl)) != 0) { 148 | goto cleanup; 149 | } 150 | // fall through 151 | turnOff: 152 | options.pOptions[0].Value.dwValue = PROXY_TYPE_DIRECT; 153 | options.pOptions[1].Value.pszValue = ""; 154 | } 155 | 156 | DWORD dwBufferSize = sizeof(INTERNET_PER_CONN_OPTION_LIST); 157 | BOOL result = InternetSetOption(NULL, 158 | INTERNET_OPTION_PER_CONNECTION_OPTION, 159 | &options, 160 | dwBufferSize); 161 | if (!result) { 162 | reportWindowsError("setting options"); 163 | ret = SYSCALL_FAILED; 164 | goto cleanup; 165 | } 166 | result = InternetSetOption(NULL, INTERNET_OPTION_SETTINGS_CHANGED, NULL, 0); 167 | if (!result) { 168 | reportWindowsError("propagating changes"); 169 | ret = SYSCALL_FAILED; 170 | goto cleanup; 171 | } 172 | result = InternetSetOption(NULL, INTERNET_OPTION_REFRESH , NULL, 0); 173 | if (!result) { 174 | reportWindowsError("refreshing"); 175 | ret = SYSCALL_FAILED; 176 | goto cleanup; 177 | } 178 | 179 | cleanup: 180 | free(options.pOptions); 181 | return ret; 182 | } 183 | -------------------------------------------------------------------------------- /core/vendor/sysproxy-cmd/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *~ 3 | *.gch 4 | -------------------------------------------------------------------------------- /core/vendor/sysproxy-cmd/Makefile: -------------------------------------------------------------------------------- 1 | # This Makefile is GNU make compatible. You can get GNU Make from 2 | # http://gnuwin32.sourceforge.net/packages/make.htm 3 | 4 | CCFLAGS = -Wall -c 5 | 6 | ifeq ($(OS),Windows_NT) 7 | os = windows 8 | CCFLAGS += -D WIN32 9 | # 32 bit `make` utility over 64 bit OS 10 | ifeq ($(PROCESSOR_ARCHITEW6432),AMD64) 11 | CCFLAGS += -D AMD64 12 | BIN = binaries/windows/sysproxy_amd64 13 | else 14 | ifeq ($(PROCESSOR_ARCHITECTURE),AMD64) 15 | CCFLAGS += -D AMD64 16 | BIN = binaries/windows/sysproxy_amd64 17 | endif 18 | ifeq ($(PROCESSOR_ARCHITECTURE),x86) 19 | CCFLAGS += -D IA32 20 | BIN = binaries/windows/sysproxy_386 21 | endif 22 | endif 23 | LDFLAGS += -l rasapi32 -l wininet -Wl,--subsystem,windows 24 | else 25 | UNAME_S := $(shell uname -s) 26 | ifeq ($(UNAME_S),Linux) 27 | os = linux 28 | CCFLAGS += -D LINUX $(shell pkg-config --cflags gio-2.0) 29 | LDFLAGS += $(shell pkg-config --libs gio-2.0) 30 | UNAME_P := $(shell uname -p) 31 | ifeq ($(UNAME_P),x86_64) 32 | CCFLAGS += -D AMD64 33 | BIN = binaries/linux_amd64/sysproxy 34 | endif 35 | ifneq ($(filter %86,$(UNAME_P)),) 36 | CCFLAGS += -D IA32 37 | BIN = binaries/linux_386/sysproxy 38 | endif 39 | ifneq ($(filter arm%,$(UNAME_P)),) 40 | CCFLAGS += -D ARM 41 | BIN = binaries/linux_arm/sysproxy 42 | endif 43 | endif 44 | ifeq ($(UNAME_S),Darwin) 45 | os = darwin 46 | CCFLAGS += -D DARWIN -D AMD64 -x objective-c 47 | LDFLAGS += -framework Cocoa -framework SystemConfiguration -framework Security 48 | BIN = binaries/darwin/sysproxy 49 | endif 50 | endif 51 | 52 | CC=gcc 53 | 54 | all: $(BIN) 55 | main.o: main.c common.h 56 | $(CC) $(CCFLAGS) $^ 57 | $(os).o: $(os).c common.h 58 | $(CC) $(CCFLAGS) $^ 59 | $(BIN): $(os).o main.o 60 | $(CC) -o $@ $^ $(LDFLAGS) 61 | 62 | clean: 63 | rm *.o 64 | -------------------------------------------------------------------------------- /core/vendor/sysproxy-cmd/README.md: -------------------------------------------------------------------------------- 1 | # sysproxy-cmd 2 | 3 | A command line tool to change HTTP(s) proxy settings of the operating system. 4 | 5 | Binaries included in repo. Simply `make` to build it again. 6 | 7 | Note - you will need to run make separately on each platform. 8 | 9 | # Usage 10 | 11 | ```sh 12 | sysproxy show 13 | sysproxy on 14 | sysproxy off 15 | sysproxy wait-and-cleanup 16 | ``` 17 | 18 | `sysproxy off` and `sysproxy wait-and-cleanup` turns off proxy setting only if the 19 | existing host and port equal . 20 | 21 | `sysproxy wait-and-cleanup` differs from `sysproxy off` in that it waits for input 22 | from stdin (or close) before turning off proxy setting. Any signal or Windows 23 | system shutdown message triggers the cleanup too. 24 | 25 | 26 | # Notes 27 | 28 | * **Mac** 29 | 30 | Setting the system proxy is a privileged action on Mac OS. `sudo` or elevate it 31 | as below. 32 | 33 | There's an additional option to chown itself to root:wheel and add setuid bit. 34 | 35 | ```sh 36 | sysproxy setuid 37 | ``` 38 | 39 | * **Windows** 40 | 41 | Install [MinGW-W64](http://sourceforge.net/projects/mingw-w64) to build sysproxy 42 | as it has up to date SDK headers we require. The make command is `mingw32-make`. 43 | 44 | To avoid bringing up console window, it doesn't show anything directly to 45 | console. Piping the result to other utilities should work. 46 | 47 | ``` 48 | sysproxy show | cat 49 | ``` 50 | 51 | * **Linux** 52 | 53 | `sudo apt-get install libgtk2.0-dev` 54 | -------------------------------------------------------------------------------- /core/vendor/sysproxy-cmd/binaries/darwin/sysproxy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/vendor/sysproxy-cmd/binaries/darwin/sysproxy -------------------------------------------------------------------------------- /core/vendor/sysproxy-cmd/binaries/linux_386/sysproxy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/vendor/sysproxy-cmd/binaries/linux_386/sysproxy -------------------------------------------------------------------------------- /core/vendor/sysproxy-cmd/binaries/linux_amd64/sysproxy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/vendor/sysproxy-cmd/binaries/linux_amd64/sysproxy -------------------------------------------------------------------------------- /core/vendor/sysproxy-cmd/binaries/windows/sysproxy_386.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/vendor/sysproxy-cmd/binaries/windows/sysproxy_386.exe -------------------------------------------------------------------------------- /core/vendor/sysproxy-cmd/binaries/windows/sysproxy_amd64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/core/vendor/sysproxy-cmd/binaries/windows/sysproxy_amd64.exe -------------------------------------------------------------------------------- /core/vendor/sysproxy-cmd/common.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #ifdef DARWIN 5 | int setUid(); 6 | int elevate(char *path, char *prompt, char *iconPath); 7 | #endif 8 | 9 | const char* proxyHost; 10 | const char* proxyPort; 11 | 12 | #ifdef _WIN32 13 | void setupSystemShutdownHandler(); 14 | #endif 15 | 16 | int show(); 17 | int toggleProxy(bool turnOn); 18 | 19 | enum RET_ERRORS { 20 | RET_NO_ERROR = 0, 21 | INVALID_FORMAT = 1, 22 | NO_PERMISSION = 2, 23 | SYSCALL_FAILED = 3, 24 | NO_MEMORY = 4 25 | }; 26 | -------------------------------------------------------------------------------- /core/vendor/sysproxy-cmd/linux.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "common.h" 6 | 7 | void init() { 8 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" 9 | // deprecated since version 2.36, must leave here or prior glib will crash 10 | g_type_init(); 11 | #pragma GCC diagnostic warning "-Wdeprecated-declarations" 12 | } 13 | 14 | int show() 15 | { 16 | init(); 17 | GSettings* setting = g_settings_new("org.gnome.system.proxy"); 18 | GSettings* httpSetting = g_settings_new("org.gnome.system.proxy.http"); 19 | char* oldMode = g_settings_get_string(setting, "mode"); 20 | gboolean oldEnabled = g_settings_get_boolean(httpSetting, "enabled"); 21 | char* oldHost = g_settings_get_string(httpSetting, "host"); 22 | gint oldPort = g_settings_get_int(httpSetting, "port"); 23 | if (oldEnabled && strcmp(oldMode, "manual") == 0) { 24 | printf("%s:%d\n", oldHost, oldPort); 25 | } 26 | return RET_NO_ERROR; 27 | } 28 | 29 | int toggleProxy(bool turnOn) 30 | { 31 | long port = strtol(proxyPort, NULL, 10); 32 | if (port == 0) { 33 | fprintf(stderr, "unable to parse port '%s'\n", proxyPort); 34 | return INVALID_FORMAT; 35 | } 36 | 37 | int ret = RET_NO_ERROR; 38 | init(); 39 | GSettings* setting = g_settings_new("org.gnome.system.proxy"); 40 | GSettings* httpSetting = g_settings_new("org.gnome.system.proxy.http"); 41 | GSettings* httpsSetting = g_settings_new("org.gnome.system.proxy.https"); 42 | if (turnOn == true) { 43 | gboolean success = g_settings_set_string(httpSetting, "host", proxyHost); 44 | if (!success) { 45 | fprintf(stderr, "error setting http host to %s\n", proxyHost); 46 | ret = SYSCALL_FAILED; 47 | goto cleanup; 48 | } 49 | success = g_settings_set_int(httpSetting, "port", port); 50 | if (!success) { 51 | fprintf(stderr, "error setting http port %s\n", proxyPort); 52 | ret = SYSCALL_FAILED; 53 | goto cleanup; 54 | } 55 | success = g_settings_set_string(httpsSetting, "host", proxyHost); 56 | if (!success) { 57 | fprintf(stderr, "error setting https host to %s\n", proxyHost); 58 | ret = SYSCALL_FAILED; 59 | goto cleanup; 60 | } 61 | success = g_settings_set_int(httpsSetting, "port", port); 62 | if (!success) { 63 | fprintf(stderr, "error setting https port %s\n", proxyPort); 64 | ret = SYSCALL_FAILED; 65 | goto cleanup; 66 | } 67 | success = g_settings_set_boolean(httpSetting, "enabled", TRUE); 68 | if (!success) { 69 | fprintf(stderr, "error enabling http %s\n", proxyPort); 70 | ret = SYSCALL_FAILED; 71 | goto cleanup; 72 | } 73 | success = g_settings_set_string(setting, "mode", "manual"); 74 | if (!success) { 75 | fprintf(stderr, "error setting mode to manual\n"); 76 | ret = SYSCALL_FAILED; 77 | goto cleanup; 78 | } 79 | } 80 | else { 81 | if (strlen(proxyHost) != 0) { 82 | // clear proxy setting only if it's equal to the original setting 83 | char* oldMode = g_settings_get_string(setting, "mode"); 84 | char* oldHTTPHost = g_settings_get_string(httpSetting, "host"); 85 | long oldHTTPPort = g_settings_get_int(httpSetting, "port"); 86 | char* oldHTTPSHost = g_settings_get_string(httpsSetting, "host"); 87 | long oldHTTPSPort = g_settings_get_int(httpsSetting, "port"); 88 | if (strcmp(oldMode, "manual") != 0 || 89 | strcmp(oldHTTPHost, proxyHost) != 0 || 90 | oldHTTPPort != port || 91 | strcmp(oldHTTPSHost, proxyHost) != 0 || 92 | oldHTTPSPort != port) { 93 | fprintf(stderr, "current http or https setting is not %s:%s, skipping\n", proxyHost, proxyPort); 94 | goto cleanup; 95 | } 96 | } 97 | g_settings_reset(httpSetting, "host"); 98 | g_settings_reset(httpSetting, "port"); 99 | g_settings_reset(httpsSetting, "host"); 100 | g_settings_reset(httpsSetting, "port"); 101 | g_settings_reset(httpSetting, "enabled"); 102 | g_settings_reset(setting, "mode"); 103 | } 104 | 105 | cleanup: 106 | g_settings_sync(); 107 | g_object_unref(setting); 108 | 109 | return ret; 110 | } 111 | -------------------------------------------------------------------------------- /core/vendor/sysproxy-cmd/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "common.h" 6 | 7 | void usage(const char* binName) 8 | { 9 | printf("Usage: %s [show | on | off | wait-and-cleanup ]\n", binName); 10 | exit(INVALID_FORMAT); 11 | } 12 | 13 | void turnOffProxyOnSignal(int signal) 14 | { 15 | toggleProxy(false); 16 | exit(0); 17 | } 18 | 19 | void setupSignals() 20 | { 21 | // Register signal handlers to make sure we turn proxy off no matter what 22 | signal(SIGABRT, turnOffProxyOnSignal); 23 | signal(SIGFPE, turnOffProxyOnSignal); 24 | signal(SIGILL, turnOffProxyOnSignal); 25 | signal(SIGINT, turnOffProxyOnSignal); 26 | signal(SIGSEGV, turnOffProxyOnSignal); 27 | signal(SIGTERM, turnOffProxyOnSignal); 28 | signal(SIGSEGV, turnOffProxyOnSignal); 29 | } 30 | 31 | int main(int argc, char* argv[]) { 32 | if (argc < 2) { 33 | usage(argv[0]); 34 | } 35 | 36 | #ifdef DARWIN 37 | if (strcmp(argv[1], "setuid") == 0) { 38 | return setUid(); 39 | } 40 | #endif 41 | 42 | if (strcmp(argv[1], "show") == 0) { 43 | return show(); 44 | } else { 45 | if (argc < 4) { 46 | usage(argv[0]); 47 | } 48 | proxyHost = argv[2]; 49 | proxyPort = argv[3]; 50 | if (strcmp(argv[1], "on") == 0) { 51 | return toggleProxy(true); 52 | } else if (strcmp(argv[1], "off") == 0) { 53 | return toggleProxy(false); 54 | } else if (strcmp(argv[1], "wait-and-cleanup") == 0) { 55 | setupSignals(); 56 | #ifdef _WIN32 57 | setupSystemShutdownHandler(); 58 | #endif 59 | // wait for input from stdin (or close), then toggle off 60 | getchar(); 61 | return toggleProxy(false); 62 | } else { 63 | usage(argv[0]); 64 | } 65 | } 66 | // code never reaches here, just avoids compiler from complaining. 67 | return RET_NO_ERROR; 68 | } 69 | -------------------------------------------------------------------------------- /screenshot-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/screenshot-0.png -------------------------------------------------------------------------------- /screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/screenshot-1.png -------------------------------------------------------------------------------- /ui/.env.development: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | HOST=localhost 3 | PORT=3030 4 | CI=false 5 | -------------------------------------------------------------------------------- /ui/.env.production: -------------------------------------------------------------------------------- 1 | GENERATE_SOURCEMAP=false 2 | GOOGLE_ANALYTICS_TRACKING_ID=UA-72182315-3 3 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | *.css 24 | -------------------------------------------------------------------------------- /ui/config-overrides.js: -------------------------------------------------------------------------------- 1 | const rewireReactHotLoader = require('react-app-rewire-hot-loader'); 2 | const rewireMobX = require('react-app-rewire-mobx'); 3 | 4 | module.exports = function override(config, env) { 5 | config = rewireReactHotLoader(config, env); 6 | config = rewireMobX(config, env); 7 | return config; 8 | }; 9 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blinksocks-gui", 3 | "version": "0.1.0", 4 | "description": "A web based GUI wrapper for blinksocks", 5 | "author": "Micooz", 6 | "homepage": "/", 7 | "scripts": { 8 | "analyze": "source-map-explorer build/static/js/main.*", 9 | "build-css": "node-sass-chokidar src/ -o src/", 10 | "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive", 11 | "start-js": "react-app-rewired start", 12 | "start": "npm-run-all -p watch-css start-js", 13 | "build-js": "react-app-rewired build", 14 | "build": "npm-run-all build-css build-js", 15 | "postbuild": "node scripts/post-build.js", 16 | "test": "react-app-rewired test --env=jsdom", 17 | "eject": "react-app-rewired eject" 18 | }, 19 | "dependencies": { 20 | "@blueprintjs/core": "^2.3.1", 21 | "classnames": "^2.2.5", 22 | "echarts": "^4.1.0", 23 | "filesize": "^3.6.1", 24 | "i18next": "^11.3.2", 25 | "i18next-browser-languagedetector": "^2.2.0", 26 | "i18next-xhr-backend": "^1.5.1", 27 | "jssha": "^2.3.1", 28 | "lodash": "^4.17.10", 29 | "mobx": "^4.3.0", 30 | "mobx-react": "^5.1.2", 31 | "normalize.css": "^8.0.0", 32 | "nprogress": "^0.2.0", 33 | "prop-types": "^15.6.1", 34 | "qs": "^6.5.2", 35 | "react": "^16.4.0", 36 | "react-beautiful-dnd": "^7.1.3", 37 | "react-dom": "^16.4.0", 38 | "react-ga": "^2.5.2", 39 | "react-google-maps": "^9.4.5", 40 | "react-helmet": "^5.2.0", 41 | "react-i18next": "^7.6.1", 42 | "react-router-dom": "^4.2.2", 43 | "react-transition-group": "^2.3.1", 44 | "recompose": "^0.27.1", 45 | "socket.io-client": "^2.1.1", 46 | "url-parse": "^1.4.0" 47 | }, 48 | "devDependencies": { 49 | "fs-extra": "^6.0.1", 50 | "node-sass-chokidar": "^1.3.0", 51 | "npm-run-all": "^4.1.3", 52 | "react-app-rewire-hot-loader": "^1.0.1", 53 | "react-app-rewire-mobx": "^1.0.8", 54 | "react-app-rewired": "^1.5.2", 55 | "react-hot-loader": "^4.2.0", 56 | "react-scripts": "^2.0.0-next.b2fd8db8", 57 | "source-map-explorer": "^1.5.0" 58 | }, 59 | "browserslist": { 60 | "development": [ 61 | "last 2 chrome versions", 62 | "last 2 firefox versions", 63 | "last 2 edge versions" 64 | ], 65 | "production": [ 66 | ">1%", 67 | "last 4 versions", 68 | "Firefox ESR", 69 | "not ie < 11" 70 | ] 71 | }, 72 | "proxy": { 73 | "/verify": { 74 | "target": "http://localhost:3000" 75 | }, 76 | "/logs": { 77 | "target": "http://localhost:3000" 78 | }, 79 | "/socket.io": { 80 | "target": "http://localhost:3000", 81 | "ws": true 82 | } 83 | }, 84 | "license": "Apache-2.0" 85 | } 86 | -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/fonts/TitilliumWeb-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/ui/public/fonts/TitilliumWeb-Light.ttf -------------------------------------------------------------------------------- /ui/public/images/landing.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/ui/public/images/landing.jpeg -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | blinksocks-gui 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /ui/public/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /ui/public/locales/zh-CN/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /ui/public/locales/zh/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "blinksocks gui", 3 | "name": "friendly gui management tools for blinksocks", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /ui/scripts/post-build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const buildPath = path.resolve(__dirname, '../build'); 5 | const targetPath = path.resolve(__dirname, '../../core/public'); 6 | 7 | (async function main() { 8 | try { 9 | await fs.remove(targetPath); 10 | await fs.copy(buildPath, targetPath); 11 | } catch (err) { 12 | console.error(err); 13 | } 14 | })(); 15 | -------------------------------------------------------------------------------- /ui/src/components/Github/Github.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './Github.module.css'; 4 | 5 | export default class Github extends React.Component { 6 | 7 | static propTypes = { 8 | url: PropTypes.string.isRequired, 9 | }; 10 | 11 | render() { 12 | const style = { 13 | fill: '#151513', 14 | color: '#fff', 15 | position: 'absolute', 16 | top: 0, 17 | border: 0, 18 | right: 0 19 | }; 20 | const { url } = this.props; 21 | return ( 22 | 23 | 24 | 25 | 28 | 31 | 32 | 33 | ); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /ui/src/components/Github/Github.module.scss: -------------------------------------------------------------------------------- 1 | .github-corner:hover .octo-arm { 2 | animation: octocat-wave 560ms ease-in-out 3 | } 4 | 5 | @keyframes octocat-wave { 6 | 0%, 100% { 7 | transform: rotate(0deg) 8 | } 9 | 20%, 60% { 10 | transform: rotate(-25deg) 11 | } 12 | 40%, 80% { 13 | transform: rotate(10deg) 14 | } 15 | } 16 | 17 | @media (max-width: 500px) { 18 | .github-corner:hover .octo-arm { 19 | animation: none 20 | } 21 | .github-corner .octo-arm { 22 | animation: octocat-wave 560ms ease-in-out 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui/src/components/MenuRouter/MenuRouter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Link } from 'react-router-dom'; 3 | import classnames from 'classnames'; 4 | import styles from './MenuRouter.module.css'; 5 | 6 | export const MenuRouter = ({ routes, children, style }) => ( 7 |
    8 | {routes.map(({ path, disabled, text, exact }, i) => text && ( 9 | 10 | {({ match }) => (match || disabled) ? ( 11 | {text} 12 | ) : ( 13 | 14 | {text} 15 | 16 | )} 17 | 18 | ))} 19 | {children} 20 |
21 | ); 22 | 23 | export const MenuRouterItem = ({ active, disabled, children }) => ( 24 |
  • 25 | {children} 26 |
  • 27 | ); 28 | 29 | export const MenuRouterDivider = () => ( 30 |
  • 31 | ); 32 | -------------------------------------------------------------------------------- /ui/src/components/MenuRouter/MenuRouter.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | list-style: none; 3 | padding: 0; 4 | margin: 0 40px 0 0; 5 | 6 | a:hover { 7 | text-decoration: none; 8 | } 9 | 10 | li { 11 | margin-bottom: 10px; 12 | cursor: pointer; 13 | font-size: 16px; 14 | color: #999; 15 | 16 | &:hover { 17 | color: #676767; 18 | } 19 | 20 | &.active { 21 | color: #0069ff; 22 | } 23 | 24 | &.disabled { 25 | color: #c4c4c4; 26 | cursor: not-allowed; 27 | text-decoration: line-through; 28 | } 29 | } 30 | 31 | .divider { 32 | border-bottom: 1px solid #ececec; 33 | margin: 20px 0; 34 | cursor: default; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ui/src/components/MenuRouter/index.js: -------------------------------------------------------------------------------- 1 | export * from './MenuRouter'; -------------------------------------------------------------------------------- /ui/src/components/NoMatch/NoMatch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './NoMatch.module.css'; 3 | 4 | export default class NoMatch extends React.Component { 5 | 6 | render() { 7 | return ( 8 |
    9 |

    404

    10 |

    We couldn't find the page you
    were looking for.

    11 | 12 |
    13 | ); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/components/NoMatch/NoMatch.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | left: 0; 4 | right: 0; 5 | top: 0; 6 | bottom: 0; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | background-color: #f7f7f7; 11 | padding-top: 20%; 12 | 13 | h1 { 14 | margin-bottom: 40px; 15 | color: #989898; 16 | font-size: 6rem; 17 | } 18 | 19 | p { 20 | color: #969696; 21 | font-size: 2rem; 22 | text-align: center; 23 | } 24 | 25 | button { 26 | border: 0; 27 | padding: 10px; 28 | width: 100px; 29 | background-color: #2e2e2e; 30 | color: #fff; 31 | cursor: pointer; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/components/Title/Title.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | import { APP_NAME } from '../../constants'; 4 | 5 | export default function Title({ children = '' }) { 6 | return {children} - {APP_NAME}; 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/constants.js: -------------------------------------------------------------------------------- 1 | export const RUN_TYPE_CLIENT = 0; 2 | export const RUN_TYPE_SERVER = 1; 3 | 4 | export const APP_NAME = 'blinksocks-gui'; 5 | export const GOOGLE_MAP_API_KEY = 'AIzaSyCdm64ihbcSWFtammP9nemcH_v9CqJzWoQ'; 6 | export const TOKEN_NAME = 'BLINKSOCKS_GUI_TOKEN'; 7 | export const HASH_SALT = 'blinksocks'; 8 | 9 | // this value must be less than server dispose timeout 10 | export const KEEPALIVE_INTERVAL = 5e4; // 50s 11 | export const RPC_TIMEOUT = 3e4; // 30s 12 | 13 | export const RPC_STATUS_INIT = -1; 14 | export const RPC_STATUS_ONLINE = 0; 15 | export const RPC_STATUS_OFFLINE = 1; 16 | export const RPC_STATUS_ERROR = 2; 17 | 18 | export const SERVICE_STATUS_INIT = -1; 19 | export const SERVICE_STATUS_RUNNING = 0; 20 | export const SERVICE_STATUS_STOPPED = 1; 21 | 22 | export const CONN_STAGE_INIT = 0; 23 | export const CONN_STAGE_TRANSFER = 1; 24 | export const CONN_STAGE_FINISH = 2; 25 | export const CONN_STAGE_ERROR = 3; 26 | -------------------------------------------------------------------------------- /ui/src/containers/App/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | import { hot } from 'react-hot-loader'; 5 | import { Route, Link, Switch } from 'react-router-dom'; 6 | import { observer } from 'mobx-react'; 7 | import { Icon } from '@blueprintjs/core'; 8 | 9 | import NetworkTips from './NetworkTips/NetworkTips'; 10 | import Home from '../Home/Home'; 11 | import Services from '../Services/Services'; 12 | import AddServer from '../Services/Add/Add'; 13 | import Title from '../../components/Title/Title'; 14 | import Github from '../../components/Github/Github'; 15 | 16 | import styles from './App.module.css'; 17 | 18 | import { call, live, store } from '../../utils'; 19 | import { RUN_TYPE_CLIENT, TOKEN_NAME } from '../../constants'; 20 | 21 | @hot(module) 22 | @observer 23 | export default class App extends React.Component { 24 | 25 | static propTypes = { 26 | match: PropTypes.object.isRequired, 27 | }; 28 | 29 | async componentDidMount() { 30 | try { 31 | const [env, unlive] = await Promise.all([ 32 | call('get_env', null, { cache: true }), 33 | live('live_services', ({ services }) => { 34 | store.services = services; 35 | }), 36 | ]); 37 | store.env = env; 38 | this.unlive = unlive; 39 | } catch (err) { 40 | console.error(err.message); 41 | } 42 | } 43 | 44 | componentWillUnmount() { 45 | if (this.unlive) { 46 | this.unlive(); 47 | this.unlive = null; 48 | } 49 | } 50 | 51 | onSignOut = () => { 52 | localStorage.removeItem(TOKEN_NAME); 53 | window.location.href = '/landing'; 54 | }; 55 | 56 | render() { 57 | const { match } = this.props; 58 | const { env } = store; 59 | const FOOTER_LINKS = [{ 60 | text: 'ChangeLog', 61 | link: 'https://github.com/blinksocks/blinksocks-gui/blob/master/CHANGELOG.md', 62 | }, { 63 | text: 'Issues', 64 | link: 'https://github.com/blinksocks/blinksocks-gui/issues', 65 | }, { 66 | text: 'Document', 67 | link: 'https://github.com/blinksocks/blinksocks-gui', 68 | }, { 69 | text: `blinksocks - v${env.blinksocksVersion || '-'}`, 70 | link: 'https://github.com/blinksocks/blinksocks', 71 | }]; 72 | return ( 73 |
    74 | Dashboard 75 | 76 | 77 |

    78 | 79 |
    80 |

    blinksocks-gui

    81 | v{env.version} 82 |
    83 | 84 | {env.runType === RUN_TYPE_CLIENT ? 'client' : 'server'} 85 | 86 |

    87 | 92 |
    93 | 94 | 95 | 96 | 97 | 98 |
    99 |
    100 |
      101 | {FOOTER_LINKS.map(({ text, link }, i) => ( 102 |
    • 103 | {text} 104 | {i < FOOTER_LINKS.length - 1 && } 105 |
    • 106 | ))} 107 |
    108 |
    109 |
    110 | ); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /ui/src/containers/App/App.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | width: 100%; 4 | height: 100%; 5 | min-width: 990px; 6 | overflow-x: hidden; 7 | overflow-y: auto; 8 | 9 | .header { 10 | display: flex; 11 | align-items: center; 12 | padding: 20px 10px 10px 15px; 13 | background-color: #484848; 14 | color: #fff; 15 | height: 80px; 16 | line-height: 20px; 17 | 18 | svg { 19 | position: relative; 20 | top: -5px; 21 | margin-right: 15px; 22 | } 23 | 24 | .caption { 25 | display: flex; 26 | flex-direction: column; 27 | 28 | small { 29 | height: 20px; 30 | margin-left: 2px; 31 | color: #bdbdbd; 32 | } 33 | } 34 | 35 | .runType { 36 | position: relative; 37 | top: -20px; 38 | left: 5px; 39 | } 40 | } 41 | 42 | .signout { 43 | z-index: 2; 44 | position: absolute; 45 | right: 30px; 46 | } 47 | 48 | .body { 49 | margin: 30px 30px 30px; 50 | min-height: 800px; 51 | } 52 | 53 | footer { 54 | height: 30px; 55 | line-height: 20px; 56 | text-align: center; 57 | color: #fff; 58 | background-color: #373737; 59 | padding: 5px 0; 60 | 61 | ul { 62 | list-style: none; 63 | display: flex; 64 | justify-content: center; 65 | margin: 0; 66 | padding: 0; 67 | 68 | .divider { 69 | float: right; 70 | width: 1px; 71 | height: 15px; 72 | margin: 3px 10px 0; 73 | border-left: 1px solid #757575; 74 | } 75 | } 76 | 77 | a { 78 | color: #6db4fb; 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /ui/src/containers/App/NetworkTips/NetworkTips.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon } from '@blueprintjs/core'; 3 | import { observer } from 'mobx-react'; 4 | 5 | import { RPC_STATUS_ERROR, RPC_STATUS_INIT, RPC_STATUS_OFFLINE, RPC_STATUS_ONLINE } from '../../../constants'; 6 | import { store } from '../../../utils'; 7 | 8 | import styles from './NetworkTips.module.css'; 9 | 10 | @observer 11 | export default class NetworkTips extends React.Component { 12 | 13 | render() { 14 | const { rpcStatus: status } = store; 15 | if (status === RPC_STATUS_INIT || status === RPC_STATUS_ONLINE) { 16 | return null; 17 | } 18 | const hint = { 19 | [RPC_STATUS_OFFLINE]: 'You\'re currently offline, we are trying to reconnect...', 20 | [RPC_STATUS_ERROR]: 'We cannot connect to server, please check your connection then refresh the page.', 21 | }[status]; 22 | const bgColor = { 23 | [RPC_STATUS_OFFLINE]: '#ff7373d4', 24 | [RPC_STATUS_ERROR]: '#ff3939d4', 25 | }[status]; 26 | return ( 27 |
    28 |
    29 |  {hint} 30 |
    31 |
    32 | ); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/containers/App/NetworkTips/NetworkTips.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: fixed; 3 | z-index: 999; 4 | display: flex; 5 | justify-content: center; 6 | width: 100%; 7 | 8 | .content { 9 | height: 30px; 10 | padding: 0 10px; 11 | border-radius: 0 0 2px 2px; 12 | text-align: center; 13 | line-height: 30px; 14 | color: #fff; 15 | 16 | svg { 17 | position: relative; 18 | top: 7px; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/containers/Dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import { Link } from 'react-router-dom'; 4 | import { Icon } from '@blueprintjs/core'; 5 | 6 | import Service from './Service/Service'; 7 | import styles from './Dashboard.module.css'; 8 | 9 | import { call } from '../../utils'; 10 | 11 | export default class Dashboard extends React.Component { 12 | 13 | state = { 14 | services: [], 15 | }; 16 | 17 | componentDidMount() { 18 | this.fetchServices(); 19 | } 20 | 21 | async fetchServices() { 22 | try { 23 | const services = await call('get_services'); 24 | this.setState({ services }); 25 | } catch (err) { 26 | console.error(err.message); 27 | } 28 | } 29 | 30 | onCopyService = async (id) => { 31 | try { 32 | await call('copy_setting', { id }); 33 | await this.fetchServices(); 34 | } catch (err) { 35 | console.error(err); 36 | } 37 | }; 38 | 39 | onRenameService = async (id, remarks) => { 40 | if (remarks.length < 1) { 41 | return; 42 | } 43 | try { 44 | await call('update_remarks', { id, remarks }); 45 | await this.fetchServices(); 46 | } catch (err) { 47 | console.error(err); 48 | } 49 | }; 50 | 51 | onRemoveService = async (id) => { 52 | if (window.confirm('Are you sure to remove this service?')) { 53 | try { 54 | await call('delete_setting', { id }); 55 | await this.fetchServices(); 56 | } catch (err) { 57 | console.error(err); 58 | } 59 | } 60 | }; 61 | 62 | render() { 63 | const { services } = this.state; 64 | return ( 65 |
      66 | {services.map((service) => ( 67 | this.onCopyService(service.id)} 71 | onRename={(remarks) => this.onRenameService(service.id, remarks)} 72 | onRemove={() => this.onRemoveService(service.id)} 73 | /> 74 | ))} 75 | 76 |
    • 77 | 78 |
    • 79 | 80 |
    81 | ); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /ui/src/containers/Dashboard/Dashboard.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-wrap: wrap; 4 | list-style: none; 5 | margin: 0; 6 | padding: 0; 7 | 8 | li { 9 | width: 200px; 10 | height: 200px; 11 | margin: 0 20px 20px 0; 12 | padding: 15px; 13 | 14 | &:last-child { 15 | margin: 0; 16 | } 17 | 18 | &.center { 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | 23 | svg { 24 | fill: #eaeaea; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/containers/Dashboard/Service/Service.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | import formatSize from 'filesize'; 5 | import { Link } from 'react-router-dom'; 6 | import { observer } from 'mobx-react'; 7 | import { Dialog, Button, Icon, Switch, Popover, Menu, MenuItem, MenuDivider, Position } from '@blueprintjs/core'; 8 | 9 | import styles from './Service.module.css'; 10 | import { call, toast, store } from '../../../utils'; 11 | 12 | @observer 13 | export default class Service extends React.Component { 14 | 15 | static propTypes = { 16 | service: PropTypes.object.isRequired, 17 | onCopy: PropTypes.func, 18 | onRename: PropTypes.func, 19 | onRemove: PropTypes.func, 20 | }; 21 | 22 | static defaultProps = { 23 | onCopy: () => { 24 | }, 25 | onRename: () => { 26 | }, 27 | onRemove: () => { 28 | }, 29 | }; 30 | 31 | state = { 32 | pending: false, 33 | currentRemarks: '', 34 | isRenameDialogOpen: false, 35 | isRenaming: false, 36 | }; 37 | 38 | onToggle = async (e) => { 39 | e.stopPropagation(); 40 | const { id } = this.props.service; 41 | this.setState({ pending: true }); 42 | try { 43 | if (store.isServiceRunning(id)) { 44 | store.services[id] = await call('stop_service', { id }); 45 | toast('service stopped!', 'success'); 46 | } else { 47 | store.services[id] = await call('start_service', { id }); 48 | toast('service started successfully!', 'success'); 49 | } 50 | } catch (err) { 51 | console.error(err.message); 52 | } 53 | this.setState({ pending: false }); 54 | }; 55 | 56 | onOpenRenameDialog = () => { 57 | const { service } = this.props; 58 | this.setState({ isRenameDialogOpen: true, currentRemarks: service.remarks }); 59 | }; 60 | 61 | onEditRemarks = async () => { 62 | const { currentRemarks: remarks } = this.state; 63 | if (remarks.length < 1) { 64 | return; 65 | } 66 | this.setState({ isRenaming: true }); 67 | await this.props.onRename(remarks); 68 | this.setState({ isRenaming: false, isRenameDialogOpen: false }); 69 | }; 70 | 71 | renderMenu = () => { 72 | const { service: { id }, onRemove, onCopy } = this.props; 73 | const styles = { textDecoration: 'none', color: 'inherit' }; 74 | return ( 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 92 | 93 | ); 94 | }; 95 | 96 | renderSummary = () => { 97 | const { id } = this.props.service; 98 | const service = store.services[id]; 99 | const getValue = (key) => { 100 | if (!service || typeof service[key] === 'undefined') { 101 | return '-'; 102 | } 103 | if (key.indexOf('speed') > 0) { 104 | const [num, unit] = formatSize(service[key], { output: 'array' }); 105 | return ( 106 |
    {num} {unit}/s
    107 | ); 108 | } 109 | return service[key]; 110 | }; 111 | return ( 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |
    Connections:{getValue('connections')}
    Upload:{getValue('upload_speed')}
    Download:{getValue('download_speed')}
    128 | ); 129 | }; 130 | 131 | renderRenameDialog = () => { 132 | const { isRenameDialogOpen, isRenaming, currentRemarks } = this.state; 133 | return ( 134 | this.setState({ isRenameDialogOpen: false })} 138 | > 139 |
    140 | 155 |
    156 |
    157 |
    158 |
    165 |
    166 |
    167 | ); 168 | }; 169 | 170 | render() { 171 | const { id, protocol, address, remarks } = this.props.service; 172 | const { pending } = this.state; 173 | return ( 174 |
  • 175 |
    176 | 177 | 184 | 185 | 186 | 187 | 188 |
    189 | 190 | {remarks || '-'} 191 | {protocol}://{address} 192 | {this.renderSummary()} 193 | 194 | {this.renderRenameDialog()} 195 |
  • 196 | ); 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /ui/src/containers/Dashboard/Service/Service.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | .header { 4 | display: flex; 5 | justify-content: space-between; 6 | 7 | .switch { 8 | margin: 0 -5px 0 0; 9 | } 10 | } 11 | 12 | .menu { 13 | text-decoration: none; 14 | color: #000; 15 | } 16 | 17 | .body { 18 | display: inline-block; 19 | width: 100%; 20 | text-decoration: none; 21 | color: #000; 22 | } 23 | 24 | .remarks { 25 | display: block; 26 | margin-top: 10px; 27 | font-size: 2rem; 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | } 31 | 32 | .address { 33 | color: #a0a0a0; 34 | padding-left: 2px; 35 | font-size: .85rem; 36 | } 37 | 38 | .summary { 39 | width: 100%; 40 | margin-top: 20px; 41 | font-size: .85rem; 42 | color: #182026; 43 | 44 | tr td { 45 | padding: 0; 46 | white-space: nowrap; 47 | 48 | span { 49 | display: inline-block; 50 | max-width: 50px; 51 | overflow: hidden; 52 | text-overflow: ellipsis; 53 | line-height: 10px; 54 | } 55 | 56 | &:last-child { 57 | text-align: right; 58 | } 59 | } 60 | 61 | svg { 62 | position: relative; 63 | top: 1px; 64 | margin-right: 3px; 65 | fill: #9e9e9e; 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /ui/src/containers/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Route } from 'react-router-dom'; 4 | import { Icon } from '@blueprintjs/core'; 5 | 6 | import Usage from './Usage/Usage'; 7 | import Dashboard from '../Dashboard/Dashboard'; 8 | import Plugins from '../Plugins/Plugins'; 9 | import Info from '../Info/Info'; 10 | import Settings from '../Settings/Settings'; 11 | 12 | import { MenuRouter, MenuRouterDivider } from '../../components/MenuRouter'; 13 | import { store } from '../../utils'; 14 | 15 | import styles from './Home.module.css'; 16 | 17 | function createRoutes(match) { 18 | return [{ 19 | exact: true, 20 | path: match.url, 21 | text: 'Dashboard', 22 | component: Dashboard, 23 | }, { 24 | path: match.url + 'plugins', 25 | // disabled: true, 26 | text: 'Plugins', 27 | component: Plugins, 28 | }, { 29 | path: match.url + 'info', 30 | text: 'System Info', 31 | component: Info, 32 | }, { 33 | path: match.url + 'settings', 34 | text: 'Settings', 35 | component: Settings, 36 | }]; 37 | } 38 | 39 | export default class Home extends React.Component { 40 | 41 | static propTypes = { 42 | match: PropTypes.object.isRequired, 43 | }; 44 | 45 | render() { 46 | const { match } = this.props; 47 | const routes = createRoutes(match); 48 | return ( 49 |
    50 | 51 | 52 | 53 | 54 |   55 | Latency: {store.rpcLatency}ms 56 | 57 | 58 | {routes.map(({ path, exact, component }, i) => ( 59 | 60 | ))} 61 |
    62 | ); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /ui/src/containers/Home/Home.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/containers/Home/Usage/Usage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import formatSize from 'filesize'; 4 | 5 | import styles from './Usage.module.css'; 6 | import { store, live } from '../../../utils'; 7 | 8 | export default class Usage extends React.Component { 9 | 10 | state = { 11 | cpuUsage: null, 12 | memoryUsage: null, 13 | }; 14 | 15 | unlive = null; 16 | 17 | async componentDidMount() { 18 | try { 19 | this.unlive = await live('live_usage', ({ cpuUsage, memoryUsage }) => { 20 | this.setState({ cpuUsage, memoryUsage }); 21 | }); 22 | } catch (err) { 23 | console.error(err.message); 24 | } 25 | } 26 | 27 | componentWillUnmount() { 28 | if (this.unlive) { 29 | this.unlive(); 30 | this.unlive = null; 31 | } 32 | } 33 | 34 | render() { 35 | const { env } = store; 36 | if (!env.os) { 37 | return null; 38 | } 39 | const { cpuUsage, memoryUsage } = this.state; 40 | const totalMemory = env.os.filter(([key]) => key === 'memory')[0][1]; 41 | const cpuPercentage = (cpuUsage > 1 ? 1 : cpuUsage); 42 | const memPercentage = (memoryUsage / totalMemory); 43 | const colorClass = (value) => classnames({ 44 | 'pt-intent-warning': value > 0.5, 45 | 'pt-intent-danger': value > 0.8, 46 | }); 47 | return ( 48 |
    49 |
    50 |

    51 | CPU 52 | {(cpuPercentage * 100).toFixed(2) + '%'} 53 |

    54 | {cpuUsage !== null && ( 55 |
    56 |
    57 |
    58 | )} 59 |
    60 |
    61 |

    62 | Memory 63 | {formatSize(memoryUsage)} of {formatSize(totalMemory)} 64 |

    65 | {memoryUsage !== null && ( 66 |
    67 |
    68 |
    69 | )} 70 |
    71 |
    72 | ); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /ui/src/containers/Home/Usage/Usage.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 20px 0; 3 | 4 | .item { 5 | margin-bottom: 10px; 6 | 7 | h4 { 8 | margin-bottom: 5px; 9 | 10 | small { 11 | margin-left: 5px; 12 | color: #6f6f6f; 13 | } 14 | } 15 | 16 | &:last-child { 17 | margin-bottom: 0; 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/containers/Info/Info.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { store } from '../../utils'; 4 | import Title from '../../components/Title/Title'; 5 | 6 | import styles from './Info.module.css'; 7 | 8 | const Table = ({ pairs }) => ( 9 | 10 | 11 | {pairs.map(([key, value]) => ( 12 | 13 | 14 | 15 | 16 | ))} 17 | 18 |
    {key}:{value}
    19 | ); 20 | 21 | export default class Info extends React.Component { 22 | 23 | render() { 24 | const { env } = store; 25 | if (!env.os || !env.node) { 26 | return null; 27 | } 28 | return ( 29 |
    30 | System Info 31 |
    32 |

    OS

    33 | {env.os && } 34 | 35 |
    36 |

    Node.js

    37 | {env.node &&
    } 38 | 39 | 40 | ); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /ui/src/containers/Info/Info.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | 4 | section { 5 | display: block; 6 | margin-bottom: 30px; 7 | 8 | h3 { 9 | padding: 0 10px; 10 | margin-bottom: 10px; 11 | border-left: 3px solid #4390ff; 12 | white-space: nowrap; 13 | overflow: hidden; 14 | text-overflow: ellipsis; 15 | } 16 | 17 | table { 18 | display: block; 19 | width: 100%; 20 | padding-top: 5px; 21 | border-top: 1px solid #f1f1f1; 22 | overflow: auto; 23 | 24 | tbody tr { 25 | padding-bottom: 5px; 26 | font-size: 16px; 27 | 28 | td { 29 | vertical-align: top; 30 | 31 | b { 32 | display: inline-block; 33 | width: 150px; 34 | } 35 | } 36 | } 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /ui/src/containers/Landing/Landing.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import qs from 'querystring'; 4 | import { Icon } from '@blueprintjs/core'; 5 | import { TOKEN_NAME, HASH_SALT } from '../../constants'; 6 | import Github from '../../components/Github/Github'; 7 | import { hash } from '../../utils'; 8 | import styles from './Landing.module.css'; 9 | 10 | export default class Landing extends React.Component { 11 | 12 | state = { 13 | password: '', 14 | fail: false, 15 | }; 16 | 17 | $input = null; 18 | 19 | componentDidMount() { 20 | const { password } = qs.parse(window.location.search.substr(1)); 21 | if (password) { 22 | this.setState({ password }, this.onSignIn); 23 | } 24 | } 25 | 26 | onPasswordChange = (e) => { 27 | this.setState({ password: e.target.value, fail: false }); 28 | }; 29 | 30 | onKeyPress = (e) => { 31 | if (e.key === 'Enter') { 32 | this.onSignIn(); 33 | } 34 | }; 35 | 36 | onSignIn = async () => { 37 | const { password } = this.state; 38 | if (password !== '') { 39 | try { 40 | const token = hash('SHA-256', password + HASH_SALT); 41 | const response = await window.fetch('/verify', { 42 | method: 'POST', 43 | body: JSON.stringify({ token }), 44 | headers: new Headers({ 45 | 'content-type': 'application/json', 46 | }), 47 | }); 48 | if (response.status === 200) { 49 | localStorage.setItem(TOKEN_NAME, token); 50 | window.location.href = '/'; 51 | } else { 52 | this.setState({ fail: true }); 53 | this.$input.select(); 54 | } 55 | } catch (err) { 56 | console.error(err); 57 | } 58 | } else { 59 | this.$input.focus(); 60 | } 61 | }; 62 | 63 | render() { 64 | const { password, fail } = this.state; 65 | return ( 66 |
    67 | 68 |
    69 |
    70 |

    blinksocks gui

    71 |
    72 | 73 | this.$input = dom} 79 | onKeyPress={this.onKeyPress} 80 | onChange={this.onPasswordChange} 81 | value={password} 82 | /> 83 | 84 |
    85 | password is invalid 86 |
    87 |
    88 | ); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /ui/src/containers/Landing/Landing.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | left: 0; 4 | right: 0; 5 | top: 0; 6 | bottom: 0; 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | overflow: hidden; 12 | 13 | .background { 14 | position: absolute; 15 | left: -20px; 16 | right: -20px; 17 | top: -20px; 18 | bottom: -20px; 19 | filter: blur(15px); 20 | background: url("/images/landing.jpeg") no-repeat; 21 | background-size: 100% 100%; 22 | z-index: -1; 23 | } 24 | 25 | .content { 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: center; 29 | align-items: center; 30 | margin-top: -20%; 31 | 32 | h3 { 33 | color: #fff; 34 | font-size: 2.5rem; 35 | margin-bottom: 40px; 36 | text-shadow: #484848 2px 3px 6px; 37 | } 38 | 39 | .input { 40 | display: flex; 41 | align-items: center; 42 | 43 | svg { 44 | position: relative; 45 | right: -35px; 46 | fill: #607d8b; 47 | } 48 | 49 | input { 50 | outline: none; 51 | width: 350px; 52 | height: 40px; 53 | padding: 10px 10px 10px 40px; 54 | margin-right: 10px; 55 | border: 1px solid transparent; 56 | border-radius: 3px; 57 | background-color: #dcdcdc; 58 | color: #484848; 59 | transition: background-color .1s linear; 60 | 61 | &:focus { 62 | background-color: #f4f4f4; 63 | } 64 | 65 | &.fail { 66 | border: 1px solid #f44336; 67 | } 68 | } 69 | 70 | button { 71 | width: 60px; 72 | height: 40px; 73 | border: 0; 74 | border-radius: 3px; 75 | background-color: coral; 76 | color: #fff; 77 | cursor: pointer; 78 | transition: background-color .1s linear; 79 | 80 | &:hover { 81 | background-color: #ff8d5f; 82 | } 83 | } 84 | } 85 | 86 | small { 87 | color: #f44336; 88 | width: 100%; 89 | padding: 5px 0 0 30px; 90 | font-size: 0.8rem; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ui/src/containers/Plugins/Plugins.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import styles from './Plugins.module.css'; 5 | 6 | import { store } from '../../utils'; 7 | import Title from '../../components/Title/Title'; 8 | 9 | export default class Plugins extends React.Component { 10 | 11 | static propTypes = { 12 | match: PropTypes.object.isRequired, 13 | }; 14 | 15 | render() { 16 | const { env } = store; 17 | if (typeof env.runType === 'undefined' || !env.os) { 18 | return null; 19 | } 20 | return ( 21 |
    22 | Plugins 23 |
      24 |
    • 25 |
    • Plugins
    • 26 |
    27 |
    28 |

    29 | No plugins available yet. 30 |

    31 |
    32 | ); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/containers/Plugins/Plugins.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /ui/src/containers/Plugins/SystemProxy/SystemProxy.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Button, RadioGroup, Radio, Intent } from '@blueprintjs/core'; 4 | import styles from './SystemProxy.module.css'; 5 | 6 | import { call } from '../../../utils'; 7 | 8 | const PAC_TYPE_LOCAL = 0; 9 | const PAC_TYPE_REMOTE = 1; 10 | 11 | export default class SystemProxy extends React.Component { 12 | 13 | static propTypes = { 14 | platform: PropTypes.string.isRequired, 15 | }; 16 | 17 | state = { 18 | pacType: PAC_TYPE_LOCAL, 19 | sysProxy: '', 20 | sysPac: '', 21 | }; 22 | 23 | async componentDidMount() { 24 | try { 25 | const [sysProxy, sysPac] = await Promise.all([ 26 | call('get_system_proxy', null, { timeout: 6e4 }), 27 | // call('get_system_pac'), 28 | ]); 29 | this.setState({ sysProxy, sysPac }); 30 | } catch (err) { 31 | console.error(err); 32 | } 33 | } 34 | 35 | onSelectPacType = (e) => { 36 | this.setState({ pacType: +e.currentTarget.value }); 37 | }; 38 | 39 | onSystemProxyChange = (e) => { 40 | this.setState({ sysProxy: e.target.value }); 41 | }; 42 | 43 | onSysPacChange = (e) => { 44 | this.setState({ sysPac: e.target.value }); 45 | }; 46 | 47 | onSetSystemProxy = async () => { 48 | const { sysProxy } = this.state; 49 | if (!sysProxy) { 50 | return; 51 | } 52 | try { 53 | await call('set_system_proxy', { sysProxy }); 54 | } catch (err) { 55 | console.error(err); 56 | } 57 | }; 58 | 59 | onSetSystemPac = async () => { 60 | const { sysPac } = this.state; 61 | if (!sysPac) { 62 | return; 63 | } 64 | try { 65 | await call('set_system_pac', { sysPac }); 66 | } catch (err) { 67 | console.error(err); 68 | } 69 | }; 70 | 71 | render() { 72 | const { platform } = this.props; 73 | const { pacType, sysProxy, pac } = this.state; 74 | return ( 75 |
    76 |

    System Proxy

    77 | {platform === 'darwin' && ( 78 |

    79 | This plugin requires root privileges on macOS. 80 |

    81 | )} 82 |
    83 |
    Global HTTP Proxy
    84 |
    85 |
    86 | 87 | 95 |
    96 | 99 |
    100 |
    101 |
    102 |
    PAC
    103 | 109 | 110 | 111 | 112 | {pacType === PAC_TYPE_LOCAL && ( 113 |
    114 | 117 |
    118 | )} 119 | {pacType === PAC_TYPE_REMOTE && ( 120 |
    121 |
    122 | 123 | 131 |
    132 | 135 |
    136 | )} 137 |
    138 |
    139 | ); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /ui/src/containers/Plugins/SystemProxy/SystemProxy.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | section { 4 | margin-top: 20px; 5 | } 6 | 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Add/Add.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router-dom'; 4 | import { Button } from '@blueprintjs/core'; 5 | 6 | import { call } from '../../../utils'; 7 | 8 | export default class Add extends React.Component { 9 | 10 | static propTypes = { 11 | match: PropTypes.object.isRequired, 12 | history: PropTypes.object.isRequired, 13 | }; 14 | 15 | state = { 16 | remarks: '', 17 | isPending: false, 18 | }; 19 | 20 | onRemarksChange = (e) => { 21 | this.setState({ remarks: e.target.value }); 22 | }; 23 | 24 | onAddSetting = async () => { 25 | const { history } = this.props; 26 | const { remarks } = this.state; 27 | try { 28 | this.setState({ isPending: true }); 29 | const id = await call('add_setting', { remarks }); 30 | history.push('/services/' + id + '/setting'); 31 | } catch (err) { 32 | console.error(err); 33 | } 34 | this.setState({ isPending: false }); 35 | }; 36 | 37 | render() { 38 | const { remarks, isPending } = this.state; 39 | return ( 40 |
    41 |
      42 |
    • 43 |
    • 44 | Dashboard 45 |
    • 46 |
    • Add Service
    • 47 |
    48 |
    49 | 61 | 65 |
    66 | ); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Graphs/Graphs.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | .graphs { 4 | display: flex; 5 | margin-bottom: 20px; 6 | 7 | .graph { 8 | margin: 0 20px 0 0; 9 | 10 | > h3 { 11 | text-align: center; 12 | } 13 | 14 | .canvas { 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | width: 500px; 19 | height: 280px; 20 | padding: 10px; 21 | border: 1px solid #e5e5e5; 22 | border-radius: 3px; 23 | font-size: 1.2rem; 24 | color: #a0a0a0; 25 | } 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Log/GoogleMap/GoogleMap.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { compose, withProps, withStateHandlers } from 'recompose'; 4 | import { withScriptjs, withGoogleMap, GoogleMap, Marker, Polyline, InfoWindow } from 'react-google-maps'; 5 | 6 | import styles from './GoogleMap.module.scss'; 7 | import { GOOGLE_MAP_API_KEY } from '../../../../constants'; 8 | import { call } from '../../../../utils'; 9 | 10 | const CustomInfoWindow = ({ lat, lng, as, ips, country, regionName, city, org, hostname, onClose }) => ( 11 | 15 |
      16 |
    • Location:{[...new Set([regionName, city, country])].join(' ')}
    • 17 |
    • 18 | IP{ips.length > 1 && `(${ips.length})`}: 19 |
      {ips.join('\n')}
      20 |
    • 21 | {hostname && ( 22 |
    • 23 | Host{hostname.length > 1 && `(${hostname.length})`}: 24 |
      {hostname.join('\n') || '-'}
      25 |
    • 26 | )} 27 |
    • AS:{as || '-'}
    • 28 |
    • Org:{org || '-'}
    • 29 |
    30 |
    31 | ); 32 | 33 | const Map = compose( 34 | withProps({ 35 | googleMapURL: `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAP_API_KEY}&v=3.exp&libraries=geometry,drawing,places`, 36 | loadingElement:
    , 37 | containerElement:
    , 38 | mapElement:
    , 39 | }), 40 | withStateHandlers(() => ({ 41 | infoWindow: { 42 | display: false, 43 | props: null, 44 | }, 45 | }), { 46 | onToggleInfoWindow: () => (isOpen, props) => { 47 | return { 48 | infoWindow: { 49 | display: !isOpen, 50 | props: props, 51 | } 52 | }; 53 | }, 54 | onCloseInfoWindow: () => () => { 55 | return { infoWindow: { display: false, props: null } }; 56 | }, 57 | }), 58 | withScriptjs, 59 | withGoogleMap, 60 | )(({ defaultCenter, ips, infoWindow, onToggleInfoWindow, onCloseInfoWindow }) => 61 | 66 | {ips.map((item, index) => 67 | onToggleInfoWindow(infoWindow.display, item)} 70 | options={{ 71 | position: { lat: item.lat, lng: item.lng }, 72 | icon: item.self ? '' : { 73 | url: require('./destination.png'), 74 | scaledSize: { width: 16, height: 16 }, 75 | anchor: { x: 8, y: 8 }, 76 | }, 77 | }} 78 | /> 79 | )} 80 | {ips.map((item, index) => 81 | 89 | )} 90 | {infoWindow.display && ( 91 | 92 | )} 93 | 94 | ); 95 | 96 | export default class _GoogleMap extends React.Component { 97 | 98 | static propTypes = { 99 | sid: PropTypes.string.isRequired, 100 | }; 101 | 102 | state = { 103 | ips: [], 104 | }; 105 | 106 | componentDidMount() { 107 | this.timer = window.setInterval(this.fetchIPs, 5e3); 108 | this.fetchIPs(); 109 | } 110 | 111 | componentWillUnmount() { 112 | window.clearInterval(this.timer); 113 | } 114 | 115 | fetchIPs = async () => { 116 | try { 117 | const ips = await call('get_geoip', { id: this.props.sid }); 118 | this.setState({ ips }); 119 | } catch (err) { 120 | console.error(err); 121 | } 122 | }; 123 | 124 | render() { 125 | const { ips } = this.state; 126 | const self = ips.filter(({ self }) => self)[0] || { lat: 21.289, lng: -175.253 }; 127 | return ( 128 |
    129 | 133 |
    134 | ); 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Log/GoogleMap/GoogleMap.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | .infoWindow { 4 | list-style: none; 5 | margin: 0; 6 | padding: 5px; 7 | 8 | li { 9 | padding: 3px 0; 10 | max-width: 280px; 11 | white-space: nowrap; 12 | 13 | b { 14 | display: inline-block; 15 | width: 65px; 16 | vertical-align: top; 17 | } 18 | 19 | pre { 20 | display: inline-block; 21 | margin: -2px 0 0; 22 | padding: 0; 23 | box-shadow: none; 24 | width: 100%; 25 | max-height: 50px; 26 | overflow: auto; 27 | font-family: inherit; 28 | } 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Log/GoogleMap/destination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinksocks/blinksocks-gui/b0939ac4ed066e43ff46f9f19238899bd77b5b48/ui/src/containers/Services/Log/GoogleMap/destination.png -------------------------------------------------------------------------------- /ui/src/containers/Services/Log/Log.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | border: 1px solid #d5d8da; 3 | border-radius: 3px; 4 | 5 | .header { 6 | display: flex; 7 | align-items: center; 8 | height: 50px; 9 | padding: 0 10px; 10 | border-bottom: 1px solid #ddd; 11 | border-radius: 3px 3px 0 0; 12 | background-color: #f6f8fa; 13 | 14 | .tools { 15 | position: absolute; 16 | right: 40px; 17 | 18 | > span { 19 | cursor: pointer; 20 | font-size: 16px; 21 | padding: 5px; 22 | opacity: .5; 23 | transition: opacity .1s linear; 24 | 25 | &:hover { 26 | background-color: #eaeaea; 27 | border-radius: 2px; 28 | opacity: .85; 29 | } 30 | } 31 | } 32 | } 33 | 34 | .body { 35 | list-style: none; 36 | margin: 0; 37 | padding: 0; 38 | 39 | li { 40 | padding: 10px; 41 | border-bottom: 1px solid #ddd; 42 | cursor: pointer; 43 | 44 | .conn { 45 | 46 | .abstract { 47 | 48 | span { 49 | display: inline-block; 50 | margin-right: 5px; 51 | 52 | > svg { 53 | margin-top: 1px; 54 | } 55 | } 56 | 57 | .dot { 58 | width: 20px; 59 | height: 10px; 60 | } 61 | } 62 | 63 | .details { 64 | margin-left: 48px; 65 | margin-top: 5px; 66 | font-size: .8rem; 67 | 68 | & > span { 69 | margin-right: 5px; 70 | } 71 | } 72 | } 73 | 74 | &.empty { 75 | color: #30404d; 76 | text-align: center; 77 | } 78 | 79 | &:last-child { 80 | border-bottom: 0; 81 | } 82 | 83 | &:hover { 84 | background-color: #f7f7f7; 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Services.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Route, Link } from 'react-router-dom'; 4 | 5 | import { call } from '../../utils'; 6 | import { MenuRouter } from '../../components/MenuRouter'; 7 | 8 | import Graphs from './Graphs/Graphs'; 9 | import Setting from './Setting/Setting'; 10 | import Log from './Log/Log'; 11 | 12 | import styles from './Services.module.css'; 13 | 14 | export default class Services extends React.Component { 15 | 16 | static propTypes = { 17 | match: PropTypes.object.isRequired, 18 | }; 19 | 20 | state = { 21 | service: null, 22 | }; 23 | 24 | async componentDidMount() { 25 | const { params: { id } } = this.props.match; 26 | try { 27 | const service = await call('get_service', { id }, { showProgress: true }); 28 | this.setState({ service }); 29 | } catch (err) { 30 | console.error(err); 31 | } 32 | } 33 | 34 | createRoutes() { 35 | const { match } = this.props; 36 | return [{ 37 | path: match.url + '/graphs', 38 | text: 'Graphs', 39 | component: Graphs, 40 | }, { 41 | path: match.url + '/log', 42 | text: 'Log', 43 | component: Log, 44 | }, { 45 | path: match.url + '/setting', 46 | text: 'Setting', 47 | component: Setting, 48 | }]; 49 | } 50 | 51 | render() { 52 | const { service } = this.state; 53 | const routes = this.createRoutes(); 54 | return ( 55 |
    56 |
      57 |
    • 58 |
    • 59 | Dashboard 60 |
    • 61 |
    • {service && service.remarks}
    • 62 |
    63 |
    64 | 65 |
    66 | {routes.map(({ path, exact, component }, i) => ( 67 | 68 | ))} 69 |
    70 |
    71 |
    72 | ); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Services.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | .body { 4 | display: flex; 5 | height: 100%; 6 | margin-top: 10px; 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Setting/AddressEditor/AddressEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | import URL from 'url-parse'; 5 | import styles from './AddressEditor.module.css'; 6 | 7 | export default class AddressEditor extends React.Component { 8 | 9 | static propTypes = { 10 | address: PropTypes.string.isRequired, 11 | protocols: PropTypes.arrayOf(PropTypes.string), 12 | onChange: PropTypes.func, 13 | }; 14 | 15 | static defaultProps = { 16 | protocols: [], 17 | onChange: (/* address */) => { 18 | }, 19 | }; 20 | 21 | state = { 22 | protocol: '', 23 | hostname: '', 24 | port: 0, 25 | }; 26 | 27 | constructor(props) { 28 | super(props); 29 | const { protocol, hostname, port } = new URL(this.props.address); 30 | this.state = { 31 | protocol, 32 | hostname, 33 | port, 34 | }; 35 | } 36 | 37 | onChange = () => { 38 | const { protocol, hostname, port } = this.state; 39 | this.props.onChange(`${protocol}//${hostname}:${port}`); 40 | }; 41 | 42 | onProtocolChange = (e) => { 43 | this.setState({ protocol: e.target.value }, this.onChange); 44 | }; 45 | 46 | onHostnameChange = (e) => { 47 | this.setState({ hostname: e.target.value }, this.onChange); 48 | }; 49 | 50 | onPortChange = (e) => { 51 | this.setState({ port: e.target.value | 0 }, this.onChange); 52 | }; 53 | 54 | render() { 55 | const { protocols } = this.props; 56 | const { protocol, hostname, port } = this.state; 57 | return ( 58 |
    59 |
    60 | 66 |
    67 |
    68 | 75 |
    76 |
    77 | 86 |
    87 |
    88 | ); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Setting/AddressEditor/AddressEditor.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | select { 4 | font-family: "Open Sans", "Helvetica Neue", sans-serif; 5 | } 6 | 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Setting/ClientEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | import { TagInput } from '@blueprintjs/core'; 5 | 6 | import AddressEditor from './AddressEditor/AddressEditor'; 7 | 8 | import styles from './Editor.module.css'; 9 | 10 | export default class ClientEditor extends React.Component { 11 | 12 | static propTypes = { 13 | client: PropTypes.object.isRequired, 14 | onChange: PropTypes.func, 15 | }; 16 | 17 | static defaultProps = { 18 | onChange: (/* client */) => { 19 | }, 20 | }; 21 | 22 | static getDerivedStateFromProps({ client }) { 23 | return client; 24 | } 25 | 26 | state = { 27 | service: '', 28 | dns: [], 29 | dns_expire: 3600, 30 | timeout: 200, 31 | log_path: '', 32 | log_level: '', 33 | log_max_days: 30, 34 | }; 35 | 36 | _isAdvancedShow = false; 37 | 38 | onChange = () => { 39 | this.props.onChange(this.state); 40 | }; 41 | 42 | onToggleAdvanced = () => { 43 | this._isAdvancedShow = !this._isAdvancedShow; 44 | this.forceUpdate(); 45 | }; 46 | 47 | onAddressChange = (address) => { 48 | this.setState({ service: address }, this.onChange); 49 | }; 50 | 51 | onLogPathChange = (e) => { 52 | this.setState({ log_path: e.target.value }, this.onChange); 53 | }; 54 | 55 | onLogLevelChange = (e) => { 56 | this.setState({ log_level: e.target.value }, this.onChange); 57 | }; 58 | 59 | onLogMaxDaysChange = (e) => { 60 | this.setState({ log_max_days: e.target.value | 0 }, this.onChange); 61 | }; 62 | 63 | onTimeoutChange = (e) => { 64 | this.setState({ timeout: e.target.value | 0 }, this.onChange); 65 | }; 66 | 67 | onDnsChange = (servers) => { 68 | this.setState({ dns: servers }, this.onChange); 69 | }; 70 | 71 | onDnsExpireChange = (e) => { 72 | this.setState({ dns_expire: e.target.value | 0 }, this.onChange); 73 | }; 74 | 75 | render() { 76 | const { service, log_path, log_level, log_max_days } = this.state; 77 | const { timeout, dns, dns_expire } = this.state; 78 | return ( 79 |
    80 |
    81 |
    Address
    82 | 87 |
    88 | 101 | {this._isAdvancedShow && ( 102 | <> 103 |

    Log

    104 |
    105 |
    Log Path
    106 | 113 |
    114 |
    115 |
    Log Level
    116 |
    117 | 123 |
    124 |
    125 |
    126 |
    Log Max Days
    127 | 134 |
    135 |

    DNS

    136 |
    137 |
    DNS Servers
    138 | 144 |
    145 |
    146 |
    DNS Expire
    147 | 154 |
    155 |

    Others

    156 |
    157 |
    Timeout
    158 | 165 |
    166 | 167 | )} 168 |
    169 | ); 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Setting/Editor.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | > section { 4 | margin-bottom: 10px; 5 | } 6 | 7 | .advanced { 8 | margin: 10px 0; 9 | } 10 | 11 | h3 { 12 | margin-top: 30px; 13 | padding-bottom: 10px; 14 | border-bottom: 1px solid #e8e8e8; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Setting/ProtocolStatckEditor/ProtocolStackEditor.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .presets { 3 | list-style: none; 4 | padding: 0; 5 | margin: 10px 0; 6 | 7 | :global(.sortable-chosen) { 8 | background-color: #fff; 9 | } 10 | 11 | :global(.sortable-ghost) { 12 | background-color: #cee0fd; 13 | } 14 | 15 | .preset { 16 | display: flex; 17 | align-items: center; 18 | justify-content: space-between; 19 | padding: 10px; 20 | margin-bottom: 5px; 21 | border-radius: 3px; 22 | background-color: #f6f6f6; 23 | 24 | .content { 25 | display: flex; 26 | align-items: center; 27 | 28 | .addressing { 29 | float: left; 30 | margin-top: -30px; 31 | margin-left: -10px; 32 | width: 0; 33 | height: 0; 34 | border-radius: 3px 0 0 0; 35 | border-top: 8px solid transparent; 36 | border-bottom: 8px solid transparent; 37 | border-right: 8px solid #2196f3; 38 | transform: rotate(45deg); 39 | } 40 | 41 | .dragger { 42 | margin-right: 5px; 43 | } 44 | } 45 | 46 | .operations { 47 | display: flex; 48 | 49 | svg { 50 | margin-right: 10px; 51 | cursor: pointer; 52 | 53 | &:last-child { 54 | margin-right: 0; 55 | } 56 | } 57 | } 58 | 59 | p { 60 | margin: 0; 61 | font-weight: 600; 62 | } 63 | 64 | small { 65 | margin-top: 5px; 66 | } 67 | 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Setting/Setting.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { observer } from 'mobx-react'; 4 | import { Prompt, matchPath } from 'react-router-dom'; 5 | import { Button, Intent } from '@blueprintjs/core'; 6 | import classnames from 'classnames'; 7 | import omit from 'lodash/omit'; 8 | import keyBy from 'lodash/keyBy'; 9 | import cloneDeep from 'lodash/cloneDeep'; 10 | 11 | import ClientEditor from './ClientEditor'; 12 | import ServerEditor from './ServerEditor'; 13 | import styles from './Setting.module.css'; 14 | 15 | import { call, toast, store } from '../../../utils'; 16 | import Title from '../../../components/Title/Title'; 17 | 18 | @observer 19 | export default class Setting extends React.Component { 20 | 21 | static propTypes = { 22 | match: PropTypes.string.isRequired, 23 | }; 24 | 25 | state = { 26 | id: '', 27 | config: null, 28 | client: null, // client only 29 | server: null, 30 | isClient: false, 31 | defs: null, 32 | isUnSaved: false, 33 | isSaving: false, 34 | }; 35 | 36 | async componentDidMount() { 37 | const { params: { id } } = matchPath(this.props.match.url, { path: '/services/:id' }); 38 | try { 39 | const [config, defs] = await Promise.all([ 40 | call('get_config', { id }), 41 | call('get_preset_defs', null, { cache: true }), 42 | ]); 43 | const isClient = !!config.server; 44 | let client, server; 45 | if (isClient) { 46 | client = omit(config, 'server'); 47 | server = config.server; 48 | } else { 49 | server = config; 50 | } 51 | // mix "._def" in each server.presets 52 | const map = keyBy(defs, 'name'); 53 | for (const preset of server.presets) { 54 | preset._def = map[preset.name]; 55 | } 56 | this.setState({ id, config, client, server, isClient, defs: map }); 57 | } catch (err) { 58 | console.error(err); 59 | } 60 | } 61 | 62 | onClientChange = (client) => { 63 | this.setState({ client, isUnSaved: true }); 64 | }; 65 | 66 | onServerChange = (server) => { 67 | this.setState({ server, isUnSaved: true }); 68 | }; 69 | 70 | onSave = async () => { 71 | const { id } = this.state; 72 | const { isClient, client, server } = this.state; 73 | // drop "._def" of each server.presets 74 | const serverCopy = cloneDeep(server); 75 | for (const preset of serverCopy.presets) { 76 | delete preset._def; 77 | } 78 | let config; 79 | if (isClient) { 80 | config = { ...client, server: serverCopy }; 81 | } else { 82 | config = serverCopy; 83 | } 84 | this.setState({ isSaving: true }); 85 | try { 86 | await call('save_setting', { id, config }); 87 | toast('configuration saved!', 'success'); 88 | } catch (err) { 89 | console.error(err.message); 90 | } 91 | this.setState({ isSaving: false, isUnSaved: false }); 92 | }; 93 | 94 | onSaveAndRestart = async () => { 95 | const { id } = this.state; 96 | await this.onSave(); 97 | try { 98 | await call('restart_service', { id }); 99 | toast('service restart successfully!', 'success'); 100 | } catch (err) { 101 | console.error(err.message); 102 | } 103 | }; 104 | 105 | renderSaveButton = () => { 106 | const { id, isSaving, isUnSaved } = this.state; 107 | const props = { 108 | disabled: !isUnSaved, 109 | loading: isSaving, 110 | }; 111 | if (store.isServiceRunning(id)) { 112 | return ( 113 |
    114 |

    115 | This service is RUNNING now, save and restart to take effect immediately. 116 |

    117 | 120 |    121 | 124 |
    125 | ); 126 | } else { 127 | return ; 128 | } 129 | }; 130 | 131 | renderClient = () => { 132 | const { client, server, isClient, defs } = this.state; 133 | return ( 134 | <> 135 |
    136 |

    Local Service

    137 | {client && ( 138 | 142 | )} 143 |
    144 |
    145 |

    Remote Server

    146 | {server && defs && ( 147 | 153 | )} 154 |
    155 | 156 | ); 157 | }; 158 | 159 | renderServer = () => { 160 | const { server, isClient, defs } = this.state; 161 | return ( 162 |
    163 |

    Local Service

    164 | {server && defs && ( 165 | 171 | )} 172 |
    173 | ); 174 | }; 175 | 176 | render() { 177 | const { id, isClient, isUnSaved } = this.state; 178 | return ( 179 |
    180 | Setting 181 | 182 | {isUnSaved && !store.isServiceRunning(id) && ( 183 |

    184 | You have unsaved changes, remember to save before start/restart service. 185 | 188 |

    189 | )} 190 |
    191 | {isClient ? this.renderClient() : this.renderServer()} 192 |
    193 | {this.renderSaveButton()} 194 |
    195 | ); 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Setting/Setting.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-bottom: 20px; 3 | 4 | .warning { 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | 9 | &:before { 10 | margin-top: 5px; 11 | } 12 | } 13 | 14 | .body { 15 | & > section { 16 | padding: 20px; 17 | margin-bottom: 20px; 18 | border: 2px solid #f3f3f3; 19 | border-radius: 4px; 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/containers/Services/Setting/ToolTip/ToolTip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tooltip, Icon, Position } from "@blueprintjs/core/lib/esm/index"; 3 | 4 | const style = { 5 | cursor: 'pointer', 6 | position: 'relative', 7 | top: '3px', 8 | left: '3px', 9 | }; 10 | 11 | export default ({ content }) => ( 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /ui/src/containers/Settings/Settings.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | 4 | h3 { 5 | padding: 0 10px; 6 | margin-bottom: 10px; 7 | border-left: 3px solid #4390ff; 8 | white-space: nowrap; 9 | overflow: hidden; 10 | text-overflow: ellipsis; 11 | } 12 | 13 | .table { 14 | width: 100%; 15 | margin-bottom: 20px; 16 | 17 | tbody { 18 | tr td { 19 | vertical-align: middle; 20 | 21 | &:nth-child(3) { 22 | text-align: center; 23 | } 24 | 25 | svg { 26 | cursor: pointer; 27 | } 28 | } 29 | } 30 | } 31 | 32 | .buttons { 33 | display: flex; 34 | 35 | & > button { 36 | margin-right: 10px; 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /ui/src/containers/Settings/UserItem/UserItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | 5 | import styles from './UserItem.module.css'; 6 | 7 | export default class UserItem extends React.Component { 8 | 9 | static propTypes = { 10 | user: PropTypes.object.isRequired, 11 | onChange: PropTypes.func, 12 | }; 13 | 14 | static defaultProps = { 15 | onChange: (/* user */) => { 16 | }, 17 | }; 18 | 19 | $password = null; 20 | 21 | state = { 22 | user: null, 23 | isShowPassword: false, 24 | }; 25 | 26 | static getDerivedStateFromProps({ user }) { 27 | return { user }; 28 | } 29 | 30 | onChange = () => { 31 | this.props.onChange(this.state.user); 32 | }; 33 | 34 | onPasswordChange = (e) => { 35 | this.setState({ 36 | user: { 37 | ...this.state.user, 38 | password: e.target.value 39 | }, 40 | }, this.onChange); 41 | }; 42 | 43 | onTogglePasswordView = () => { 44 | this.setState({ isShowPassword: !this.state.isShowPassword }); 45 | this.$password.focus(); 46 | }; 47 | 48 | onToggleMethod = (e, _name) => { 49 | const { user: { methods } } = this.state; 50 | const checked = e.target.checked; 51 | this.setState({ 52 | user: { 53 | ...this.state.user, 54 | methods: methods.map(({ name, active }) => ({ 55 | name, 56 | active: name === _name ? checked : active, 57 | })), 58 | }, 59 | }, this.onChange); 60 | }; 61 | 62 | render() { 63 | const { user, isShowPassword } = this.state; 64 | return ( 65 |
    66 |
    67 | 70 |
    71 |
    72 | this.$password = dom} 74 | type={isShowPassword ? 'text' : 'password'} 75 | className="pt-input" 76 | placeholder="type password here" 77 | value={user && user.password} 78 | onChange={this.onPasswordChange} 79 | /> 80 |
    88 |
    89 |
    90 |
    91 | 94 |
    95 | {user && user.methods.map(({ name, active }) => ( 96 | 101 | ))} 102 |
    103 |
    104 |
    105 | ); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /ui/src/containers/Settings/UserItem/UserItem.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | .password { 4 | margin-bottom: 10px; 5 | } 6 | 7 | .methods { 8 | display: flex; 9 | flex-wrap: wrap; 10 | margin-bottom: 0; 11 | max-height: 180px; 12 | overflow-y: auto; 13 | 14 | > label { 15 | width: 200px; 16 | margin-right: 20px; 17 | overflow: hidden; 18 | text-overflow: ellipsis; 19 | line-height: 13px; 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import XHR from 'i18next-xhr-backend'; 3 | import LanguageDetector from 'i18next-browser-languagedetector'; 4 | 5 | i18n 6 | .use(XHR) 7 | .use(LanguageDetector) 8 | .init({ 9 | fallbackLng: 'en', 10 | debug: true, 11 | backend: { 12 | loadPath: '/locales/{{lng}}/{{ns}}.json' 13 | }, 14 | react: { 15 | wait: false, 16 | bindI18n: 'languageChanged loaded', 17 | bindStore: 'added removed', 18 | nsMode: 'default' 19 | } 20 | }); 21 | 22 | export default i18n; 23 | -------------------------------------------------------------------------------- /ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import ReactGA from 'react-ga'; 4 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 5 | import { FocusStyleManager } from "@blueprintjs/core"; 6 | import { I18nextProvider } from 'react-i18next'; 7 | 8 | import 'echarts/lib/chart/line'; 9 | import 'echarts/lib/component/title'; 10 | import 'echarts/lib/component/tooltip'; 11 | 12 | import i18n from './i18n'; 13 | 14 | import 'nprogress/nprogress.css'; 15 | import 'normalize.css'; 16 | import './index.css'; 17 | 18 | import App from './containers/App/App'; 19 | import Landing from './containers/Landing/Landing'; 20 | // import registerServiceWorker from './registerServiceWorker'; 21 | 22 | document.addEventListener('DOMContentLoaded', () => { 23 | ReactDOM.render( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | , 32 | document.getElementById('root') 33 | ); 34 | }); 35 | 36 | // registerServiceWorker(); 37 | FocusStyleManager.onlyShowFocusOnTabs(); 38 | 39 | // initialize google analytics 40 | if (process.env.NODE_ENV === 'production') { 41 | ReactGA.initialize(process.env.GOOGLE_ANALYTICS_TRACKING_ID); 42 | } 43 | -------------------------------------------------------------------------------- /ui/src/index.scss: -------------------------------------------------------------------------------- 1 | @import '~@blueprintjs/core/lib/css/blueprint.css'; 2 | @import '~@blueprintjs/icons/lib/css/blueprint-icons.css'; 3 | 4 | @font-face { 5 | font-family: "Titillium Web"; 6 | src: url('/fonts/TitilliumWeb-Light.ttf') format("truetype"); 7 | } 8 | 9 | body { 10 | font-family: "Titillium Web", "Open Sans", "Helvetica Neue", sans-serif; 11 | padding: 0; 12 | margin: 0; 13 | background-color: #fff; 14 | } 15 | 16 | :global { 17 | 18 | .link-reset { 19 | color: #fff; 20 | text-decoration: none; 21 | 22 | &:hover { 23 | color: #fff; 24 | text-decoration: none; 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /ui/src/utils/hash.js: -------------------------------------------------------------------------------- 1 | const jsSHA = require('jssha'); 2 | 3 | export default function hash(algorithm, message) { 4 | const shaObj = new jsSHA(algorithm, 'TEXT'); 5 | shaObj.update(message); 6 | return shaObj.getHash('HEX'); 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/utils/index.js: -------------------------------------------------------------------------------- 1 | import hash from './hash'; 2 | import live from './live'; 3 | import call from './rpc'; 4 | import store from './store'; 5 | import toast from './toast'; 6 | 7 | export { hash, live, call, toast, store }; 8 | -------------------------------------------------------------------------------- /ui/src/utils/live.js: -------------------------------------------------------------------------------- 1 | import call from './rpc'; 2 | import ws from './ws'; 3 | import { KEEPALIVE_INTERVAL } from '../constants'; 4 | 5 | const SERVER_PUSH_REGISTER_SUCCESS = 0; 6 | const SERVER_PUSH_REGISTER_ERROR = 1; 7 | 8 | export default async function live(method, args, callback) { 9 | // refine arguments 10 | if (typeof args === 'function') { 11 | callback = args; 12 | args = {}; 13 | } 14 | 15 | // a timer for keepalive 16 | let timer = null; 17 | 18 | function onServerPush(response) { 19 | console.log(new Date().toISOString(), '[PUSH]', `[${method}]`, response); 20 | // reset timer 21 | // if (timer) { 22 | // window.clearInterval(timer); 23 | // timer = window.setInterval(keepalive, KEEPALIVE_INTERVAL); 24 | // } 25 | // handle message 26 | callback(response); 27 | } 28 | 29 | // listening for server push before send register request 30 | ws.on(method, onServerPush); 31 | 32 | // register a server push method 33 | const { code } = await call('_register_server_push', { method, args }); 34 | switch (code) { 35 | case SERVER_PUSH_REGISTER_SUCCESS: 36 | break; 37 | case SERVER_PUSH_REGISTER_ERROR: 38 | ws.off(method, onServerPush); 39 | break; 40 | default: 41 | throw Error(`unknown register response status code: ${code}`); 42 | } 43 | 44 | async function keepalive() { 45 | try { 46 | await call('_keepalive_server_push', { method }); 47 | } catch (err) { 48 | await unlive(); 49 | } 50 | } 51 | 52 | // setup keepalive timer 53 | timer = window.setInterval(keepalive, KEEPALIVE_INTERVAL); 54 | 55 | // return a function to unregister server push 56 | async function unlive() { 57 | window.clearInterval(timer); 58 | ws.off(method, onServerPush); 59 | await call('_unregister_server_push', { method }); 60 | } 61 | 62 | return unlive; 63 | } 64 | -------------------------------------------------------------------------------- /ui/src/utils/rpc.js: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress'; 2 | import ws from './ws'; 3 | import store from './store'; 4 | import toast from './toast'; 5 | import { RPC_TIMEOUT, SERVICE_STATUS_INIT, RPC_STATUS_ONLINE } from '../constants'; 6 | 7 | const cacheStorage = {}; 8 | 9 | function getCache(request) { 10 | try { 11 | const key = JSON.stringify(request); 12 | const value = cacheStorage[key]; 13 | if (typeof value !== 'undefined') { 14 | return value; 15 | } 16 | } catch (err) { 17 | // ignore 18 | } 19 | return null; 20 | } 21 | 22 | function setCache(request, data) { 23 | try { 24 | const key = JSON.stringify(request); 25 | cacheStorage[key] = data; 26 | } catch (err) { 27 | // ignore 28 | } 29 | } 30 | 31 | function createRequest(method, args) { 32 | if (typeof method !== 'string' || method === '') { 33 | throw Error('method is invalid'); 34 | } 35 | const request = { method }; 36 | if (typeof args !== 'undefined' && args !== null) { 37 | request.args = args; 38 | } 39 | return request; 40 | } 41 | 42 | export default async function rpc(method, args, options) { 43 | const { cache = false, showProgress = false, timeout = RPC_TIMEOUT } = options || {}; 44 | 45 | if (store.rpcStatus !== SERVICE_STATUS_INIT && store.rpcStatus !== RPC_STATUS_ONLINE) { 46 | throw Error('rpc service is offline'); 47 | } 48 | 49 | const request = createRequest(method, args); 50 | 51 | // return directly if cached 52 | let cachedValue; 53 | if (cache && (cachedValue = getCache(request)) !== null) { 54 | return cachedValue; 55 | } 56 | 57 | return new Promise((resolve, reject) => { 58 | console.log(new Date().toISOString(), '[request]', request); 59 | showProgress && NProgress.start(); 60 | 61 | let isTimeout = false; 62 | 63 | function onTimeout() { 64 | if (store.rpcStatus !== RPC_STATUS_ONLINE && !showProgress) { 65 | // prevent timeout toast when it's not online 66 | return; 67 | } 68 | const hint = `method "${method}" timeout`; 69 | isTimeout = true; 70 | showProgress && NProgress.done(); 71 | toast(hint, 'warning'); 72 | reject(Error(hint)); 73 | } 74 | 75 | // timeout timer 76 | const timer = window.setTimeout(onTimeout, timeout); 77 | 78 | ws.emit('request', request, (response) => { 79 | if (isTimeout) { 80 | // drop response returned after timeout 81 | return; 82 | } 83 | console.log(new Date().toISOString(), '[response]', response); 84 | clearTimeout(timer); 85 | showProgress && NProgress.done(); 86 | const { code, data, message } = response; 87 | if (code === 0) { 88 | if (cache) { 89 | setCache(request, data); 90 | } 91 | resolve(data); 92 | } else { 93 | const msg = `Server Respond Error: ${message || 'unknown error'}`; 94 | toast(msg, 'warning'); 95 | reject(Error(msg)); 96 | } 97 | }); 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /ui/src/utils/store.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | import { RPC_STATUS_INIT, SERVICE_STATUS_INIT, SERVICE_STATUS_RUNNING } from '../constants'; 3 | 4 | const store = observable({ 5 | 6 | // global environment variables 7 | env: observable.map({}), 8 | 9 | // global websocket status 10 | rpcStatus: RPC_STATUS_INIT, 11 | 12 | // global websocket latency in ms 13 | rpcLatency: 0, 14 | 15 | // services 16 | services: observable.map({ 17 | // : { 18 | // status: SERVICE_XXX, 19 | // connections: 0, 20 | // total_download_bytes: 0, 21 | // total_upload_bytes: 0, 22 | // download_speed: 0, 23 | // upload_speed: 0, 24 | // }, 25 | // ... 26 | }), 27 | 28 | // custom methods 29 | isServiceRunning(id) { 30 | return this.getServiceStatusById(id) === SERVICE_STATUS_RUNNING; 31 | }, 32 | getServiceStatusById(id) { 33 | return !this.services[id] ? SERVICE_STATUS_INIT : this.services[id].status; 34 | }, 35 | 36 | }); 37 | 38 | if (process.env.NODE_ENV === 'development') { 39 | window.store = store; 40 | } 41 | 42 | export default store; 43 | -------------------------------------------------------------------------------- /ui/src/utils/toast.js: -------------------------------------------------------------------------------- 1 | import { Position, Toaster, Intent } from '@blueprintjs/core'; 2 | 3 | const MyToaster = Toaster.create({ 4 | position: Position.TOP, 5 | }); 6 | 7 | export default function toast(message, intent = Intent.NONE) { 8 | if (typeof message === 'object') { 9 | MyToaster.show(message); 10 | } else { 11 | MyToaster.show({ message, intent }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/utils/ws.js: -------------------------------------------------------------------------------- 1 | import io from 'socket.io-client'; 2 | import toast from './toast'; 3 | import store from './store'; 4 | import { TOKEN_NAME, RPC_STATUS_ERROR, RPC_STATUS_OFFLINE, RPC_STATUS_ONLINE } from '../constants'; 5 | 6 | const ws = io.connect('/', { 7 | query: { 8 | token: localStorage.getItem(TOKEN_NAME) || '', 9 | }, 10 | }); 11 | 12 | ws.on('connect', function onConnect() { 13 | store.rpcStatus = RPC_STATUS_ONLINE; 14 | }); 15 | 16 | ws.on('connect_error', () => { 17 | store.rpcStatus = RPC_STATUS_OFFLINE; 18 | }); 19 | 20 | ws.on('reconnecting', (attempts) => { 21 | if (attempts >= 10) { 22 | ws.close(); 23 | store.rpcStatus = RPC_STATUS_ERROR; 24 | } 25 | }); 26 | 27 | ws.on('reconnect_failed', () => { 28 | store.rpcStatus = RPC_STATUS_ERROR; 29 | }); 30 | 31 | ws.on('pong', (latency) => { 32 | store.rpcLatency = latency; 33 | }); 34 | 35 | ws.on('error', (message) => { 36 | switch (message) { 37 | case 'authentication error': 38 | const { pathname } = window.location; 39 | if (pathname !== '/landing') { 40 | toast({ 41 | message: 'authentication error, taking you to verify page...', 42 | intent: 'danger', 43 | timeout: 1000, 44 | onDismiss() { 45 | window.location.replace('/landing'); 46 | }, 47 | }); 48 | } 49 | break; 50 | default: 51 | console.error(message); 52 | break; 53 | } 54 | }); 55 | 56 | export default ws; 57 | --------------------------------------------------------------------------------