├── docs ├── .nojekyll ├── admin.md ├── user.md ├── _media │ ├── home.png │ ├── alipay.jpg │ ├── favicon.png │ └── wechat.jpg ├── upgrade.md ├── donate.md ├── transfer.md ├── _coverpage.md ├── managerapi.md ├── vps.md ├── skin.md ├── _sidebar.md ├── home.md ├── install.md ├── pay.md ├── topological.md ├── ssmgrapi.md ├── index.html └── config.md ├── plugins ├── webgui │ ├── public │ │ ├── views │ │ │ ├── home │ │ │ │ ├── ref.html │ │ │ │ ├── macLogin.html │ │ │ │ ├── refInput.html │ │ │ │ ├── telegramLogin.html │ │ │ │ └── index.html │ │ │ ├── admin │ │ │ │ ├── unfinished.html │ │ │ │ ├── mailSetting.html │ │ │ │ ├── previewNotice.html │ │ │ │ ├── orderSetting.html │ │ │ │ ├── pickAccount.html │ │ │ │ ├── groupList.html │ │ │ │ ├── paymentList.html │ │ │ │ ├── notice.html │ │ │ │ ├── settings.html │ │ │ │ ├── giftcardBatchList.html │ │ │ │ ├── editRefCode.html │ │ │ │ ├── user.html │ │ │ │ ├── telegramSetting.html │ │ │ │ ├── account.html │ │ │ │ ├── addUser.html │ │ │ │ ├── refCodeList.html │ │ │ │ ├── accountSortAndFilterDialog.html │ │ │ │ ├── newNotice.html │ │ │ │ ├── editNotice.html │ │ │ │ ├── changePassword.html │ │ │ │ └── userSortDialog.html │ │ │ ├── skin │ │ │ │ ├── fs_zelda │ │ │ │ │ └── zelda.jpg │ │ │ │ ├── fs_dinosaur │ │ │ │ │ ├── assets │ │ │ │ │ │ ├── offline-sprite-1x.png │ │ │ │ │ │ ├── offline-sprite-2x.png │ │ │ │ │ │ ├── default_100_percent │ │ │ │ │ │ │ ├── 100-disabled.png │ │ │ │ │ │ │ ├── 100-error-offline.png │ │ │ │ │ │ │ └── 100-offline-sprite.png │ │ │ │ │ │ └── default_200_percent │ │ │ │ │ │ │ ├── 200-disabled.png │ │ │ │ │ │ │ ├── 200-error-offline.png │ │ │ │ │ │ │ └── 200-offline-sprite.png │ │ │ │ │ └── README.md │ │ │ │ ├── bing.html │ │ │ │ └── default.html │ │ │ ├── user │ │ │ │ ├── macAddress.html │ │ │ │ ├── qrcodeDialog.html │ │ │ │ ├── order.html │ │ │ │ ├── telegram.html │ │ │ │ └── changePassword.html │ │ │ └── dialog │ │ │ │ ├── autopop.html │ │ │ │ ├── showWireGuardConfig.html │ │ │ │ ├── language.html │ │ │ │ ├── alert.html │ │ │ │ ├── serverChart.html │ │ │ │ ├── addMacAccount.html │ │ │ │ ├── setUserGroup.html │ │ │ │ ├── confirm.html │ │ │ │ ├── subscribe.html │ │ │ │ ├── setEmail.html │ │ │ │ ├── email.html │ │ │ │ ├── editUserComment.html │ │ │ │ └── payByGiftCard.html │ │ ├── translate │ │ │ ├── zh-CN.js │ │ │ └── index.js │ │ ├── app.js │ │ ├── configs │ │ │ ├── index.js │ │ │ ├── sceProvider.js │ │ │ ├── urlRouter.js │ │ │ ├── themingProvider.js │ │ │ └── auth.js │ │ ├── dialogs │ │ │ ├── index.js │ │ │ ├── serverChart.js │ │ │ ├── markdown.js │ │ │ ├── qrcode.js │ │ │ ├── addGiftCardBatch.js │ │ │ ├── editUserComment.js │ │ │ ├── autopop.js │ │ │ ├── alert.js │ │ │ ├── language.js │ │ │ ├── setUserGroup.js │ │ │ ├── changePassword.js │ │ │ ├── user.js │ │ │ ├── addMacAccount.js │ │ │ ├── confirm.js │ │ │ ├── ban.js │ │ │ └── email.js │ │ ├── filters │ │ │ ├── index.js │ │ │ ├── substr.js │ │ │ ├── mac.js │ │ │ ├── giftcard.js │ │ │ ├── ban.js │ │ │ └── orderStatus.js │ │ ├── routes │ │ │ ├── index.js │ │ │ ├── adminUser.js │ │ │ ├── adminServer.js │ │ │ ├── adminAccount.js │ │ │ ├── admin.js │ │ │ ├── user.js │ │ │ └── home.js │ │ ├── controllers │ │ │ └── index.js │ │ ├── services │ │ │ ├── configService.js │ │ │ ├── websocketService.js │ │ │ └── preloadService.js │ │ ├── directives │ │ │ └── focusMe.js │ │ ├── index.js │ │ └── styles │ │ │ └── default.css │ ├── dependence.js │ ├── libs │ │ ├── favicon.png │ │ ├── MaterialIcons-Regular.eot │ │ ├── MaterialIcons-Regular.ttf │ │ ├── MaterialIcons-Regular.woff │ │ ├── MaterialIcons-Regular.woff2 │ │ ├── forkme_right_white_ffffff.png │ │ ├── ngclipboard.min.js │ │ └── style.css │ ├── screenshot │ │ ├── 01.png │ │ ├── 02.png │ │ ├── 03.png │ │ ├── 04.png │ │ └── 05.png │ ├── db │ │ ├── setting.js │ │ ├── push.js │ │ └── notice.js │ ├── views │ │ ├── manifest.js │ │ └── error.html │ └── server │ │ └── adminGroup.js ├── giftcard │ ├── README.md │ └── db │ │ └── giftcard.js ├── cli │ ├── screenshot │ │ ├── cli01.png │ │ └── cli02.png │ ├── index.js │ ├── menu │ │ ├── addPort.js │ │ ├── main.js │ │ ├── flow.js │ │ └── addServer.js │ └── README.md ├── telegram │ ├── screenshot │ │ ├── telegram01.png │ │ └── telegram02.png │ ├── db │ │ ├── telegram.js │ │ ├── flow.js │ │ └── server.js │ ├── managerAddress.js │ ├── help.js │ ├── serverManager.js │ ├── README.md │ ├── auth.js │ └── flow.js ├── flowSaver │ ├── README.md │ └── db │ │ ├── saveFlow.js │ │ ├── saveFlow5min.js │ │ ├── saveFlowDay.js │ │ ├── saveFlowHour.js │ │ └── server.js ├── email │ ├── README.md │ └── db │ │ └── email.js ├── webgui_telegram │ ├── db │ │ └── webgui_telegram.js │ ├── admin.js │ ├── help.js │ └── flow.js ├── webgui_order │ ├── flowPack.js │ └── db │ │ └── webgui_flow_pack.js ├── webgui_ref │ └── db │ │ ├── webgui_ref.js │ │ ├── webgui_ref_code.js │ │ └── webgui_ref_time.js ├── freeAccount │ ├── db │ │ └── freeAccount.js │ └── README.md ├── macAccount │ └── db │ │ └── macAccount.js ├── webgui_autoban │ └── README.md ├── alipay │ └── db │ │ └── alipay.js ├── paypal │ └── db │ │ └── paypal.js ├── account │ └── db │ │ ├── accountFlow.js │ │ └── account.js ├── user │ └── db │ │ └── user.js └── group │ ├── db │ └── group.js │ └── index.js ├── .gitattributes ├── wikiImage ├── signup.png ├── addServer.png ├── addAccount.png ├── homeScreenChrome.jpg └── homeScreenSafari.jpg ├── bin └── ssmgr ├── config └── default.yml ├── init ├── utils.js ├── loadModels.js ├── loadServices.js ├── cron.js ├── knex.js ├── moveConfigFile.js ├── runShadowsocks.js ├── log.js ├── loadPlugins.js └── checkConfig.js ├── .eslintrc.json ├── models ├── account.js ├── command.js └── flow.js ├── docker ├── ubuntu │ └── Dockerfile └── alpine │ └── Dockerfile ├── .github └── issue_template.md ├── server.js ├── .gitignore ├── .npmignore └── services └── config.js /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/admin.md: -------------------------------------------------------------------------------- 1 | # 管理员 -------------------------------------------------------------------------------- /docs/user.md: -------------------------------------------------------------------------------- 1 | # 普通用户 -------------------------------------------------------------------------------- /plugins/webgui/public/views/home/ref.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/home/macLogin.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/home/refInput.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/home/telegramLogin.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/webgui/public/translate/zh-CN.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/unfinished.html: -------------------------------------------------------------------------------- 1 |
该页面尚未完工
2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf 2 | *.html text eol=lf 3 | *.css text eol=lf -------------------------------------------------------------------------------- /plugins/webgui/public/app.js: -------------------------------------------------------------------------------- 1 | require('@babel/polyfill'); 2 | require('./index'); 3 | -------------------------------------------------------------------------------- /docs/_media/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/docs/_media/home.png -------------------------------------------------------------------------------- /wikiImage/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/wikiImage/signup.png -------------------------------------------------------------------------------- /docs/_media/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/docs/_media/alipay.jpg -------------------------------------------------------------------------------- /docs/_media/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/docs/_media/favicon.png -------------------------------------------------------------------------------- /docs/_media/wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/docs/_media/wechat.jpg -------------------------------------------------------------------------------- /wikiImage/addServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/wikiImage/addServer.png -------------------------------------------------------------------------------- /wikiImage/addAccount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/wikiImage/addAccount.png -------------------------------------------------------------------------------- /plugins/giftcard/README.md: -------------------------------------------------------------------------------- 1 | # Enable 2 | config.yaml: 3 | ``` 4 | plugins: 5 | giftcard: 6 | use: true 7 | ``` -------------------------------------------------------------------------------- /plugins/webgui/dependence.js: -------------------------------------------------------------------------------- 1 | module.exports = ['webgui_ref', 'group', 'macAccount', 'webgui_order', 'account_checker']; -------------------------------------------------------------------------------- /wikiImage/homeScreenChrome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/wikiImage/homeScreenChrome.jpg -------------------------------------------------------------------------------- /wikiImage/homeScreenSafari.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/wikiImage/homeScreenSafari.jpg -------------------------------------------------------------------------------- /plugins/cli/screenshot/cli01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/cli/screenshot/cli01.png -------------------------------------------------------------------------------- /plugins/cli/screenshot/cli02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/cli/screenshot/cli02.png -------------------------------------------------------------------------------- /plugins/webgui/libs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/libs/favicon.png -------------------------------------------------------------------------------- /plugins/webgui/screenshot/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/screenshot/01.png -------------------------------------------------------------------------------- /plugins/webgui/screenshot/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/screenshot/02.png -------------------------------------------------------------------------------- /plugins/webgui/screenshot/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/screenshot/03.png -------------------------------------------------------------------------------- /plugins/webgui/screenshot/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/screenshot/04.png -------------------------------------------------------------------------------- /plugins/webgui/screenshot/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/screenshot/05.png -------------------------------------------------------------------------------- /plugins/telegram/screenshot/telegram01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/telegram/screenshot/telegram01.png -------------------------------------------------------------------------------- /plugins/telegram/screenshot/telegram02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/telegram/screenshot/telegram02.png -------------------------------------------------------------------------------- /plugins/webgui/libs/MaterialIcons-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/libs/MaterialIcons-Regular.eot -------------------------------------------------------------------------------- /plugins/webgui/libs/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/libs/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /plugins/webgui/libs/MaterialIcons-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/libs/MaterialIcons-Regular.woff -------------------------------------------------------------------------------- /plugins/webgui/libs/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/libs/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /plugins/flowSaver/README.md: -------------------------------------------------------------------------------- 1 | # flow saver plugin 2 | 3 | This plugin can count the flows from every port. It can not use alone, must be use with other plugins. 4 | -------------------------------------------------------------------------------- /plugins/webgui/libs/forkme_right_white_ffffff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/libs/forkme_right_white_ffffff.png -------------------------------------------------------------------------------- /plugins/webgui/public/views/skin/fs_zelda/zelda.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/public/views/skin/fs_zelda/zelda.jpg -------------------------------------------------------------------------------- /docs/upgrade.md: -------------------------------------------------------------------------------- 1 | # 升级 2 | 3 | 重新安装并带上版本号即可升级到指定版本: 4 | 5 | `npm i -g shadowsocks-manager@a.b.c` 6 | 7 | !> 升级前请做好备份 8 | 9 | !> 请勿跨版本升级,例如`0.21.0`可以升级到`0.22.x`,但不能直接升级到`0.23.x` -------------------------------------------------------------------------------- /plugins/webgui/public/configs/index.js: -------------------------------------------------------------------------------- 1 | const req = require.context('./', true, /^(?!.*index.js)((.*\.(js\.*))[^.]*$)/im); 2 | req.keys().forEach(file => { 3 | req(file); 4 | }); -------------------------------------------------------------------------------- /plugins/webgui/public/configs/sceProvider.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | 3 | app.config(['$sceProvider', $sceProvider => { 4 | $sceProvider.enabled(false); 5 | }]); -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/index.js: -------------------------------------------------------------------------------- 1 | const req = require.context('./', true, /^(?!.*index.js)((.*\.(js\.*))[^.]*$)/im); 2 | req.keys().forEach(file => { 3 | req(file); 4 | }); -------------------------------------------------------------------------------- /plugins/webgui/public/filters/index.js: -------------------------------------------------------------------------------- 1 | const req = require.context('./', true, /^(?!.*index.js)((.*\.(js\.*))[^.]*$)/im); 2 | req.keys().forEach(file => { 3 | req(file); 4 | }); -------------------------------------------------------------------------------- /plugins/webgui/public/routes/index.js: -------------------------------------------------------------------------------- 1 | const req = require.context('./', true, /^(?!.*index.js)((.*\.(js\.*))[^.]*$)/im); 2 | req.keys().forEach(file => { 3 | req(file); 4 | }); -------------------------------------------------------------------------------- /plugins/email/README.md: -------------------------------------------------------------------------------- 1 | # email plugin 2 | 3 | This plugin can send email to users. You can use it to send verification codes. It can not use alone, must be use with other plugins. 4 | -------------------------------------------------------------------------------- /plugins/webgui/public/controllers/index.js: -------------------------------------------------------------------------------- 1 | const req = require.context('./', true, /^(?!.*index.js)((.*\.(js\.*))[^.]*$)/im); 2 | req.keys().forEach(file => { 3 | req(file); 4 | }); 5 | -------------------------------------------------------------------------------- /docs/donate.md: -------------------------------------------------------------------------------- 1 | # 捐赠 2 | 3 | 如果觉得这个项目对你有帮助,不妨给作者捐赠。 4 | 5 | 1. 支付宝 6 | 7 | ![alipay](/_media/alipay.jpg ':size=480') 8 | 9 | 2. 微信 10 | 11 | ![wechat](/_media/wechat.jpg ':size=480') -------------------------------------------------------------------------------- /plugins/webgui/public/views/skin/fs_dinosaur/assets/offline-sprite-1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/public/views/skin/fs_dinosaur/assets/offline-sprite-1x.png -------------------------------------------------------------------------------- /plugins/webgui/public/views/skin/fs_dinosaur/assets/offline-sprite-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/public/views/skin/fs_dinosaur/assets/offline-sprite-2x.png -------------------------------------------------------------------------------- /docs/transfer.md: -------------------------------------------------------------------------------- 1 | # 迁移 2 | 3 | ## Web 端 4 | 5 | 若使用 SQLite,__先停止服务__,备份`~/.ssmgr`目录下的文件到新的机器即可 6 | 7 | 若使用 MySQL,把配置文件移到新的机器即可 8 | 9 | ## 节点端 10 | 11 | 节点端无需迁移,新节点的服务成功开启后,在管理界面的“服务器”部分编辑旧的服务器,填上新的服务器信息,点击保存,大约几分钟端口信息就能同步到新节点 -------------------------------------------------------------------------------- /plugins/webgui/public/views/skin/fs_dinosaur/assets/default_100_percent/100-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/public/views/skin/fs_dinosaur/assets/default_100_percent/100-disabled.png -------------------------------------------------------------------------------- /plugins/webgui/public/views/skin/fs_dinosaur/assets/default_200_percent/200-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/public/views/skin/fs_dinosaur/assets/default_200_percent/200-disabled.png -------------------------------------------------------------------------------- /plugins/webgui/public/views/skin/fs_dinosaur/assets/default_100_percent/100-error-offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/public/views/skin/fs_dinosaur/assets/default_100_percent/100-error-offline.png -------------------------------------------------------------------------------- /plugins/webgui/public/views/skin/fs_dinosaur/assets/default_200_percent/200-error-offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/public/views/skin/fs_dinosaur/assets/default_200_percent/200-error-offline.png -------------------------------------------------------------------------------- /plugins/webgui/public/views/skin/fs_dinosaur/assets/default_100_percent/100-offline-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/public/views/skin/fs_dinosaur/assets/default_100_percent/100-offline-sprite.png -------------------------------------------------------------------------------- /plugins/webgui/public/views/skin/fs_dinosaur/assets/default_200_percent/200-offline-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/shadowsocks-manager/master/plugins/webgui/public/views/skin/fs_dinosaur/assets/default_200_percent/200-offline-sprite.png -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | ![logo](/_media/favicon.png ':size=125x125') 2 | 3 |
4 | 5 | # Shadowsocks Manager 6 | 7 | [GitHub](https://github.com/shadowsocks/shadowsocks-manager/) 8 | [Docs](/home) 9 | 10 | ![color](#FAFAFA) -------------------------------------------------------------------------------- /docs/managerapi.md: -------------------------------------------------------------------------------- 1 | # manager API 2 | 3 | shadowsocks 的 manager API 可以[参考官方文档](https://github.com/shadowsocks/shadowsocks/wiki/Manage-Multiple-Users),采用`UDP`协议,指令比较简单,以 4 | 5 | `command[: JSON data]` 6 | 7 | 为格式发送指令,可添加端口、删除端口、返回流量信息。由于这个接口设计过于简单,所以需要运行一个前置系统用于更加复杂、容错的设计。 -------------------------------------------------------------------------------- /bin/ssmgr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | process.env.NODE_ENV = 'production'; 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const lib = path.join(path.dirname(fs.realpathSync(__filename)), '../'); 6 | console.log(lib); 7 | process.chdir(lib); 8 | require(lib + '/server'); 9 | -------------------------------------------------------------------------------- /docs/vps.md: -------------------------------------------------------------------------------- 1 | # VPS推荐 2 | 3 | * [Vultr](https://www.vultr.com/?ref=6926595) 4 | 5 | * [Linode](https://www.linode.com/?r=bbc24323b3adaf3d74f242fd958d91b55cc6fdea) 6 | 7 | * [DigitalOcean](https://m.do.co/c/d43891b79a52) 8 | 9 | * [BandwagonHost](https://bandwagonhost.com/aff.php?aff=19999) -------------------------------------------------------------------------------- /config/default.yml: -------------------------------------------------------------------------------- 1 | type: s 2 | 3 | shadowsocks: 4 | address: 127.0.0.1:6001 5 | 6 | manager: 7 | address: 0.0.0.0:6002 8 | password: '123456' 9 | 10 | db: 'db.sqlite' 11 | 12 | # db: 13 | # host: '1.1.1.1' 14 | # user: 'root' 15 | # password: 'abcdefg' 16 | # database: 'ssmgr' 17 | -------------------------------------------------------------------------------- /init/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | global.appRequire = (filePath) => { 4 | return require(path.resolve(__dirname, '../' + filePath)); 5 | }; 6 | 7 | global.appFork = filePath => { 8 | const child = require('child_process'); 9 | return child.fork(path.resolve(__dirname, '../' + filePath)); 10 | }; -------------------------------------------------------------------------------- /plugins/webgui/public/filters/substr.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | 3 | app.filter('substr', function() { 4 | return function(input, number = 20) { 5 | if(input.toString().length > number) { 6 | return input.toString().substr(0, number) + '...'; 7 | } 8 | return input; 9 | }; 10 | }); 11 | -------------------------------------------------------------------------------- /plugins/webgui/public/filters/mac.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | 3 | app.filter('mac', function() { 4 | return function(mac) { 5 | return mac.toUpperCase().split('').map((m, index, array) => { 6 | if(index % 2 === 0) { 7 | return m + array[index + 1]; 8 | } 9 | }).filter(f => f).join(':'); 10 | }; 11 | }); 12 | -------------------------------------------------------------------------------- /plugins/webgui/public/configs/urlRouter.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | 3 | app.config(['$urlRouterProvider', '$locationProvider', 4 | ($urlRouterProvider, $locationProvider) => { 5 | $locationProvider.html5Mode(true); 6 | $urlRouterProvider 7 | .when('/', '/home/index') 8 | .otherwise('/home/index') 9 | ; 10 | } 11 | ]); -------------------------------------------------------------------------------- /plugins/webgui/public/filters/giftcard.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | 3 | app.filter('prettyPrintBatchStatus', function () { 4 | return function (status) { 5 | const result = { 6 | AVAILABLE: '可用', 7 | USEDUP: '售罄', 8 | REVOKED: '召回' 9 | }; 10 | return result[status] || '其它'; 11 | }; 12 | }); -------------------------------------------------------------------------------- /plugins/webgui/public/services/configService.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | 3 | app.factory('configManager', [() => { 4 | let config = {}; 5 | const setConfig = data => { 6 | config = data; 7 | }; 8 | const getConfig = () => { 9 | return config; 10 | }; 11 | const deleteConfig = () => { 12 | config = {}; 13 | }; 14 | return { setConfig, getConfig, deleteConfig }; 15 | }]); -------------------------------------------------------------------------------- /init/loadModels.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const init = async () => { 4 | const files = fs.readdirSync(path.resolve(__dirname, '../models')); 5 | if(!files) { 6 | return Promise.reject('load models error'); 7 | } 8 | for (let file of files) { 9 | await appRequire('models/' + file).createTable(); 10 | } 11 | return; 12 | }; 13 | 14 | exports.init = init; 15 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/home/index.html: -------------------------------------------------------------------------------- 1 |
2 |
4 |
5 |
6 |
{{ config.version }}
-------------------------------------------------------------------------------- /init/loadServices.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const config = appRequire('services/config').all(); 3 | 4 | const shadowsocks = () => { 5 | appRequire('services/shadowsocks'); 6 | appRequire('services/server'); 7 | }; 8 | const manager = () => { 9 | appRequire('services/manager'); 10 | }; 11 | if(config.type === 's') { 12 | shadowsocks(); 13 | } else if (config.type === 'm') { 14 | manager(); 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": false 8 | } 9 | }, 10 | "rules": { 11 | "semi": 2, 12 | "no-unreachable": 2, 13 | "eol-last": 2, 14 | "no-console": 1, 15 | "linebreak-style": 1 16 | } 17 | } -------------------------------------------------------------------------------- /docs/skin.md: -------------------------------------------------------------------------------- 1 | # 首页皮肤 2 | 3 | ## 使用自带皮肤 4 | 5 | 在 webgui 的配置里增加`skin`字段即可使用首页皮肤 6 | 7 | ```yaml 8 | plugins: 9 | webgui: 10 | skin: 'default' 11 | ``` 12 | 13 | 目前可以填的值有: 14 | 15 | - default 16 | - bing 17 | - fs_bing 18 | - fs_dinosaur 19 | - fs_sample 20 | - fs_zelda 21 | 22 | ## 创建自定义皮肤 23 | 24 | - 在`/plugin/webgui/public/views/skin`目录下创建自己的皮肤文件`yourskin.html` 25 | - 更改配置文件填上皮肤名称 26 | 27 | !> 若皮肤名称以 fs_ 开头,则首页会隐藏顶部的导航条 -------------------------------------------------------------------------------- /models/account.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'account'; 3 | 4 | const createTable = async() => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | return; 8 | } 9 | return knex.schema.createTable(tableName, function(table) { 10 | table.integer('port').primary(); 11 | table.string('password'); 12 | }); 13 | }; 14 | 15 | exports.createTable = createTable; 16 | -------------------------------------------------------------------------------- /models/command.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'command'; 3 | 4 | const createTable = async() => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | return; 8 | } 9 | return knex.schema.createTable(tableName, function(table) { 10 | table.string('code').primary(); 11 | table.bigInteger('time'); 12 | }); 13 | }; 14 | 15 | exports.createTable = createTable; 16 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [首页](/home) 2 | 3 | * 入门 4 | * [安装](/install) 5 | * [配置](/config) 6 | 7 | * 进阶 8 | * [高级配置](/advanced) 9 | * [支付](/pay) 10 | * [首页皮肤](/skin) 11 | 12 | * 使用 13 | * [管理员](/admin) 14 | * [普通用户](/user) 15 | 16 | * 升级与迁移 17 | * [升级](/upgrade) 18 | * [迁移](/transfer) 19 | 20 | * 原理 21 | * [拓扑图](/topological) 22 | * [manager API](/managerapi) 23 | * [ssmgr API](/ssmgrapi) 24 | 25 | * [VPS推荐](/vps) 26 | * [捐赠](/donate) -------------------------------------------------------------------------------- /models/flow.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'flow'; 3 | 4 | const createTable = async() => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | return; 8 | } 9 | return knex.schema.createTable(tableName, function(table) { 10 | table.integer('port'); 11 | table.integer('flow'); 12 | table.bigInteger('time'); 13 | }); 14 | }; 15 | 16 | exports.createTable = createTable; 17 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/mailSetting.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | {{ mail.name }} 8 |
9 | 10 |
11 |
12 |
13 |
-------------------------------------------------------------------------------- /docs/home.md: -------------------------------------------------------------------------------- 1 | # 首页 2 | 3 | Shadowsocks-Manager 是一个基于`Node.js`开发的 shadowsocks 多用户管理平台,支持 libev 和 python 版 4 | 5 | ## Demo {docsify-ignore} 6 | 7 | * [ShadowGhost](https://ssmgr.gyteng.com) 8 | * [FreeAccount](https://free.gyteng.com) 9 | 10 | ## Telegram {docsify-ignore} 11 | 12 | * [交流群](https://t.me/ssmgr) 13 | * [付费技术支持](https://t.me/gyteng) 14 | 15 | ## License {docsify-ignore} 16 | 17 | * [GPLv3](https://github.com/shadowsocks/shadowsocks-manager/blob/master/LICENSE) -------------------------------------------------------------------------------- /plugins/telegram/db/telegram.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'telegram'; 3 | 4 | const config = appRequire('services/config').all(); 5 | const createTable = async() => { 6 | if(config.empty) { 7 | await knex.schema.dropTableIfExists(tableName); 8 | } 9 | return knex.schema.createTable(tableName, function(table) { 10 | table.string('key'); 11 | table.string('value'); 12 | }); 13 | }; 14 | 15 | exports.createTable = createTable; 16 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/user/macAddress.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
{{ account.mac | mac }}
7 |
8 |
9 |
10 |
11 |
-------------------------------------------------------------------------------- /plugins/webgui_telegram/db/webgui_telegram.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'webgui_telegram'; 3 | 4 | const config = appRequire('services/config').all(); 5 | const createTable = async() => { 6 | const exist = await knex.schema.hasTable(tableName); 7 | if(exist) { return; } 8 | return knex.schema.createTable(tableName, function(table) { 9 | table.string('key'); 10 | table.string('value'); 11 | }); 12 | }; 13 | 14 | exports.createTable = createTable; -------------------------------------------------------------------------------- /plugins/webgui/db/setting.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'webguiSetting'; 3 | 4 | const createTable = async() => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | return; 8 | } 9 | return knex.schema.createTable(tableName, function(table) { 10 | table.increments('id').primary(); 11 | table.string('key').unique(); 12 | table.string('value', 16384); 13 | }); 14 | }; 15 | 16 | exports.createTable = createTable; 17 | -------------------------------------------------------------------------------- /plugins/webgui_order/flowPack.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const account = appRequire('plugins/account/index'); 3 | 4 | const getFlowPack = async (accountId, start, end) => { 5 | const flowPacks = await knex('webgui_flow_pack').where({ accountId }).whereBetween('createTime', [ start, end ]); 6 | if(!flowPacks.length) { return 0; } 7 | return flowPacks.reduce((a, b) => { 8 | return { flow: a.flow + b.flow }; 9 | }, { flow: 0 }).flow; 10 | }; 11 | 12 | exports.getFlowPack = getFlowPack; -------------------------------------------------------------------------------- /plugins/webgui_ref/db/webgui_ref.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'webgui_ref'; 3 | 4 | const createTable = async () => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { return; } 7 | return knex.schema.createTable(tableName, function(table) { 8 | table.increments('id').primary(); 9 | table.integer('codeId'); 10 | table.integer('userId'); 11 | table.bigInteger('time'); 12 | }); 13 | }; 14 | 15 | exports.createTable = createTable; 16 | -------------------------------------------------------------------------------- /plugins/freeAccount/db/freeAccount.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'freeAccount'; 3 | 4 | const config = appRequire('services/config').all(); 5 | const createTable = async() => { 6 | const exist = await knex.schema.hasTable(tableName); 7 | if(exist) { 8 | return; 9 | } 10 | return knex.schema.createTable(tableName, function(table) { 11 | table.string('key').primary(); 12 | table.string('value'); 13 | }); 14 | }; 15 | 16 | exports.createTable = createTable; 17 | -------------------------------------------------------------------------------- /init/cron.js: -------------------------------------------------------------------------------- 1 | const later = require('later'); 2 | later.date.localTime(); 3 | 4 | const minute = function(fn, time = 1) { 5 | later.setInterval(fn, later.parse.text(`every ${ time } mins`)); 6 | }; 7 | 8 | const second = function(fn, time = 10) { 9 | later.setInterval(fn, later.parse.text(`every ${ time } seconds`)); 10 | }; 11 | 12 | const cron = function(fn, cronString) { 13 | later.setInterval(fn, later.parse.cron(cronString)); 14 | }; 15 | 16 | exports.minute = minute; 17 | exports.second = second; 18 | exports.cron = cron; -------------------------------------------------------------------------------- /plugins/webgui_order/db/webgui_flow_pack.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'webgui_flow_pack'; 3 | 4 | const createTable = async () => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { return; } 7 | return knex.schema.createTable(tableName, function(table) { 8 | table.increments('id').primary(); 9 | table.integer('accountId'); 10 | table.bigInteger('flow'); 11 | table.bigInteger('createTime'); 12 | }); 13 | }; 14 | 15 | exports.createTable = createTable; -------------------------------------------------------------------------------- /plugins/macAccount/db/macAccount.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'mac_account'; 3 | 4 | const createTable = async() => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { return; } 7 | return knex.schema.createTable(tableName, function(table) { 8 | table.increments('id'); 9 | table.string('mac').unique(); 10 | table.integer('userId'); 11 | table.integer('accountId'); 12 | table.integer('serverId'); 13 | }); 14 | }; 15 | 16 | exports.createTable = createTable; 17 | -------------------------------------------------------------------------------- /plugins/telegram/db/flow.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'saveFlow'; 3 | 4 | const createTable = async () => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | return; 8 | } 9 | return knex.schema.createTable(tableName, function(table) { 10 | table.integer('id'); 11 | table.integer('port'); 12 | table.bigInteger('flow'); 13 | table.bigInteger('time'); 14 | table.index(['time', 'port'], 'index'); 15 | }); 16 | }; 17 | 18 | exports.createTable = createTable; 19 | -------------------------------------------------------------------------------- /plugins/webgui/public/translate/index.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | app.config(['$translateProvider', $translateProvider => { 3 | $translateProvider.translations('en-US', require('./en-US.js')); 4 | $translateProvider.translations('zh-CN', require('./zh-CN.js')); 5 | $translateProvider.translations('ja-JP', require('./ja-JP.js')); 6 | $translateProvider.translations('ru-RU', require('./ru-RU.js')); 7 | $translateProvider.preferredLanguage(navigator.language || 'zh-CN'); 8 | $translateProvider.useSanitizeValueStrategy('escape'); 9 | }]); 10 | 11 | -------------------------------------------------------------------------------- /plugins/webgui_telegram/admin.js: -------------------------------------------------------------------------------- 1 | const telegram = appRequire('plugins/webgui_telegram/index').telegram; 2 | const knex = appRequire('init/knex').knex; 3 | 4 | const getAdmin = async () => { 5 | const exists = await knex('user').where({ 6 | type: 'admin' 7 | }).then(success => success[0]); 8 | if(!exists || !exists.telegram) { 9 | return; 10 | } 11 | return exists.telegram; 12 | }; 13 | 14 | const push = async (message) => { 15 | const telegramId = await getAdmin(); 16 | telegramId && telegram.emit('send', +telegramId, message); 17 | }; 18 | 19 | exports.push = push; -------------------------------------------------------------------------------- /plugins/telegram/managerAddress.js: -------------------------------------------------------------------------------- 1 | const config = appRequire('services/config').all(); 2 | 3 | let managerAddress = { 4 | host: config.manager.address.split(':')[0], 5 | port: +config.manager.address.split(':')[1], 6 | password: config.manager.password, 7 | }; 8 | 9 | const setManagerAddress = (host, port, password) => { 10 | managerAddress.host = host; 11 | managerAddress.port = port; 12 | managerAddress.password = password; 13 | }; 14 | 15 | const getManagerAddress = () => { 16 | return managerAddress; 17 | }; 18 | 19 | exports.set = setManagerAddress; 20 | exports.get = getManagerAddress; 21 | -------------------------------------------------------------------------------- /plugins/webgui_autoban/README.md: -------------------------------------------------------------------------------- 1 | # webgui_autoban plugin 2 | 3 | ``` 4 | type: m 5 | manager: 6 | address: 1.2.3.4:5678 7 | password: 'pwd' 8 | 9 | plugins: 10 | webgui_autoban: 11 | use: true 12 | speed: 10 13 | data: 14 | - accountId: '1,2,3-10,20,50-60' 15 | serverId: '1,2-5,11,19' 16 | time: 1800000 17 | flow: 100000000 18 | banTime: 600000 19 | - accountId: '30' 20 | serverId: '40' 21 | time: '30m' 22 | flow: '0.5g' 23 | banTime: '10m' 24 | 25 | db: 26 | host: '1.2.3.4' 27 | user: 'u' 28 | password: 'pwd' 29 | database: 'ssmgr' 30 | ``` -------------------------------------------------------------------------------- /plugins/webgui_ref/db/webgui_ref_code.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'webgui_ref_code'; 3 | 4 | const createTable = async () => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { return; } 7 | return knex.schema.createTable(tableName, function(table) { 8 | table.increments('id').primary(); 9 | table.string('code').unique(); 10 | table.integer('sourceUserId'); 11 | table.integer('visit').defaultTo(0); 12 | table.integer('maxUser').defaultTo(3); 13 | table.bigInteger('time'); 14 | }); 15 | }; 16 | 17 | exports.createTable = createTable; -------------------------------------------------------------------------------- /plugins/webgui/public/filters/ban.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | 3 | app.filter('ban', function() { 4 | return function(input) { 5 | if(input <= 30) { 6 | return input; 7 | } else if (input === 35) { 8 | return 45; 9 | } else if (input === 40) { 10 | return 60; 11 | } else if (input === 45) { 12 | return 75; 13 | } else if (input === 50) { 14 | return 90; 15 | } else if (input === 55) { 16 | return 120; 17 | } else if (input === 60) { 18 | return 180; 19 | } else if (input === 65) { 20 | return 240; 21 | } else { 22 | return input; 23 | } 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /plugins/webgui_ref/db/webgui_ref_time.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'webgui_ref_time'; 3 | 4 | const createTable = async () => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { return; } 7 | return knex.schema.createTable(tableName, function(table) { 8 | table.increments('id').primary(); 9 | table.string('orderId').unique(); 10 | table.integer('user'); 11 | table.integer('refUser'); 12 | table.integer('account'); 13 | table.string('status'); 14 | table.bigInteger('refTime'); 15 | table.bigInteger('createTime'); 16 | }); 17 | }; 18 | 19 | exports.createTable = createTable; -------------------------------------------------------------------------------- /docker/ubuntu/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | MAINTAINER gyteng 3 | 4 | RUN apt-get update && \ 5 | export DEBIAN_FRONTEND=noninteractive && \ 6 | apt-get install tzdata iproute2 curl git sudo software-properties-common python-pip -y && \ 7 | pip install git+https://github.com/gyteng/shadowsocks.git@master && \ 8 | curl -sL https://deb.nodesource.com/setup_8.x | bash - && \ 9 | apt-get install -y nodejs shadowsocks-libev && \ 10 | npm i -g shadowsocks-manager --unsafe-perm && \ 11 | echo "Asia/Shanghai" > /etc/timezone && \ 12 | rm /etc/localtime && \ 13 | dpkg-reconfigure -f noninteractive tzdata 14 | 15 | CMD ["/usr/bin/ssmgr"] -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | Please answer these questions before submitting your issue. Thanks! 2 | 3 | (Please mention that if the issue you filed is solved, you may wish to close it by yourself. Thanks again.) 4 | 5 | (PS, you can remove 3 lines above, including this one, before post your issue.) 6 | 7 | ### What version of shadowsocks-manager are you using? 8 | 9 | 10 | ### What operating system are you using? 11 | 12 | 13 | ### What version of Node.js are you using? 14 | 15 | 16 | ### What did you do? 17 | 18 | 19 | ### What did you expect to see? 20 | 21 | 22 | ### What did you see instead? 23 | 24 | 25 | ### What is your config in detail (with all sensitive info masked)? 26 | -------------------------------------------------------------------------------- /plugins/cli/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | appRequire('plugins/cli/menu/main'); 4 | 5 | const config = appRequire('services/config').all(); 6 | 7 | let managerAddress = { 8 | host: config.manager.address.split(':')[0], 9 | port: +config.manager.address.split(':')[1], 10 | password: config.manager.password, 11 | }; 12 | 13 | const setManagerAddress = (host, port, password) => { 14 | managerAddress.host = host; 15 | managerAddress.port = port; 16 | managerAddress.password = password; 17 | }; 18 | 19 | const getManagerAddress = () => { 20 | return managerAddress; 21 | }; 22 | 23 | exports.setManagerAddress = setManagerAddress; 24 | exports.getManagerAddress = getManagerAddress; 25 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/previewNotice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

{{publicInfo.title}}

5 | 6 | 7 | close 8 | 9 |
10 |
11 | 12 |
13 |
14 |
15 |
16 |
-------------------------------------------------------------------------------- /plugins/webgui/libs/ngclipboard.min.js: -------------------------------------------------------------------------------- 1 | /*! ngclipboard - v2.0.0 - 2018-03-03 2 | * https://github.com/sachinchoolur/ngclipboard 3 | * Copyright (c) 2018 Sachin; Licensed MIT */ 4 | !function(){"use strict";var a,b,c="ngclipboard";"object"==typeof module&&module.exports?(a=require("angular"),b=require("clipboard"),module.exports=c):(a=window.angular,b=window.ClipboardJS),a.module(c,[]).directive("ngclipboard",function(){return{restrict:"A",scope:{ngclipboardSuccess:"&",ngclipboardError:"&"},link:function(a,c){var d=new b(c[0]);d.on("success",function(b){a.$apply(function(){a.ngclipboardSuccess({e:b})})}),d.on("error",function(b){a.$apply(function(){a.ngclipboardError({e:b})})}),c.on("$destroy",function(){d.destroy()})}}})}(); -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # 安装 2 | 3 | ## 普通方式 4 | 5 | 1. 安装 shadowsocks 6 | 7 | 可以采用`libev`或`python`版本 8 | 9 | 2. 安装 Node.js 8.x 10 | 11 | 建议使用[`nodesource`](https://github.com/nodesource/distributions)里边的方式安装 12 | 13 | 3. 安装 ssmgr 14 | 15 | ```shell 16 | npm i -g shadowsocks-manager 17 | ``` 18 | 19 | 若出现权限相关的错误提示,则需要尝试: 20 | 21 | ```shell 22 | sudo npm i -g shadowsocks-manager --unsafe-perm 23 | ``` 24 | 25 | 安装完成后,使用`ssmgr`命令来运行程序 26 | 27 | ## Docker 方式 28 | 29 | 1. 安装 Docker 30 | 31 | 参见[Docker官网](https://docs.docker.com/install/)。 32 | 33 | 2. 运行 34 | 35 | ```shell 36 | docker run --name ssmgr -idt --net=host \ 37 | -v ~/.ssmgr:/root/.ssmgr \ 38 | gyteng/ssmgr \ 39 | ssmgr -c /your/config/file 40 | ``` -------------------------------------------------------------------------------- /plugins/alipay/db/alipay.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'alipay'; 3 | 4 | const createTable = async () => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { return; } 7 | return knex.schema.createTable(tableName, function(table) { 8 | table.increments('id').primary(); 9 | table.string('orderId').unique(); 10 | table.integer('orderType'); 11 | table.string('amount'); 12 | table.integer('user'); 13 | table.integer('account'); 14 | table.string('qrcode'); 15 | table.string('status'); 16 | table.string('alipayData', 4096); 17 | table.bigInteger('createTime'); 18 | table.bigInteger('expireTime'); 19 | }); 20 | }; 21 | 22 | exports.createTable = createTable; 23 | -------------------------------------------------------------------------------- /plugins/telegram/help.js: -------------------------------------------------------------------------------- 1 | const telegram = appRequire('plugins/telegram/index').telegram; 2 | 3 | telegram.on('message', message => { 4 | if (message.message.text === 'help') { 5 | let str = ''; 6 | str += `Command: 7 | 8 | auth 9 | 10 | list 11 | add 12 | del {port} 13 | add {port} {password} 14 | pwd {port} {password} 15 | 16 | listserver 17 | switchserver {id} 18 | delserver {name} 19 | addserver {name} {host} {port} {password} 20 | editserver {name} {newName} {host} {port} {password} 21 | 22 | flow 23 | flow{number}min 24 | flow{number}hour 25 | 26 | Read more info at https://github.com/shadowsocks/shadowsocks-manager/tree/master/plugins/telegram/README.md 27 | `; 28 | telegram.emit('send', message, str); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /plugins/paypal/db/paypal.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'paypal'; 3 | 4 | const createTable = async () => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { return; } 7 | return knex.schema.createTable(tableName, function(table) { 8 | table.increments('id').primary(); 9 | table.string('orderId').unique(); 10 | table.integer('orderType'); 11 | table.string('amount'); 12 | table.integer('user'); 13 | table.integer('account'); 14 | table.string('paypalId').unique(); 15 | table.string('status'); 16 | table.string('paypalData', 4096); 17 | table.bigInteger('createTime'); 18 | table.bigInteger('expireTime'); 19 | }); 20 | }; 21 | 22 | exports.createTable = createTable; 23 | -------------------------------------------------------------------------------- /init/knex.js: -------------------------------------------------------------------------------- 1 | const config = appRequire('services/config').get('db'); 2 | 3 | let knex; 4 | if(typeof config === 'object') { 5 | const { host, user, password, database, port } = config; 6 | knex = require('knex')({ 7 | client: 'mysql', 8 | connection: { 9 | host, 10 | user, 11 | port, 12 | password, 13 | database, 14 | charset: 'utf8', 15 | collate: 'utf8_unicode_ci', 16 | }, 17 | useNullAsDefault: true, 18 | pool: { min: 2, max: 10 }, 19 | acquireConnectionTimeout: 120 * 1000, 20 | }); 21 | } else { 22 | knex = require('knex')({ 23 | client: 'sqlite3', 24 | connection: { 25 | filename: config, 26 | }, 27 | useNullAsDefault: true, 28 | }); 29 | } 30 | 31 | 32 | exports.knex = knex; 33 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/orderSetting.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
{{ order.name }}
7 |
{{ order.accountNumber }}
8 |
9 |
10 |
11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/dialog/autopop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

{{ publicInfo.notices[publicInfo.index].title }}

5 | 6 | 7 | {{ publicInfo.index < publicInfo.notices.length - 1 ? 'keyboard_arrow_right' : 'close' }} 8 | 9 |
10 |
11 | 12 |
13 |
14 |
15 |
16 |
-------------------------------------------------------------------------------- /plugins/webgui/db/push.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'push'; 3 | 4 | const createTable = async() => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | const hasUserId = await knex.schema.hasColumn(tableName, 'userId'); 8 | if(!hasUserId) { 9 | await knex.schema.table(tableName, function(table) { 10 | table.integer('userId').defaultTo(1); 11 | }); 12 | } 13 | return; 14 | } 15 | return knex.schema.createTable(tableName, function(table) { 16 | table.increments('id').primary(); 17 | table.integer('userId').defaultTo(1); 18 | table.string('endpoint').unique(); 19 | table.string('auth'); 20 | table.string('p256dh'); 21 | }); 22 | }; 23 | 24 | exports.createTable = createTable; 25 | -------------------------------------------------------------------------------- /plugins/giftcard/db/giftcard.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'giftcard'; 3 | 4 | const createTable = async () => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { return; } 7 | return knex.schema.createTable(tableName, function(table) { 8 | table.increments('id').primary(); 9 | table.string('password').unique().notNull(); 10 | table.integer('orderType').notNull(); 11 | table.string('status').notNull(); 12 | table.integer('batchNumber').notNull(); 13 | table.integer('user'); 14 | table.integer('account'); 15 | table.bigInteger('createTime').notNull(); 16 | table.bigInteger('usedTime'); 17 | table.string('comment').defaultTo(''); 18 | }); 19 | }; 20 | 21 | exports.createTable = createTable; 22 | exports.tableName = tableName; 23 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/pickAccount.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |
6 | 暂无可分配的账号 7 |
8 |
9 |
10 | 11 | {{ a.port }} 12 | 13 |
14 |
15 |
16 |
17 | 18 | 19 | 确定 20 | 21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/groupList.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | {{ group.name }}
{{ group.comment }} 8 |
9 |
10 |
{{ group.userNumber }}
11 |
12 |
13 |
14 |
15 |
16 |
-------------------------------------------------------------------------------- /plugins/webgui/public/views/skin/bing.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 登录 7 | 8 | 9 | 注册 10 | 11 |
12 |
13 | Fork me on GitHub -------------------------------------------------------------------------------- /init/moveConfigFile.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const fs = require('fs'); 3 | const fse = require('fs-extra'); 4 | const path = require('path'); 5 | const ssmgrPath = path.resolve(os.homedir(), './.ssmgr/'); 6 | 7 | const configFiles = [ 8 | 'default.yml', 9 | ]; 10 | 11 | const log4js = require('log4js'); 12 | const logger = log4js.getLogger('system'); 13 | 14 | try { 15 | fs.statSync(ssmgrPath); 16 | } catch(err) { 17 | logger.info('~/.ssmgr/ not found, make dir for it.'); 18 | fs.mkdirSync(ssmgrPath); 19 | } 20 | configFiles.forEach(configFile => { 21 | try { 22 | fs.statSync(path.resolve(ssmgrPath, configFile)); 23 | } catch(err) { 24 | logger.info(`~/.ssmgr/${ configFile } not found, make file for it.`); 25 | fse.copySync(path.resolve(`./config/${ configFile }`), path.resolve(ssmgrPath, configFile)); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /plugins/webgui/db/notice.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'notice'; 3 | 4 | const createTable = async() => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | const hasAutopop = await knex.schema.hasColumn(tableName, 'autopop'); 8 | if(!hasAutopop) { 9 | await knex.schema.table(tableName, function(table) { 10 | table.integer('autopop').defaultTo(0); 11 | }); 12 | } 13 | return; 14 | } 15 | return knex.schema.createTable(tableName, function(table) { 16 | table.increments('id').primary(); 17 | table.string('title'); 18 | table.string('content', 16384); 19 | table.bigInteger('time'); 20 | table.integer('group').defaultTo(0); 21 | table.integer('autopop').defaultTo(0); 22 | }); 23 | }; 24 | 25 | exports.createTable = createTable; 26 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/dialog/showWireGuardConfig.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

{{ server.name }}

5 | 6 | 7 | close 8 | 9 |
10 |
11 | 12 |
13 | 14 | 15 | 16 | 17 |
18 |
19 |
-------------------------------------------------------------------------------- /plugins/webgui/public/views/dialog/language.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
{{ '请选择语言:' | translate }}
6 | 7 | 8 | 9 | {{ language.name }} 10 | 11 | 12 | 13 |
14 |
15 |
16 |
-------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('./init/log'); 2 | 3 | const log4js = require('log4js'); 4 | const logger = log4js.getLogger('system'); 5 | 6 | logger.info('System start.'); 7 | 8 | process.on('unhandledRejection', (reason, p) => { 9 | logger.error('Unhandled Rejection at: Promise', p, 'reason:', reason); 10 | }); 11 | 12 | process.on('uncaughtException', (err) => { 13 | logger.error(`Caught exception:`); 14 | logger.error(err); 15 | }); 16 | 17 | require('./init/utils'); 18 | 19 | require('./init/moveConfigFile'); 20 | require('./init/checkConfig'); 21 | require('./init/knex'); 22 | 23 | const initDb = require('./init/loadModels').init; 24 | 25 | initDb().then(() => { 26 | return require('./init/runShadowsocks').run(); 27 | }).then(() => { 28 | require('./init/loadServices'); 29 | require('./init/loadPlugins'); 30 | }).catch(err => { 31 | logger.error(err); 32 | }); 33 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/paymentList.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | {{ t.name }} 7 | 8 | 9 | 10 | 11 |
12 |
13 | 编辑>> 14 |
15 |
16 |
17 |
18 |
19 |
20 |
-------------------------------------------------------------------------------- /plugins/webgui/public/views/dialog/alert.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
7 |
8 |
{{ publicInfo.content | translate }}
9 |
10 |
11 |
12 | 13 | 14 | {{ publicInfo.button | translate }} 15 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /plugins/email/db/email.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'email'; 3 | 4 | const createTable = async () => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | const hasTelegramId = await knex.schema.hasColumn(tableName, 'telegramId'); 8 | if(!hasTelegramId) { 9 | await knex.schema.table(tableName, function(table) { 10 | table.string('telegramId'); 11 | }); 12 | } 13 | return; 14 | } 15 | return knex.schema.createTable(tableName, function(table) { 16 | table.string('to'); 17 | table.string('subject'); 18 | table.string('text', 16384); 19 | table.string('type'); 20 | table.string('remark'); 21 | table.string('ip'); 22 | table.string('session'); 23 | table.string('telegramId'); 24 | table.bigInteger('time'); 25 | }); 26 | }; 27 | 28 | exports.createTable = createTable; 29 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/notice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 8 | 9 |
10 | {{ notice.title }} 11 |
12 |
13 | {{ notice.groupName }} 14 |
15 | 16 |
17 |
18 |
19 |
-------------------------------------------------------------------------------- /plugins/webgui/public/services/websocketService.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | 3 | app.factory('ws', ['$websocket', '$location', '$timeout', ($websocket, $location, $timeout) => { 4 | const protocol = $location.protocol() === 'http' ? 'ws://' : 'wss://'; 5 | const url = protocol + $location.host() + ':' + $location.port() + '/user'; 6 | let connection = null; 7 | const messages = []; 8 | const connect = () => { 9 | connection = $websocket(url); 10 | connection.onMessage(function(message) { 11 | console.log(message.data); 12 | messages.push(message.data); 13 | }); 14 | connection.onClose(() => { 15 | $timeout(() => { 16 | connect(); 17 | }, 3000); 18 | }); 19 | }; 20 | connect(); 21 | const methods = { 22 | messages, 23 | send: function (msg) { 24 | connection.send(msg); 25 | }, 26 | }; 27 | return methods; 28 | }]); 29 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/dialog/serverChart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
显示流量
6 |
7 |
8 |
9 |
显示图表
10 |
11 |
12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /plugins/webgui/views/manifest.js: -------------------------------------------------------------------------------- 1 | let gcmSenderId = appRequire('services/config').get('plugins.webgui.gcmSenderId'); 2 | if(gcmSenderId) { gcmSenderId = gcmSenderId.toString(); } 3 | 4 | const manifest = { 5 | short_name: 'ssmgr', 6 | name: 'Shadowsocks-Manager', 7 | icons: [ 8 | { 9 | src: '/favicon.png', 10 | type: 'image/png', 11 | sizes: '48x48' 12 | }, 13 | { 14 | src: '/favicon.png', 15 | type: 'image/png', 16 | sizes: '128x128' 17 | }, 18 | { 19 | src: '/favicon.png', 20 | type: 'image/png', 21 | sizes: '144x144' 22 | }, 23 | { 24 | src: '/favicon.png', 25 | type: 'image/png', 26 | sizes: '256x256' 27 | } 28 | ], 29 | start_url: '/', 30 | display: 'standalone', 31 | background_color: '#2196F3', 32 | theme_color: '#2196F3', 33 | gcm_sender_id: gcmSenderId, 34 | }; 35 | 36 | exports.manifest = manifest; 37 | -------------------------------------------------------------------------------- /docs/pay.md: -------------------------------------------------------------------------------- 1 | # 支付 2 | 3 | ## 支付宝 4 | 5 | 1. 申请当面付接口 6 | 7 | 成功申请后需要到蚂蚁金服开放平台中“PID和公钥管理”的开放平台密钥对你生成的应用进行密钥设置,格式必须为“RSA2(SHA256)” 8 | 9 | 2. 生成密钥 10 | 11 | 建议使用支付宝提供的“RSA签名验签工具”生成应用密钥,格式请选择**PKCS1(非JAVA适用)** 12 | 13 | 3. 添加相应配置 14 | 15 | ```yaml 16 | plugins: 17 | alipay: 18 | use: true 19 | appid: 2015012108272442 20 | notifyUrl: 'http://yourwebsite.com/api/user/alipay/callback' 21 | merchantPrivateKey: '/merchant/private/key' 22 | alipayPublicKey: '/alipay/public/key' 23 | gatewayUrl: 'https://openapi.alipay.com/gateway.do' 24 | ``` 25 | !> `merchantPrivateKey`和`alipayPublicKey`这两个字段内容,可以填写key的路径,也可以填写key的内容 26 | 27 | ## Paypal 28 | 29 | 申请Paypal商家号,添加一个`REST API`,将 id 和 secret 填入配置文件即可 30 | 31 | ```yaml 32 | plugins: 33 | paypal: 34 | use: true 35 | mode: 'live' # sandbox or live 36 | client_id: 'At9xcGd1t5L6OrICKNnp2g9' 37 | client_secret: 'EP40s6pQAZmqp_G_nrU9kKY4XaZph' 38 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | .remote-sync.json 39 | 40 | # config 41 | config/development.yml 42 | 43 | .vscode 44 | lib 45 | plugins/webgui/libs/bundle.js 46 | plugins/webgui/libs/lib.js 47 | plugins/webgui/libs/style.css 48 | *.yml 49 | *.sqlite -------------------------------------------------------------------------------- /plugins/webgui/public/routes/adminUser.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.config(['$stateProvider', $stateProvider => { 6 | $stateProvider 7 | .state('admin.user', { 8 | url: '/user', 9 | controller: 'AdminUserController', 10 | templateUrl: `${ cdn }/public/views/admin/user.html`, 11 | }) 12 | .state('admin.userPage', { 13 | url: '/user/:userId', 14 | controller: 'AdminUserPageController', 15 | templateUrl: `${ cdn }/public/views/admin/userPage.html`, 16 | }) 17 | .state('admin.adminPage', { 18 | url: '/admin/:userId', 19 | controller: 'AdminAdminPageController', 20 | templateUrl: `${ cdn }/public/views/admin/adminPage.html`, 21 | }) 22 | .state('admin.addUser', { 23 | url: '/addUser', 24 | controller: 'AdminAddUserController', 25 | templateUrl: `${ cdn }/public/views/admin/addUser.html`, 26 | }); 27 | }]) 28 | ; -------------------------------------------------------------------------------- /plugins/webgui/public/views/dialog/addMacAccount.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 18 | 19 | 确认 20 | 21 | 22 |
-------------------------------------------------------------------------------- /plugins/webgui/public/configs/themingProvider.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | 4 | const config = window.ssmgrConfig; 5 | app.config(['$mdThemingProvider', $mdThemingProvider => { 6 | const checkColor = color => { 7 | const colors = [ 8 | 'red', 9 | 'pink', 10 | 'purple', 11 | 'deep-purple', 12 | 'indigo', 13 | 'blue', 14 | 'light-blue', 15 | 'cyan', 16 | 'teal', 17 | 'green', 18 | 'light-green', 19 | 'lime', 20 | 'yellow', 21 | 'amber', 22 | 'orange', 23 | 'deep-orange', 24 | 'brown', 25 | 'grey', 26 | 'blue-grey', 27 | ]; 28 | return colors.indexOf(color) >= 0; 29 | }; 30 | checkColor(config.themePrimary) && $mdThemingProvider.theme('default').primaryPalette(config.themePrimary); 31 | checkColor(config.themeAccent) && $mdThemingProvider.theme('default').accentPalette(config.themeAccent); 32 | $mdThemingProvider.alwaysWatchTheme(true); 33 | }]); -------------------------------------------------------------------------------- /plugins/webgui/views/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Error 15 | 16 | 17 | 18 |
19 | 20 |

Something broke!

21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /plugins/webgui/public/services/preloadService.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | 3 | app.factory('preload', ['$http', '$q', 'moment', ($http, $q, moment) => { 4 | const pool = {}; 5 | const clean = () => { 6 | for(const p in pool) { 7 | if(pool[p].expire < Date.now()) { 8 | delete pool[p]; 9 | } 10 | } 11 | }; 12 | const set = (id, promise, time) => { 13 | pool[id] = { 14 | promise: promise(), 15 | expire: Date.now() + time, 16 | }; 17 | }; 18 | const get = (id, promise, time) => { 19 | clean(); 20 | if(pool[id] && !pool[id].promise.$$state.status) { 21 | return pool[id].promise; 22 | } else if (pool[id] && pool[id].data && Date.now() <= pool[id].expire) { 23 | return $q.resolve(pool[id].data); 24 | } else { 25 | set(id, promise, time); 26 | return pool[id].promise.then(success => { 27 | pool[id].data = success; 28 | return success; 29 | }); 30 | } 31 | }; 32 | return { 33 | get, 34 | }; 35 | }]); -------------------------------------------------------------------------------- /plugins/webgui/public/routes/adminServer.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.config(['$stateProvider', $stateProvider => { 6 | $stateProvider 7 | .state('admin.server', { 8 | url: '/server', 9 | controller: 'AdminServerController', 10 | templateUrl: `${ cdn }/public/views/admin/server.html`, 11 | }) 12 | .state('admin.serverPage', { 13 | url: '/server/:serverId', 14 | controller: 'AdminServerPageController', 15 | templateUrl: `${ cdn }/public/views/admin/serverPage.html`, 16 | }) 17 | .state('admin.addServer', { 18 | url: '/addServer', 19 | controller: 'AdminAddServerController', 20 | templateUrl: `${ cdn }/public/views/admin/addServer.html`, 21 | }) 22 | .state('admin.editServer', { 23 | url: '/server/:serverId/edit', 24 | controller: 'AdminEditServerController', 25 | templateUrl: `${ cdn }/public/views/admin/editServer.html`, 26 | }); 27 | }]) 28 | ; -------------------------------------------------------------------------------- /plugins/webgui/public/routes/adminAccount.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.config(['$stateProvider', $stateProvider => { 6 | $stateProvider 7 | .state('admin.account', { 8 | url: '/account', 9 | controller: 'AdminAccountController', 10 | templateUrl: `${ cdn }/public/views/admin/account.html`, 11 | }) 12 | .state('admin.accountPage', { 13 | url: '/account/:accountId', 14 | controller: 'AdminAccountPageController', 15 | templateUrl: `${ cdn }/public/views/admin/accountPage.html`, 16 | }) 17 | .state('admin.addAccount', { 18 | url: '/addAccount', 19 | controller: 'AdminAddAccountController', 20 | templateUrl: `${ cdn }/public/views/admin/addAccount.html`, 21 | }) 22 | .state('admin.editAccount', { 23 | url: '/account/:accountId/edit', 24 | controller: 'AdminEditAccountController', 25 | templateUrl: `${ cdn }/public/views/admin/editAccount.html`, 26 | }); 27 | }]) 28 | ; -------------------------------------------------------------------------------- /docker/alpine/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | LABEL maintainer="gyteng " 3 | 4 | COPY ./shadowsocks-libev /tmp/repo 5 | RUN set -ex \ 6 | && apk add --no-cache --virtual .build-deps \ 7 | autoconf \ 8 | automake \ 9 | build-base \ 10 | c-ares-dev \ 11 | libev-dev \ 12 | libtool \ 13 | libsodium-dev \ 14 | linux-headers \ 15 | mbedtls-dev \ 16 | pcre-dev \ 17 | 18 | && cd /tmp/repo \ 19 | && ./autogen.sh \ 20 | && ./configure --prefix=/usr --disable-documentation \ 21 | && make install \ 22 | && apk del .build-deps \ 23 | 24 | && apk add --no-cache \ 25 | rng-tools \ 26 | $(scanelf --needed --nobanner /usr/bin/ss-* \ 27 | | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ 28 | | sort -u) \ 29 | && rm -rf /tmp/repo 30 | RUN apk --no-cache add tzdata iproute2 && \ 31 | ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ 32 | echo "Asia/Shanghai" > /etc/timezone && \ 33 | npm i -g shadowsocks-manager --unsafe-perm 34 | 35 | CMD ["ssmgr"] -------------------------------------------------------------------------------- /docs/topological.md: -------------------------------------------------------------------------------- 1 | # 拓扑图 2 | 3 | ssmgr 利用 shadowsocks 的 manager API 工作,支持`python`和`libev`版本。假设有 n 台服务器,每台服务器上均需要安装 shadowsocks,然后启动一个 ssmgr,配置文件里的 type 一栏填上 s,代表这是 shadowsocks 的前置系统,然后再开启一个 plugin,即可管理这 n 台服务器上的 shadowsocks 了,如下图所示: 4 | 5 | ``` 6 | +-------------+ +-------------+ +------+ 7 | | Shadowsocks | | Shadowsocks | ... | | 8 | | manager API | | manager API | | | 9 | +-------------+ +-------------+ +------+ 10 | | | | 11 | | | | 12 | +-------------+ +-------------+ +------+ 13 | | ssmgr | | ssmgr | ... | | 14 | | with type s | | with type s | | | 15 | +-------------+ +-------------+ +------+ 16 | | | | 17 | +------------+----+-------- ... ---+ 18 | | 19 | | 20 | +---------------+ 21 | | ssmgr plugins | 22 | | with type m | 23 | +---------------+ 24 | ``` -------------------------------------------------------------------------------- /plugins/flowSaver/db/saveFlow.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'saveFlow'; 3 | 4 | const createTable = async () => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | await knex.schema.table(tableName, function(table) { 8 | table.index('id'); 9 | table.index('accountId'); 10 | }); 11 | const hasColumnAccountId = await knex.schema.hasColumn(tableName, 'accountId'); 12 | if(!hasColumnAccountId) { 13 | await knex.schema.table(tableName, function(table) { 14 | table.integer('accountId').defaultTo(0); 15 | }); 16 | } 17 | return; 18 | } 19 | return knex.schema.createTable(tableName, function(table) { 20 | table.integer('id'); 21 | table.integer('accountId').defaultTo(0); 22 | table.integer('port'); 23 | table.bigInteger('flow'); 24 | table.bigInteger('time'); 25 | table.index(['time', 'port'], 'index'); 26 | table.index('id'); 27 | table.index('accountId'); 28 | }); 29 | }; 30 | 31 | exports.createTable = createTable; 32 | -------------------------------------------------------------------------------- /plugins/flowSaver/db/saveFlow5min.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'saveFlow5min'; 3 | 4 | const createTable = async() => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | await knex.schema.table(tableName, function(table) { 8 | table.index('id'); 9 | table.index('accountId'); 10 | }); 11 | const hasColumnAccountId = await knex.schema.hasColumn(tableName, 'accountId'); 12 | if(!hasColumnAccountId) { 13 | await knex.schema.table(tableName, function(table) { 14 | table.integer('accountId').defaultTo(0); 15 | }); 16 | } 17 | return; 18 | } 19 | return knex.schema.createTable(tableName, function(table) { 20 | table.integer('id'); 21 | table.integer('accountId').defaultTo(0); 22 | table.integer('port'); 23 | table.bigInteger('flow'); 24 | table.bigInteger('time'); 25 | table.index(['time', 'port'], '5minIndex'); 26 | table.index('id'); 27 | table.index('accountId'); 28 | }); 29 | }; 30 | 31 | exports.createTable = createTable; 32 | -------------------------------------------------------------------------------- /plugins/flowSaver/db/saveFlowDay.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'saveFlowDay'; 3 | 4 | const createTable = async() => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | await knex.schema.table(tableName, function(table) { 8 | table.index('id'); 9 | table.index('accountId'); 10 | }); 11 | const hasColumnAccountId = await knex.schema.hasColumn(tableName, 'accountId'); 12 | if(!hasColumnAccountId) { 13 | await knex.schema.table(tableName, function(table) { 14 | table.integer('accountId').defaultTo(0); 15 | }); 16 | } 17 | return; 18 | } 19 | return knex.schema.createTable(tableName, function(table) { 20 | table.integer('id'); 21 | table.integer('accountId').defaultTo(0); 22 | table.integer('port'); 23 | table.bigInteger('flow'); 24 | table.bigInteger('time'); 25 | table.index(['time', 'port'], 'dayIndex'); 26 | table.index('id'); 27 | table.index('accountId'); 28 | }); 29 | }; 30 | 31 | exports.createTable = createTable; 32 | -------------------------------------------------------------------------------- /plugins/flowSaver/db/saveFlowHour.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'saveFlowHour'; 3 | 4 | const createTable = async() => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | await knex.schema.table(tableName, function(table) { 8 | table.index('id'); 9 | table.index('accountId'); 10 | }); 11 | const hasColumnAccountId = await knex.schema.hasColumn(tableName, 'accountId'); 12 | if(!hasColumnAccountId) { 13 | await knex.schema.table(tableName, function(table) { 14 | table.integer('accountId').defaultTo(0); 15 | }); 16 | } 17 | return; 18 | } 19 | return knex.schema.createTable(tableName, function(table) { 20 | table.integer('id'); 21 | table.integer('accountId').defaultTo(0); 22 | table.integer('port'); 23 | table.bigInteger('flow'); 24 | table.bigInteger('time'); 25 | table.index(['time', 'port'], 'hourIndex'); 26 | table.index('id'); 27 | table.index('accountId'); 28 | }); 29 | }; 30 | 31 | exports.createTable = createTable; 32 | -------------------------------------------------------------------------------- /plugins/webgui/public/filters/orderStatus.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | 3 | app.filter('order', function () { 4 | return function (status) { 5 | const result = { 6 | CREATE: '创建', 7 | WAIT_BUYER_PAY: '等待', 8 | TRADE_SUCCESS: '付款', 9 | FINISH: '完成', 10 | USED: '完成', 11 | TRADE_CLOSED: '关闭', 12 | created: '创建', 13 | approved: '付款', 14 | finish: '完成', 15 | closed: '关闭', 16 | }; 17 | return result[status] || '其它'; 18 | }; 19 | }) 20 | .filter('prettyOrderId', function () { 21 | return function (id) { 22 | return `${id.substr(0, 4)}-${id.substr(4, 2)}-${id.substr(6, 2)} ${id.substr(8, 2)}:${id.substr(10, 2)}:${id.substr(12, 2)} ${id.substr(14)}`; 23 | }; 24 | }).filter('prettyOrderType', function () { 25 | // TODO: 将此处的类型和其他地方的类型代码全部集中到一处 26 | return function (type) { 27 | const cardType = { 28 | 5: '小时', 29 | 4: '日', 30 | 2: '周', 31 | 3: '月', 32 | 6: '季度', 33 | 7: '年', 34 | 8: '两周', 35 | 9: '半年', 36 | }; 37 | return cardType[type]; 38 | }; 39 | }); 40 | -------------------------------------------------------------------------------- /plugins/webgui/public/configs/auth.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | 4 | app 5 | .service('authInterceptor', ['$q', '$localStorage', function($q, $localStorage) { 6 | const service = this; 7 | service.responseError = function(response) { 8 | if (response.status === 401) { 9 | $localStorage.home = {}; 10 | $localStorage.admin = {}; 11 | $localStorage.user = {}; 12 | window.location = '/'; 13 | } 14 | return $q.reject(response); 15 | }; 16 | }]) 17 | .config(['$httpProvider', '$compileProvider', ($httpProvider, $compileProvider) => { 18 | $httpProvider.interceptors.push('authInterceptor'); 19 | $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|ss|blob):/); 20 | $httpProvider.interceptors.push(['$q', $q => { 21 | return { 22 | request: function (config) { 23 | if(config.url.match(/^\/api\//)) { 24 | config.url = window.api + config.url; 25 | config.withCredentials = true; 26 | } 27 | return config || $q.when(config); 28 | } 29 | }; 30 | }]); 31 | }]) 32 | ; -------------------------------------------------------------------------------- /plugins/webgui/libs/style.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:'Material Icons';font-style:normal;font-weight:400;src:url(/libs/MaterialIcons-Regular.eot);src:local('Material Icons'),local('MaterialIcons-Regular'),url(/libs/MaterialIcons-Regular.woff2) format('woff2'),url(/libs/MaterialIcons-Regular.woff) format('woff'),url(/libs/MaterialIcons-Regular.ttf) format('truetype')}.material-icons{font-family:'Material Icons';font-weight:400;font-style:normal;font-size:24px;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:'liga'}.animate-show{line-height:20px;opacity:1;padding:30px}.animate-show.ng-hide-add,.animate-show.ng-hide-remove{transition:all linear .5s}.animate-show.ng-hide{line-height:0;opacity:0;padding:30 0}.check-element{padding:10px;border:1px solid #000;background:#fff}.search-input>input{color:#fff}.search-input>.md-errors-spacer{min-height:0}.markdown-table table{border-collapse:collapse}.markdown-table table,.markdown-table td,.markdown-table th{padding:5px;border:1px solid #999} -------------------------------------------------------------------------------- /plugins/webgui/public/views/dialog/setUserGroup.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | {{ group.name }} 13 | 14 | 15 | 16 |
17 |
18 |
19 | 20 | 21 | 确定 22 | 23 | 24 |
-------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | .remote-sync.json 39 | 40 | # config 41 | config/development.yml 42 | 43 | .vscode 44 | .github 45 | docker 46 | docs 47 | wikiImage 48 | .eslintrc.json 49 | .gitattributes 50 | *.sqlite 51 | gulpfile.js 52 | plugins/webgui/libs/*.min.js 53 | plugins/webgui/libs/*.min.map 54 | plugins/webgui/libs/*.min.js.map 55 | plugins/webgui/libs/angular-inview.js 56 | plugins/webgui/public/* 57 | !plugins/webgui/public/views 58 | plugins/*/screenshot -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/settings.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | {{ config.email }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
{{ setting.name }}
16 |
17 | 点击进入>> 18 |
19 |
20 |
21 |
22 | 23 |
24 |
25 |
-------------------------------------------------------------------------------- /plugins/webgui/public/views/user/qrcodeDialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

{{ publicInfo.serverName }}

5 | 6 | 7 | close 8 | 9 |
10 |
11 | 12 |
13 |
14 | 15 | 16 | 17 |
18 |
{{ '点击二维码或者用移动设备扫描二维码可自动填充服务器信息' | translate }} 19 |
20 |

{{ publicInfo.ssAddress }}
21 |
22 |
23 |
24 |
-------------------------------------------------------------------------------- /plugins/telegram/serverManager.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const manager = appRequire('services/manager'); 3 | 4 | const add = options => { 5 | const { name, host, port, password } = options; 6 | return knex('server').insert({ 7 | name, 8 | host, 9 | port, 10 | password 11 | }); 12 | }; 13 | 14 | const del = (id) => { 15 | return knex.transaction(trx => { 16 | return knex('server').transacting(trx).where({ id }).delete() 17 | .then(() => knex('saveFlow').transacting(trx).where({ id }).delete()) 18 | .then(trx.commit) 19 | .catch(trx.rollback); 20 | }); 21 | }; 22 | 23 | const edit = options => { 24 | const { id, name, host, port, password } = options; 25 | return knex('server').where({ id }).update({ 26 | name, 27 | host, 28 | port, 29 | password 30 | }); 31 | }; 32 | 33 | const list = async (options = {}) => { 34 | const serverList = await knex('server').select([ 35 | 'id', 36 | 'name', 37 | 'host', 38 | 'port', 39 | 'password', 40 | 'method' 41 | ]).orderBy('name'); 42 | return serverList; 43 | }; 44 | 45 | exports.add = add; 46 | exports.del = del; 47 | exports.edit = edit; 48 | exports.list = list; -------------------------------------------------------------------------------- /plugins/webgui/public/views/skin/default.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
8 |
登录
9 |
注册
10 |
11 |
12 |
13 |
14 |
15 | Fork me on GitHub 16 | -------------------------------------------------------------------------------- /plugins/webgui/public/routes/admin.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.config(['$stateProvider', $stateProvider => { 6 | $stateProvider 7 | .state('admin', { 8 | url: '/admin', 9 | abstract: true, 10 | templateUrl: `${ cdn }/public/views/admin/admin.html`, 11 | resolve: { 12 | myConfig: ['$http', 'configManager', ($http, configManager) => { 13 | if(configManager.getConfig().version) { return; } 14 | return $http.get('/api/home/login').then(success => { 15 | configManager.setConfig(success.data); 16 | }); 17 | }] 18 | }, 19 | }) 20 | .state('admin.index', { 21 | url: '/index', 22 | controller: 'AdminIndexController', 23 | templateUrl: `${ cdn }/public/views/admin/index.html`, 24 | }) 25 | .state('admin.pay', { 26 | url: '/pay', 27 | controller: 'AdminPayController', 28 | templateUrl: `${ cdn }/public/views/admin/pay.html`, 29 | }) 30 | .state('admin.unfinished', { 31 | url: '/unfinished', 32 | templateUrl: `${ cdn }/public/views/admin/unfinished.html`, 33 | }); 34 | } 35 | ]); 36 | -------------------------------------------------------------------------------- /plugins/cli/menu/addPort.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const manager = appRequire('services/manager'); 4 | const index = appRequire('plugins/cli/index'); 5 | 6 | const inquirer = require('inquirer'); 7 | 8 | const menu = [{ 9 | type: 'input', 10 | name: 'port', 11 | message: 'Enter port:', 12 | validate: function (value) { 13 | if(Number.isNaN(+value)) { 14 | return 'Please enter a valid port number.'; 15 | } else if (+value <= 0 || +value >= 65536) { 16 | return 'Port number must between 1 to 65535.'; 17 | } else { 18 | return true; 19 | } 20 | }, 21 | }, { 22 | type: 'input', 23 | name: 'password', 24 | message: 'Enter password:', 25 | validate: function (value) { 26 | if(value === '') { 27 | return 'You can not set an empty password.'; 28 | } else { 29 | return true; 30 | } 31 | }, 32 | }]; 33 | 34 | const add = async () => { 35 | try { 36 | const addPort = await inquirer.prompt(menu); 37 | await manager.send({ 38 | command: 'add', 39 | port: +addPort.port, 40 | password: addPort.password, 41 | }, index.getManagerAddress()); 42 | return; 43 | } catch(err) { 44 | return Promise.reject(err); 45 | } 46 | }; 47 | 48 | exports.add = add; 49 | -------------------------------------------------------------------------------- /plugins/account/db/accountFlow.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'account_flow'; 3 | 4 | const createTable = async() => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | const hasStatus = await knex.schema.hasColumn(tableName, 'status'); 8 | if(!hasStatus) { 9 | await knex.schema.table(tableName, function(table) { 10 | table.string('status').defaultTo('checked'); 11 | }); 12 | } 13 | const hasAutobanTime = await knex.schema.hasColumn(tableName, 'autobanTime'); 14 | if(!hasAutobanTime) { 15 | await knex.schema.table(tableName, function(table) { 16 | table.bigInteger('autobanTime'); 17 | }); 18 | } 19 | return; 20 | } 21 | return knex.schema.createTable(tableName, function(table) { 22 | table.increments('id'); 23 | table.integer('serverId'); 24 | table.integer('accountId'); 25 | table.integer('port'); 26 | table.bigInteger('updateTime'); 27 | table.bigInteger('checkTime'); 28 | table.bigInteger('nextCheckTime'); 29 | table.bigInteger('autobanTime'); 30 | table.bigInteger('flow').defaultTo(0); 31 | table.string('status').defaultTo('checked'); 32 | }); 33 | }; 34 | 35 | exports.createTable = createTable; 36 | -------------------------------------------------------------------------------- /plugins/webgui/public/directives/focusMe.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | 3 | app.directive('focusMe', ['$timeout', $timeout => { 4 | return { 5 | restrict: 'A', 6 | link: ($scope, $element) => { 7 | $timeout(() => { 8 | $element[0].focus(); 9 | }); 10 | } 11 | }; 12 | }]); 13 | 14 | app.directive('ga', () => { 15 | return { 16 | restrict: 'E', 17 | scope: { 18 | adClient: '@', 19 | adSlot: '@', 20 | adFormat: '@', 21 | }, 22 | template: ` 23 | 24 | 25 | 31 | 32 | `, 33 | controller: ['$scope', '$timeout', ($scope, $timeout) => { 34 | $scope.show = Math.random() >= 0.95; 35 | if($scope.show) { 36 | $timeout(function () { 37 | (window.adsbygoogle = window.adsbygoogle || []).push({}); 38 | }); 39 | } 40 | }] 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /plugins/webgui/public/index.js: -------------------------------------------------------------------------------- 1 | angular.module('app', [ 2 | 'ngMaterial', 3 | 'ui.router', 4 | 'ngMessages', 5 | 'ja.qr', 6 | 'chart.js', 7 | 'angularMoment', 8 | 'ngWebSocket', 9 | 'ngStorage', 10 | 'angular-inview', 11 | 'hc.marked', 12 | 'pascalprecht.translate', 13 | 'ngclipboard', 14 | ]); 15 | 16 | const window = require('window'); 17 | angular.element(() => { 18 | $.get(window.api + '/api/home/login').then(success => { 19 | window.ssmgrConfig = success; 20 | 21 | require('./directives/focusMe'); 22 | 23 | require('./services/preloadService.js'); 24 | require('./services/adminService.js'); 25 | require('./services/homeService.js'); 26 | require('./services/userService.js'); 27 | require('./services/configService.js'); 28 | // require('./services/websocketService.js'); 29 | 30 | require('./configs/index.js'); 31 | require('./controllers/index.js'); 32 | require('./dialogs/index.js'); 33 | require('./filters/index.js'); 34 | require('./translate/index.js'); 35 | require('./routes/index.js'); 36 | 37 | angular.bootstrap(document, ['app']); 38 | }).catch(err => { 39 | let time = 5000; 40 | if(err.status === 403) { time = 1500; } 41 | setTimeout(() => { location.reload(); }, time); 42 | }); 43 | }); -------------------------------------------------------------------------------- /plugins/telegram/db/server.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'server'; 3 | const config = appRequire('services/config').all(); 4 | const manager = appRequire('services/manager'); 5 | 6 | const createTable = async () => { 7 | await knex.schema.createTable(tableName, function(table) { 8 | table.increments('id'); 9 | table.string('name'); 10 | table.string('host'); 11 | table.integer('port'); 12 | table.string('password'); 13 | table.string('method').defaultTo('aes-256-cfb'); 14 | }); 15 | const list = await knex('server').select(['name', 'host', 'port', 'password']); 16 | if(list.length === 0) { 17 | const host = config.manager.address.split(':')[0]; 18 | const port = +config.manager.address.split(':')[1]; 19 | const password = config.manager.password; 20 | await manager.send({ 21 | command: 'version' 22 | }, { 23 | host, 24 | port, 25 | password, 26 | }).catch(() => { 27 | logger.error(`connect to server ${ password }@${ host }:${ port } fail.`); 28 | process.exit(1); 29 | }); 30 | await knex('server').insert({ 31 | name: 'default', 32 | host, 33 | port, 34 | password, 35 | }); 36 | } 37 | return; 38 | }; 39 | 40 | exports.createTable = createTable; 41 | -------------------------------------------------------------------------------- /plugins/user/db/user.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'user'; 3 | 4 | const createTable = async() => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | const hasColumnGroup = await knex.schema.hasColumn(tableName, 'group'); 8 | if(!hasColumnGroup) { 9 | await knex.schema.table(tableName, function(table) { 10 | table.integer('group').defaultTo(0); 11 | }); 12 | } 13 | const hasComment = await knex.schema.hasColumn(tableName, 'comment'); 14 | if(!hasComment) { 15 | await knex.schema.table(tableName, function(table) { 16 | table.string('comment').defaultTo(''); 17 | }); 18 | } 19 | return; 20 | } 21 | return knex.schema.createTable(tableName, function(table) { 22 | table.increments('id').primary(); 23 | table.string('username').unique(); 24 | table.string('email'); 25 | table.string('telegram'); 26 | table.string('password'); 27 | table.string('type'); 28 | table.bigInteger('createTime'); 29 | table.bigInteger('lastLogin'); 30 | table.string('resetPasswordId'); 31 | table.bigInteger('resetPasswordTime'); 32 | table.integer('group').defaultTo(0); 33 | table.string('comment').defaultTo(''); 34 | }); 35 | }; 36 | 37 | exports.createTable = createTable; 38 | -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/serverChart.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('serverChartDialog' , [ '$mdDialog', $mdDialog => { 6 | const publicInfo = {}; 7 | const hide = () => { 8 | return $mdDialog.hide() 9 | .then(success => { 10 | dialogPromise = null; 11 | return; 12 | }).catch(err => { 13 | dialogPromise = null; 14 | return; 15 | }); 16 | }; 17 | publicInfo.hide = hide; 18 | let dialogPromise = null; 19 | const isDialogShow = () => { 20 | if(dialogPromise && !dialogPromise.$$state.status) { 21 | return true; 22 | } 23 | return false; 24 | }; 25 | const dialog = { 26 | templateUrl: `${ cdn }/public/views/dialog/serverChart.html`, 27 | escapeToClose: false, 28 | locals: { bind: publicInfo }, 29 | bindToController: true, 30 | controller: ['$scope', 'bind', function($scope, bind) { 31 | $scope.publicInfo = bind; 32 | }], 33 | clickOutsideToClose: true, 34 | }; 35 | const show = serverChart => { 36 | if(isDialogShow()) { 37 | return dialogPromise; 38 | } 39 | publicInfo.serverChart = serverChart; 40 | dialogPromise = $mdDialog.show(dialog); 41 | return dialogPromise; 42 | }; 43 | return { 44 | show, 45 | hide, 46 | }; 47 | }]); -------------------------------------------------------------------------------- /plugins/account/db/account.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'account_plugin'; 3 | 4 | const createTable = async() => { 5 | const exist = await knex.schema.hasTable(tableName); 6 | if(exist) { 7 | const hasKey = await knex.schema.hasColumn(tableName, 'key'); 8 | if(!hasKey) { 9 | await knex.schema.table(tableName, function(table) { 10 | table.string('key'); 11 | }); 12 | } 13 | const results = await knex(tableName).whereNull('orderId'); 14 | for(const result of results) { 15 | await knex(tableName).update({ orderId: result.type === 1 ? 0 : result.type }).where({ id: result.id }); 16 | } 17 | return; 18 | } 19 | return knex.schema.createTable(tableName, function(table) { 20 | table.increments('id'); 21 | table.integer('type'); 22 | table.integer('orderId'); 23 | table.integer('userId'); 24 | table.string('server'); 25 | table.integer('port').unique(); 26 | table.string('password'); 27 | table.string('key'); 28 | table.string('data'); 29 | table.string('subscribe'); 30 | table.integer('status'); 31 | table.integer('autoRemove').defaultTo(0); 32 | table.bigInteger('autoRemoveDelay').defaultTo(0); 33 | table.integer('multiServerFlow').defaultTo(0); 34 | table.integer('active').defaultTo(1); 35 | }); 36 | }; 37 | 38 | exports.createTable = createTable; 39 | -------------------------------------------------------------------------------- /docs/ssmgrapi.md: -------------------------------------------------------------------------------- 1 | # ssmgr API 2 | 3 | ssmgr 之间采用 TCP socket 的方式通讯,接口协议如下: 4 | 5 | 发送:`[2字节 消息长度][6字节时间戳][指令][4字节 校验码]` 6 | 7 | 返回:`[2字节 消息长度[内容]` 8 | 9 | * 列出服务器上的端口和密码 10 | 11 | ``` 12 | { 13 | command: 'list' 14 | } 15 | [ 16 | { port: 1234, password: '5678'}, 17 | { port: 1235, password: '5678'} 18 | ] 19 | ``` 20 | 21 | * 添加端口 22 | 23 | ``` 24 | { 25 | command: 'add', 26 | port: 1234, 27 | password: 'qwer' 28 | } 29 | { 30 | port: 1234, 31 | password: 'qwer' 32 | } 33 | ``` 34 | 35 | * 删除端口 36 | 37 | ``` 38 | { 39 | command: 'del', 40 | port: 1234 41 | } 42 | { 43 | port: 1234 44 | } 45 | ``` 46 | 47 | * 修改密码 48 | 49 | ``` 50 | { 51 | command: 'pwd' 52 | port: 1234, 53 | password: 'asdfgh' 54 | } 55 | { 56 | port: 1234, 57 | password: 'asdfgh' 58 | } 59 | ``` 60 | 61 | * 查询流量 62 | 63 | ``` 64 | { 65 | command: 'flow', 66 | port: 1234, 67 | options: { 68 | startTime: 1489137503258, 69 | endTime: 1489137603258, 70 | clear: true 71 | } 72 | } 73 | [ 74 | { port: 1234, sumFlow: 1234 }, 75 | { port: 1235, sumFlow: 1234 } 76 | ] 77 | ``` 78 | 79 | * 查询版本号 80 | 81 | ``` 82 | { 83 | command: 'version' 84 | } 85 | { 86 | version: 0.9.0 87 | } 88 | ``` 89 | 90 | * 查询客户端连接IP 91 | 92 | ``` 93 | { 94 | command: 'ip', 95 | port: 1234 96 | } 97 | [ 98 | '1.1.1.1', 99 | '2.2.2.2' 100 | ] 101 | ``` -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Shadowsocks-Manager 6 | 7 | 8 | 9 | 10 | 11 | 12 |
加载中
13 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /init/runShadowsocks.js: -------------------------------------------------------------------------------- 1 | const log4js = require('log4js'); 2 | const logger = log4js.getLogger('system'); 3 | 4 | const config = appRequire('services/config').all(); 5 | 6 | const spawn = require('child_process').spawn; 7 | 8 | const run = async () => { 9 | let runParams = config.runShadowsocks; 10 | let type = 'libev'; 11 | let method = 'aes-256-cfb'; 12 | if(!runParams) { 13 | return; 14 | } 15 | if(typeof runParams === 'boolean' && runParams) { 16 | runParams = ''; 17 | } 18 | if(runParams.indexOf(':') >= 0) { 19 | method = runParams.split(':')[1]; 20 | } 21 | let shadowsocks; 22 | if(runParams.indexOf('python') >= 0) { 23 | type = 'python'; 24 | const tempPassword = 'qwerASDF' + Math.random().toString().substr(2, 8); 25 | shadowsocks = spawn('ssserver', ['-m', method, '-p', '65535', '-k', tempPassword, '--manager-address', config.shadowsocks.address]); 26 | } else { 27 | shadowsocks = spawn('ss-manager', [ '-v', '-m', method, '-u', '--manager-address', config.shadowsocks.address]); 28 | } 29 | 30 | shadowsocks.stdout.on('data', (data) => { 31 | // console.log(`stdout: ${data}`); 32 | }); 33 | 34 | shadowsocks.stderr.on('data', (data) => { 35 | // console.error(`stderr: ${data}`); 36 | }); 37 | 38 | shadowsocks.on('close', (code) => { 39 | console.log(`child process exited with code ${code}`); 40 | }); 41 | logger.info(`Run shadowsocks (${ type === 'python' ? 'python' : 'libev'})`); 42 | return; 43 | }; 44 | 45 | exports.run = run; 46 | -------------------------------------------------------------------------------- /services/config.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml'); 2 | const fs = require('fs'); 3 | const os = require('os'); 4 | const path = require('path'); 5 | const _ = require('lodash'); 6 | 7 | const log4js = require('log4js'); 8 | const logger = log4js.getLogger('system'); 9 | 10 | let config; 11 | 12 | const defaultPath = path.resolve(os.homedir() + '/.ssmgr/default.yml'); 13 | let configFilePath = defaultPath; 14 | if(global.configFile) { 15 | if(fs.existsSync(path.resolve(global.configFile))) { 16 | configFilePath = path.resolve(global.configFile); 17 | } else if(fs.existsSync(path.resolve(os.homedir() + '/.ssmgr/' + global.configFile))) { 18 | configFilePath = path.resolve(os.homedir() + '/.ssmgr/' + global.configFile); 19 | } else { 20 | logger.error(`Can not find file: ${ global.configFile }`); 21 | process.exit(1); 22 | } 23 | } 24 | 25 | try { 26 | logger.info('Config file path: ', configFilePath); 27 | const configFileData = fs.readFileSync(configFilePath); 28 | if(configFilePath.substr(configFilePath.length - 5) === '.json') { 29 | config = JSON.parse(configFileData); 30 | } else { 31 | config = yaml.safeLoad(configFileData, 'utf8'); 32 | } 33 | } catch (err) { 34 | logger.error(err); 35 | } 36 | 37 | exports.all = () => { 38 | return config; 39 | }; 40 | 41 | exports.get = (path) => { 42 | if(!config) { 43 | return; 44 | } 45 | return _.get(config, path); 46 | }; 47 | 48 | exports.set = (path, value) => { 49 | return _.set(config, path, value); 50 | }; 51 | -------------------------------------------------------------------------------- /plugins/telegram/README.md: -------------------------------------------------------------------------------- 1 | # telegram plugin 2 | 3 | This plugin can control shadowsocks through a telegram bot. 4 | 5 | ## Usage 6 | 7 | 1. Create a telegram bot with [BotFather](https://telegram.me/BotFather). He will give you a token for the bot. 8 | 9 | 2. Edit config file to use telegram plugin: 10 | 11 | ``` 12 | plugins: 13 | telegram: 14 | token: '12345678:********************' 15 | use: true 16 | ``` 17 | 18 | 3. Start `ssmgr` with type m, and you can talk to your bot to control it. 19 | `ssmgr -t m -m yourHost:yourPort` 20 | 21 | ## Command 22 | 23 | ### Auth 24 | 25 | * `auth` Set manager user, the first user send 'auth' to bot will be the manager. 26 | 27 | ### Help 28 | 29 | * `help` Show help message 30 | 31 | ### Port 32 | 33 | * `list` Show port and password in current server 34 | * `del {port}` Delete port 35 | * `add {port} {password}` Add port and set password 36 | * `pwd {port} {password}` Change password 37 | 38 | ### Server 39 | 40 | * `listserver` 41 | * `switchserver {id}` 42 | * `delserver {name}` 43 | * `addserver {name} {host} {port} {password}` 44 | * `editserver {id} {name} {host} {port} {password}` 45 | 46 | ### Flow 47 | 48 | * `flow` 49 | * `flow{number}min` 50 | * `flow{number}hour` 51 | 52 | ## Screenshot 53 | 54 | ### List 55 | 56 | ![Telegram01](https://github.com/shadowsocks/shadowsocks-manager/blob/master/plugins/telegram/screenshot/telegram01.png) 57 | 58 | ### Flow 59 | 60 | ![Telegram02](https://github.com/shadowsocks/shadowsocks-manager/blob/master/plugins/telegram/screenshot/telegram02.png) 61 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/dialog/confirm.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
7 |
8 |
{{publicInfo.text}}
9 |
10 |
11 |
{{publicInfo.error}}
12 |
13 |
14 |
15 | 16 | 17 | {{publicInfo.cancel | translate }} 18 | 19 | 20 | {{publicInfo.confirm | translate }} 21 | 22 | 23 | {{ '确定' | translate }} 24 | 25 | 26 |
27 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/dialog/subscribe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
{{ publicInfo.subscribeLink }}
5 |
请选择订阅链接类型:
6 | 7 | 8 |
{{ type }}
9 |
10 |
11 | 将域名转换为IP地址 12 |
13 |
14 | 15 | 复制链接 16 | 17 |
18 |
19 | 20 | 更换链接 21 | 22 |
23 |
24 |
25 |
26 |
-------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/giftcardBatchList.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | {{ batch.orderName }} 8 | 9 | 10 | ({{batch.availableCount}} / {{batch.totalCount}}) 11 | 12 | 13 | ({{batch.totalCount}}) 14 | 15 | 16 |
17 |
{{batch.createTime | timeago}}
18 |
19 |
20 |
{{ batch.comment || '-' }}
21 |
22 |
23 |
24 |
25 |
-------------------------------------------------------------------------------- /plugins/webgui/public/views/user/order.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | {{ order.orderId.substr(order.orderId.length - 6) }}
8 | {{ order.createTime | date : 'yyyy-MM-dd HH:mm' }} 9 |
10 |
11 | {{ order.type }}
12 | {{ order.amount }} 13 | {{ order.refTime | timePeriod }} 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 |
26 |
-------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/markdown.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('markdownDialog', [ '$mdDialog', ($mdDialog) => { 6 | const publicInfo = {}; 7 | const hide = () => { 8 | return $mdDialog.hide() 9 | .then(success => { 10 | dialogPromise = null; 11 | return; 12 | }).catch(err => { 13 | dialogPromise = null; 14 | return; 15 | }); 16 | }; 17 | publicInfo.hide = hide; 18 | let dialogPromise = null; 19 | const isDialogShow = () => { 20 | if(dialogPromise && !dialogPromise.$$state.status) { 21 | return true; 22 | } 23 | return false; 24 | }; 25 | const dialog = { 26 | templateUrl: `${ cdn }/public/views/admin/previewNotice.html`, 27 | escapeToClose: false, 28 | locals: { bind: publicInfo }, 29 | bindToController: true, 30 | controller: ['$scope', '$mdMedia', '$mdDialog', 'bind', function($scope, $mdMedia, $mdDialog, bind) { 31 | $scope.publicInfo = bind; 32 | $scope.setDialogWidth = () => { 33 | if($mdMedia('xs') || $mdMedia('sm')) { 34 | return {}; 35 | } 36 | return { 'min-width': '400px' }; 37 | }; 38 | }], 39 | fullscreen: true, 40 | clickOutsideToClose: true, 41 | }; 42 | const show = (title, markdown) => { 43 | if(isDialogShow()) { 44 | return dialogPromise; 45 | } 46 | publicInfo.title = title; 47 | publicInfo.markdown = markdown; 48 | dialogPromise = $mdDialog.show(dialog); 49 | return dialogPromise; 50 | }; 51 | return { 52 | show, 53 | }; 54 | }]); 55 | -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/qrcode.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('qrcodeDialog', [ '$mdDialog', ($mdDialog) => { 6 | const publicInfo = {}; 7 | const hide = () => { 8 | return $mdDialog.hide() 9 | .then(success => { 10 | dialogPromise = null; 11 | return; 12 | }).catch(err => { 13 | dialogPromise = null; 14 | return; 15 | }); 16 | }; 17 | publicInfo.hide = hide; 18 | let dialogPromise = null; 19 | const isDialogShow = () => { 20 | if(dialogPromise && !dialogPromise.$$state.status) { 21 | return true; 22 | } 23 | return false; 24 | }; 25 | const dialog = { 26 | templateUrl: `${ cdn }/public/views/user/qrcodeDialog.html`, 27 | escapeToClose: false, 28 | locals: { bind: publicInfo }, 29 | bindToController: true, 30 | controller: ['$scope', '$mdDialog', '$mdMedia', 'bind', function($scope, $mdDialog, $mdMedia, bind) { 31 | $scope.publicInfo = bind; 32 | $scope.setDialogWidth = () => { 33 | if($mdMedia('xs') || $mdMedia('sm')) { 34 | return {}; 35 | } 36 | return { 'min-width': '400px' }; 37 | }; 38 | }], 39 | fullscreen: true, 40 | clickOutsideToClose: true, 41 | }; 42 | const show = (serverName, ssAddress) => { 43 | if(isDialogShow()) { 44 | return dialogPromise; 45 | } 46 | publicInfo.serverName = serverName; 47 | publicInfo.ssAddress = ssAddress; 48 | dialogPromise = $mdDialog.show(dialog); 49 | return dialogPromise; 50 | }; 51 | return { 52 | show, 53 | }; 54 | }]); -------------------------------------------------------------------------------- /plugins/cli/menu/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const config = appRequire('services/config').all(); 5 | const isFlowSaverUse = _.get(config, 'plugins.flowSaver.use'); 6 | const inquirer = require('inquirer'); 7 | const listPort = appRequire('plugins/cli/menu/listPort'); 8 | const addPort = appRequire('plugins/cli/menu/addPort'); 9 | const listServer = appRequire('plugins/cli/menu/listServer'); 10 | const addServer = appRequire('plugins/cli/menu/addServer'); 11 | const flow = appRequire('plugins/cli/menu/flow'); 12 | const index = appRequire('plugins/cli/index'); 13 | 14 | const main = [ 15 | { 16 | type: 'list', 17 | name: 'mainMeun', 18 | message: () => { 19 | return 'Main menu [' + index.getManagerAddress().host + ':' + index.getManagerAddress().port + ']'; 20 | }, 21 | choices: ['add port', 'list port'], 22 | } 23 | ]; 24 | 25 | if(isFlowSaverUse) { 26 | main[0].choices.push('add server', 'list server', 'flow'); 27 | } 28 | 29 | const mainMeun = () => { 30 | console.log(); 31 | inquirer.prompt(main) 32 | .then(success => { 33 | if(success.mainMeun === 'list port') { 34 | return listPort.list(); 35 | } else if(success.mainMeun === 'add port') { 36 | return addPort.add(); 37 | } else if(success.mainMeun === 'list server') { 38 | return listServer.list(); 39 | } else if(success.mainMeun === 'add server') { 40 | return addServer.add(); 41 | } else if(success.mainMeun === 'flow') { 42 | return flow.getFlow(); 43 | } 44 | }).then(() => { 45 | return mainMeun(); 46 | }) 47 | .catch(() => { 48 | return mainMeun(); 49 | }); 50 | }; 51 | 52 | mainMeun(); 53 | -------------------------------------------------------------------------------- /plugins/telegram/auth.js: -------------------------------------------------------------------------------- 1 | const telegram = appRequire('plugins/telegram/index').telegram; 2 | const knex = appRequire('init/knex').knex; 3 | 4 | const log4js = require('log4js'); 5 | const logger = log4js.getLogger('telegram'); 6 | 7 | const setManager = async (message) => { 8 | try { 9 | const manager = await knex('telegram').select(['value']).where({ 10 | key: 'manager' 11 | }); 12 | if(manager.length === 0) { 13 | await knex('telegram').insert({ 14 | key: 'manager', 15 | value: message.message.from.id, 16 | }); 17 | telegram.emit('send', message, 'Authorize success.'); 18 | } else if(+manager[0].value === message.message.from.id) { 19 | telegram.emit('send', message, 'This user is already a manager.'); 20 | } else { 21 | telegram.emit('send', message, 'Authorize fail.'); 22 | } 23 | return; 24 | } catch(err) { 25 | logger.error(err); 26 | return Promise.reject(err); 27 | } 28 | }; 29 | 30 | const isManager = async (message) => { 31 | try { 32 | const manager = await knex('telegram').select(['value']).where({ 33 | key: 'manager', 34 | }); 35 | if(manager.length === 0) { 36 | telegram.emit('send', message, 'Send \'auth\' to become manager.'); 37 | } else if(manager.length > 0 && manager[0].value === message.message.from.id + '') { 38 | telegram.emit('manager', message); 39 | } 40 | } catch(err) { 41 | logger.error(err); 42 | return Promise.reject(err); 43 | } 44 | }; 45 | 46 | telegram.on('message', message => { 47 | if (message.message.text === 'auth') { 48 | setManager(message); 49 | } else { 50 | isManager(message); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/editRefCode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ refCode.code }}复制链接 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 |
29 | 30 |
31 |
-------------------------------------------------------------------------------- /plugins/cli/menu/flow.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const inquirer = require('inquirer'); 4 | const flow = appRequire('plugins/flowSaver/flow'); 5 | const index = appRequire('plugins/cli/index'); 6 | const flowSaverServer = appRequire('plugins/flowSaver/server'); 7 | 8 | const menu = [ 9 | { 10 | type: 'list', 11 | name: 'flow', 12 | message: 'Select time:', 13 | choices: ['5 mins', '1 hour', '24 hours'], 14 | } 15 | ]; 16 | 17 | const flowNumber = (number) => { 18 | if(number < 1000) return number + ' B'; 19 | else if(number < 1000 * 1000) return number / 1000 + ' KB'; 20 | else if(number < 1000 * 1000 * 1000) return (number / 1000000).toFixed(2) + ' MB'; 21 | else if(number < 1000 * 1000 * 1000 * 1000) return (number / 1000000000).toFixed(3) + ' GB'; 22 | }; 23 | 24 | const getFlow = async () => { 25 | try { 26 | const flowTime = await inquirer.prompt(menu); 27 | const managerAddress = index.getManagerAddress(); 28 | let startTime = Date.now(); 29 | const endTime = Date.now(); 30 | if(flowTime.flow === '5 mins') { 31 | startTime -= 5 * 60 * 1000; 32 | } else if(flowTime.flow === '1 hour') { 33 | startTime -= 60 * 60 * 1000; 34 | } else if(flowTime.flow === '24 hours') { 35 | startTime -= 24 * 60 * 60 * 1000; 36 | } 37 | const myFlow = await flow.getFlow(managerAddress.host, managerAddress.port, startTime, endTime); 38 | console.log(myFlow.map(m => { 39 | return { 40 | port: m.port, 41 | flow: flowNumber(m.sumFlow), 42 | }; 43 | })); 44 | return; 45 | } catch(err) { 46 | console.log(err); 47 | return Promise.reject(err); 48 | } 49 | }; 50 | 51 | exports.getFlow = getFlow; 52 | -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/addGiftCardBatch.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('addGiftCardBatchDialog', ['$mdDialog', '$http', ($mdDialog, $http) => { 6 | const publicInfo = { 7 | status: 'show', 8 | count: 20, 9 | orderId: 3, 10 | }; 11 | 12 | $http.get('/api/admin/order').then(success => { 13 | publicInfo.orderList = success.data; 14 | }); 15 | 16 | let dialogPromise = null; 17 | const isDialogShow = () => dialogPromise && !dialogPromise.$$state.status; 18 | 19 | const show = () => { 20 | if (isDialogShow()) { 21 | return dialogPromise; 22 | } 23 | publicInfo.status = 'show'; 24 | dialogPromise = $mdDialog.show(dialog); 25 | return dialogPromise; 26 | }; 27 | 28 | const close = () => { 29 | $mdDialog.hide(); 30 | dialogPromise = null; 31 | }; 32 | 33 | const submit = () => { 34 | publicInfo.status = 'loading'; 35 | $http.post('/api/admin/giftcard/add', { 36 | count: publicInfo.count, 37 | orderId: publicInfo.orderId, 38 | comment: publicInfo.comment, 39 | }) 40 | .then(() => close()) 41 | .catch(err => { publicInfo.status = 'error'; }); 42 | }; 43 | publicInfo.close = close; 44 | publicInfo.submit = submit; 45 | 46 | const dialog = { 47 | templateUrl: `${cdn}/public/views/dialog/addGiftCardBatch.html`, 48 | escapeToClose: true, 49 | locals: { bind: publicInfo }, 50 | bindToController: true, 51 | controller: ['$scope', 'bind', ($scope, bind) => { 52 | $scope.publicInfo = bind; 53 | }], 54 | clickOutsideToClose: false, 55 | }; 56 | return { show }; 57 | }]); -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/user.html: -------------------------------------------------------------------------------- 1 |
共 {{ total }} 个用户
2 |
3 |
4 | 5 | 6 |
7 |
{{user.username}}
8 |
9 | chevron_right 10 |
11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/dialog/setEmail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

邮件设置

5 | 6 | 7 | close 8 | 9 |
10 |
11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 | 保存 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /plugins/webgui_telegram/help.js: -------------------------------------------------------------------------------- 1 | const tg = appRequire('plugins/webgui_telegram/index'); 2 | const telegram = appRequire('plugins/webgui_telegram/index').telegram; 3 | const isUser = appRequire('plugins/webgui_telegram/index').isUser; 4 | const isNotUserOrAdmin = appRequire('plugins/webgui_telegram/index').isNotUserOrAdmin; 5 | const config = appRequire('services/config').all(); 6 | const knex = appRequire('init/knex').knex; 7 | 8 | const isHelp = message => { 9 | if(!message.message || !message.message.text) { return false; } 10 | if(!message.message || !message.message.chat || !message.message.chat.type === 'private') { return false; } 11 | if(message.message.text.trim() !== 'help' && message.message.text !== '/start') { return false; } 12 | return true; 13 | }; 14 | 15 | telegram.on('message', async message => { 16 | if(!isHelp(message)) { return; } 17 | const telegramId = message.message.chat.id.toString(); 18 | const userStatus = await tg.getUserStatus(telegramId); 19 | const title = (await knex('webguiSetting').select().where({ 20 | key: 'base', 21 | }).then(success => { 22 | if (!success.length) { return Promise.reject('settings not found'); } 23 | success[0].value = JSON.parse(success[0].value); 24 | return success[0].value; 25 | })).title; 26 | const site = config.plugins.webgui.site; 27 | if(userStatus.status === 'empty') { 28 | tg.sendKeyboard(`欢迎使用 ${ title },\n\n请在这里输入您的邮箱以接收验证码来注册账号\n\n或者点击以下按钮访问网页版`, telegramId, { 29 | inline_keyboard: [[{ 30 | text: '登录网页版', 31 | url: site, 32 | }]], 33 | }); 34 | } else if (userStatus.status === 'normal') { 35 | tg.sendMessage('指令列表:\n\naccount: 显示ss账号信息\nlogin: 快捷登录网页版', telegramId); 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/dialog/email.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

发送邮件

5 | 6 | 7 | close 8 | 9 |
10 |
11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 | 发送 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/dialog/editUserComment.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
修改备注失败
17 |
18 |
19 |
20 | 21 | 22 | {{ '取消' | translate }} 23 | 24 | 25 | {{ '保存' | translate }} 26 | 27 | 28 | {{ '确定' | translate }} 29 | 30 | 31 |
32 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/dialog/payByGiftCard.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
{{ publicInfo.message }}
17 |
18 |
19 |
20 | 21 | 22 | {{ '确定' | translate }} 23 | 24 | 25 | {{ '关闭' | translate }} 26 | 27 | 28 | {{ '确定' | translate }} 29 | 30 | 31 |
-------------------------------------------------------------------------------- /plugins/cli/README.md: -------------------------------------------------------------------------------- 1 | # cli plugin 2 | 3 | ## Summary 4 | 5 | This plugin provides a command line tool to control shadowsocks. 6 | 7 | ## Usage 8 | 9 | #### Without flowSaver 10 | 11 | If you only have one shadowsocks server and you don't need to count the flows: 12 | 13 | 1. Edit config file like this: 14 | 15 | ``` 16 | plugins: 17 | cli: 18 | use: true 19 | ``` 20 | 21 | 2. Start ssmgr with type m: 22 | `ssmgr -t m -m yourHost:yourPort` 23 | 24 | 3. Menu like this: 25 | 26 | ``` 27 | Main menu 28 | |-- add port 29 | \-- list port 30 | |-- Delete port 31 | \-- Change password 32 | ``` 33 | 34 | #### With flowSaver 35 | 36 | If you have more than one shadowsocks server or you have to count the flows, you need to start with flowSaver plugin: 37 | 38 | 1. Edit config file like this: 39 | 40 | ``` 41 | plugins: 42 | cli: 43 | use: true 44 | flowSaver: 45 | use: true 46 | ``` 47 | 48 | 2. Start ssmgr with type m: 49 | `ssmgr -t m -m yourHost:yourPort` 50 | 51 | 3. Menu like this: 52 | 53 | ``` 54 | Main menu 55 | |-- add port 56 | |-- list port 57 | | |-- Delete port 58 | | \-- Change password 59 | |-- add server 60 | |-- list server 61 | | |-- Switch to it 62 | | |-- Delete server 63 | | \-- Edit server 64 | \-- flow 65 | |-- 5 mins 66 | |-- 1 hour 67 | \-- 24 hours 68 | ``` 69 | 70 | ## Screenshot 71 | 72 | ![cli01](https://github.com/shadowsocks/shadowsocks-manager/blob/master/plugins/cli/screenshot/cli01.png) 73 | 74 | ![cli02](https://github.com/shadowsocks/shadowsocks-manager/blob/master/plugins/cli/screenshot/cli02.png) 75 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/telegramSetting.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 |
请添加 Telegram 账号 @{{ code.telegram }} 并输入“{{ code.code }}”完成绑定
18 |
19 |
20 |
21 | 22 | 23 |
24 |
已绑定
25 |
26 | 解除绑定>> 27 |
28 |
29 |
30 |
31 | 32 |
33 |
34 |
-------------------------------------------------------------------------------- /plugins/cli/menu/addServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const flowSaverServer = appRequire('plugins/flowSaver/server'); 4 | const inquirer = require('inquirer'); 5 | 6 | const menu = [ 7 | { 8 | type: 'input', 9 | name: 'name', 10 | message: 'Enter server name:', 11 | validate: function (value) { 12 | if(value === '') { 13 | return 'You can not set an empty name.'; 14 | } else { 15 | return true; 16 | } 17 | }, 18 | }, { 19 | type: 'input', 20 | name: 'host', 21 | message: 'Enter server host:', 22 | validate: function (value) { 23 | if(value === '') { 24 | return 'You can not set an empty host.'; 25 | } else { 26 | return true; 27 | } 28 | }, 29 | }, { 30 | type: 'input', 31 | name: 'port', 32 | message: 'Enter server port:', 33 | validate: function (value) { 34 | if(Number.isNaN(+value)) { 35 | return 'Please enter a valid port number.'; 36 | } else if (+value <= 0 || +value >= 65536) { 37 | return 'Port number must between 1 to 65535.'; 38 | } else { 39 | return true; 40 | } 41 | } 42 | }, { 43 | type: 'input', 44 | name: 'password', 45 | message: 'Enter password:', 46 | validate: function (value) { 47 | if(value === '') { 48 | return 'You can not set an empty password.'; 49 | } else { 50 | return true; 51 | } 52 | }, 53 | } 54 | ]; 55 | 56 | const add = async () => { 57 | try { 58 | const addServer = await inquirer.prompt(menu); 59 | await flowSaverServer.add(addServer.name, addServer.host, +addServer.port, addServer.password); 60 | return; 61 | } catch(err) { 62 | console.log(err); 63 | return Promise.reject(err); 64 | } 65 | }; 66 | 67 | exports.add = add; 68 | -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/editUserComment.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('editUserCommentDialog', [ '$mdDialog', '$http', ($mdDialog, $http) => { 6 | const publicInfo = { 7 | status: 'show', 8 | }; 9 | let dialogPromise = null; 10 | const isDialogShow = () => { 11 | if(dialogPromise && !dialogPromise.$$state.status) { 12 | return true; 13 | } 14 | return false; 15 | }; 16 | const show = (userId, comment) => { 17 | if(isDialogShow()) { 18 | return dialogPromise; 19 | } 20 | publicInfo.status = 'show'; 21 | publicInfo.userId = userId; 22 | publicInfo.comment = comment; 23 | dialogPromise = $mdDialog.show(dialog); 24 | return dialogPromise; 25 | }; 26 | const close = () => { 27 | return $mdDialog.hide() 28 | .then(success => { 29 | dialogPromise = null; 30 | return; 31 | }).catch(err => { 32 | dialogPromise = null; 33 | return; 34 | }); 35 | }; 36 | const editComment = () => { 37 | publicInfo.status = 'loading'; 38 | $http.put(`/api/admin/user/${ publicInfo.userId }/comment`, { 39 | comment: publicInfo.comment 40 | }).then(() => { 41 | close(); 42 | }).catch(() => { 43 | publicInfo.status = 'error'; 44 | }); 45 | }; 46 | publicInfo.close = close; 47 | publicInfo.editComment = editComment; 48 | const dialog = { 49 | templateUrl: `${ cdn }/public/views/dialog/editUserComment.html`, 50 | escapeToClose: false, 51 | locals: { bind: publicInfo }, 52 | bindToController: true, 53 | controller: ['$scope', 'bind', ($scope, bind) => { 54 | $scope.publicInfo = bind; 55 | }], 56 | clickOutsideToClose: false, 57 | }; 58 | return { 59 | show, 60 | }; 61 | }]); -------------------------------------------------------------------------------- /plugins/webgui/public/views/user/telegram.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 |
请添加 Telegram 账号 @{{ code.telegram }} 并输入“{{ code.code }}”完成绑定
18 |
19 |
20 |
21 | 22 | 23 |
24 |
已绑定
25 |
26 | 解除绑定>> 27 |
28 |
29 |
30 |
31 | 32 |
33 |
34 |
-------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/autopop.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('autopopDialog', [ '$mdDialog', ($mdDialog) => { 6 | const publicInfo = {}; 7 | const hide = () => { 8 | return $mdDialog.hide() 9 | .then(success => { 10 | dialogPromise = null; 11 | return; 12 | }).catch(err => { 13 | dialogPromise = null; 14 | return; 15 | }); 16 | }; 17 | publicInfo.hide = hide; 18 | const next = () => { 19 | if(publicInfo.index < publicInfo.notices.length - 1) { 20 | publicInfo.index += 1; 21 | } else { 22 | hide(); 23 | } 24 | }; 25 | publicInfo.next = next; 26 | let dialogPromise = null; 27 | const isDialogShow = () => { 28 | if(dialogPromise && !dialogPromise.$$state.status) { 29 | return true; 30 | } 31 | return false; 32 | }; 33 | const dialog = { 34 | templateUrl: `${ cdn }/public/views/dialog/autopop.html`, 35 | escapeToClose: false, 36 | locals: { bind: publicInfo }, 37 | bindToController: true, 38 | controller: ['$scope', '$mdMedia', '$mdDialog', 'bind', function($scope, $mdMedia, $mdDialog, bind) { 39 | $scope.publicInfo = bind; 40 | $scope.publicInfo.index = 0; 41 | $scope.setDialogWidth = () => { 42 | if($mdMedia('xs') || $mdMedia('sm')) { 43 | return {}; 44 | } 45 | return { 'min-width': '400px' }; 46 | }; 47 | }], 48 | fullscreen: true, 49 | clickOutsideToClose: false, 50 | }; 51 | const show = notices => { 52 | if(isDialogShow()) { 53 | return dialogPromise; 54 | } 55 | publicInfo.notices = notices; 56 | dialogPromise = $mdDialog.show(dialog); 57 | return dialogPromise; 58 | }; 59 | return { 60 | show, 61 | }; 62 | }]); 63 | -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/alert.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('alertDialog' , [ '$q', '$mdDialog', ($q, $mdDialog) => { 6 | const publicInfo = {}; 7 | publicInfo.isLoading = false; 8 | publicInfo.content = ''; 9 | publicInfo.button = ''; 10 | let alertDialogPromise = null; 11 | const isDialogShow = () => { 12 | if(alertDialogPromise && !alertDialogPromise.$$state.status) { 13 | return true; 14 | } 15 | return false; 16 | }; 17 | const close = () => { 18 | return $mdDialog.hide().then(success => { 19 | publicInfo.isLoading = false; 20 | alertDialogPromise = null; 21 | return; 22 | }).catch(err => { 23 | publicInfo.isLoading = false; 24 | alertDialogPromise = null; 25 | return; 26 | }); 27 | }; 28 | publicInfo.close = close; 29 | const dialog = { 30 | templateUrl: `${ cdn }/public/views/dialog/alert.html`, 31 | escapeToClose: false, 32 | locals: { bind: publicInfo }, 33 | bindToController: true, 34 | controller: ['$scope', '$mdDialog', 'bind', function($scope, $mdDialog, bind) { 35 | $scope.publicInfo = bind; 36 | }], 37 | clickOutsideToClose: false, 38 | }; 39 | const show = (content, button) => { 40 | publicInfo.content = content; 41 | publicInfo.button = button; 42 | if(isDialogShow()) { 43 | publicInfo.isLoading = false; 44 | return alertDialogPromise; 45 | } 46 | alertDialogPromise = $mdDialog.show(dialog); 47 | return $q.resolve(); 48 | }; 49 | const loading = () => { 50 | publicInfo.isLoading = true; 51 | if(!isDialogShow()) { 52 | return show(); 53 | } 54 | return $q.resolve(); 55 | }; 56 | return { 57 | show, 58 | loading, 59 | close, 60 | }; 61 | }]); -------------------------------------------------------------------------------- /plugins/webgui/public/views/skin/fs_dinosaur/README.md: -------------------------------------------------------------------------------- 1 | ## t-rex-runner 2 | 3 | the trex runner game extracted from chrome offline err page. 4 | 5 | see the [source](https://cs.chromium.org/chromium/src/components/neterror/resources/offline.js?q=t-rex+package:%5Echromium$&dr=C&l=7) from chromium 6 | 7 | 8 | [go and enjoy! :smile: ](http://wayou.github.io/t-rex-runner/) 9 | 10 | ![chrome offline game cast](assets/screenshot.gif) 11 | 12 | ## Interesting Forks/In Chinese, we call it 「花样玩法」 13 | 14 | - [vianroyal](https://github.com/vianroyal)/[t-rex-runner](https://github.com/vianroyal/t-rex-runner) [Kumamon runner](http://vianroyal.github.io/t-rex-runner/) 15 |
16 | 17 | ![](assets/kumamon-runner.gif) 18 | 19 | - [xkuga](https://github.com/xkuga)/[t-rex-runner](https://github.com/xkuga/t-rex-runner) [Hello KuGou](http://hellokugou.com/) 20 |
21 | 22 | ![](assets/hello-kugou.gif) 23 | 24 | - [d-nery](https://github.com/d-nery/)/[t-rex-runner](https://github.com/d-nery/t-rex-runner) [Novas coisas](http://d-nery.github.io/t-rex-runner/) 25 |
26 | 27 | ![](assets/novas-coisas.gif) 28 | 29 | - [chirag64](https://github.com/chirag64)/[t-rex-runner-bot](https://github.com/chirag64/t-rex-runner-bot) [t-rex runner bot](https://chirag64.github.io/t-rex-runner-bot/) 30 |
31 | 32 | ![](assets/t-rex-runner-bot.gif) 33 | 34 | - [19janil](https://github.com/19janil)/[t-rex-runner](https://github.com/19janil/t-rex-runner) [t-rex runner](https://19janil.github.io/t-rex-runner/) 35 |
36 | 37 | ![](assets/t-rex-runner-19janil.gif) 38 | 39 | - [enthus1ast](https://github.com/enthus1ast)/[chromeTrip](https://github.com/enthus1ast/chromeTrip) [Chrome Trip by code0](https://code0.itch.io/chrome-trip) 40 |
41 | 42 | ![](https://user-images.githubusercontent.com/13794470/37289691-964618be-260a-11e8-8c4a-6df04d6c490d.gif) 43 | 44 | 45 | -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/language.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('languageDialog' , [ '$mdDialog', $mdDialog => { 6 | const publicInfo = {}; 7 | const hide = () => { 8 | return $mdDialog.hide() 9 | .then(success => { 10 | dialogPromise = null; 11 | return; 12 | }).catch(err => { 13 | dialogPromise = null; 14 | return; 15 | }); 16 | }; 17 | publicInfo.hide = hide; 18 | let dialogPromise = null; 19 | const isDialogShow = () => { 20 | if(dialogPromise && !dialogPromise.$$state.status) { 21 | return true; 22 | } 23 | return false; 24 | }; 25 | const dialog = { 26 | templateUrl: `${ cdn }/public/views/dialog/language.html`, 27 | escapeToClose: false, 28 | locals: { bind: publicInfo }, 29 | bindToController: true, 30 | controller: ['$scope', '$translate', '$localStorage', 'bind', function($scope, $translate, $localStorage, bind) { 31 | $scope.publicInfo = bind; 32 | $scope.publicInfo.myLanguage = $localStorage.language || navigator.language || 'zh-CN'; 33 | $scope.chooseLanguage = () => { 34 | $translate.use($scope.publicInfo.myLanguage); 35 | $localStorage.language = $scope.publicInfo.myLanguage; 36 | $scope.publicInfo.hide(); 37 | }; 38 | $scope.languages = [ 39 | { id: 'zh-CN', name: '中文' }, 40 | { id: 'ja-JP', name: '日本語' }, 41 | { id: 'en-US', name: 'English' }, 42 | { id: 'ru-RU', name: 'Русский' }, 43 | ]; 44 | }], 45 | clickOutsideToClose: true, 46 | }; 47 | const show = () => { 48 | if(isDialogShow()) { 49 | return dialogPromise; 50 | } 51 | dialogPromise = $mdDialog.show(dialog); 52 | return dialogPromise; 53 | }; 54 | return { 55 | show, 56 | hide, 57 | }; 58 | }]); -------------------------------------------------------------------------------- /init/log.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const os = require('os'); 3 | const path = require('path'); 4 | const ssmgrPath = path.resolve(os.homedir(), '.ssmgr'); 5 | const logPath = path.resolve(os.homedir(), '.ssmgr', 'logs'); 6 | const log4js = require('log4js'); 7 | 8 | const category = [ 9 | 'system', 10 | 'account', 11 | 'email', 12 | 'telegram', 13 | 'freeAccount', 14 | 'webgui', 15 | 'alipay', 16 | 'express', 17 | 'flowSaver', 18 | 'paypal', 19 | 'giftcard', 20 | 'autoban', 21 | ]; 22 | 23 | const configure = { 24 | appenders: { 25 | console: { type: 'console' }, 26 | filter: { type: 'logLevelFilter', appender: 'console', level: 'debug' } 27 | }, 28 | categories: { 29 | default: { appenders: [ 'console' ], level: 'debug' }, 30 | } 31 | }; 32 | 33 | log4js.configure(configure); 34 | 35 | const setConsoleLevel = level => { 36 | configure.appenders.filter = { type: 'logLevelFilter', appender: 'console', level }; 37 | log4js.configure(configure); 38 | }; 39 | 40 | const setFileAppenders = (filename) => { 41 | try { 42 | fs.statSync(ssmgrPath); 43 | } catch(err) { 44 | fs.mkdirSync(ssmgrPath); 45 | } 46 | try { 47 | fs.statSync(logPath); 48 | } catch(err) { 49 | fs.mkdirSync(logPath); 50 | } 51 | try { 52 | fs.statSync(path.resolve(logPath, filename)); 53 | } catch(err) { 54 | fs.mkdirSync(path.resolve(logPath, filename)); 55 | } 56 | for(const ctg of category) { 57 | configure.appenders[ctg] = { 58 | type: 'dateFile', 59 | filename: path.resolve(logPath, filename, ctg + '.log'), 60 | pattern: '-yyyy-MM-dd', 61 | compress: true, 62 | }; 63 | configure.categories[ctg] = { appenders: [ ctg, 'filter' ], level: 'debug' }; 64 | } 65 | log4js.configure(configure); 66 | }; 67 | 68 | exports.setConsoleLevel = setConsoleLevel; 69 | exports.setFileAppenders = setFileAppenders; 70 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/account.html: -------------------------------------------------------------------------------- 1 |
共 {{accountInfo.originalAccount.length}} 条,显示 {{accountInfo.account.length}} 条
2 |
3 |
4 | 5 | 6 |
7 |
{{a.port}}
8 |
9 | {{a.user || a.password}} 10 |
chevron_right
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 |
22 |
{{ a.mac | mac }}
23 |
{{ a.port }}
24 |
25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /plugins/group/db/group.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'group'; 3 | 4 | const addDefaultGroup = async () => { 5 | const data = await knex('group') 6 | .where({ id: 0 }) 7 | .then(s => s[0]); 8 | if (!data) { 9 | const id = await knex('group').insert( 10 | { id: 0, name: '默认组', comment: '系统默认分组' }, 11 | 'id', 12 | ); 13 | if (id[0] !== 0) { 14 | await knex('group') 15 | .update({ id: 0 }) 16 | .where({ id: id[0] }); 17 | } 18 | } 19 | return; 20 | }; 21 | 22 | const createTable = async () => { 23 | const exist = await knex.schema.hasTable(tableName); 24 | if (exist) { 25 | await addDefaultGroup(); 26 | const hasShowNotice = await knex.schema.hasColumn(tableName, 'showNotice'); 27 | if (!hasShowNotice) { 28 | await knex.schema.table(tableName, function(table) { 29 | table.integer('showNotice').defaultTo(1); 30 | }); 31 | } 32 | const hasOrder = await knex.schema.hasColumn(tableName, 'order'); 33 | if (!hasOrder) { 34 | await knex.schema.table(tableName, function(table) { 35 | table.string('order'); 36 | }); 37 | } 38 | const hasMultiAccount = await knex.schema.hasColumn( 39 | tableName, 40 | 'multiAccount', 41 | ); 42 | if (!hasMultiAccount) { 43 | await knex.schema.table(tableName, function(table) { 44 | table.integer('multiAccount').defaultTo(0); 45 | }); 46 | } 47 | return; 48 | } 49 | await knex.schema.createTable(tableName, function(table) { 50 | table.increments('id'); 51 | table.string('name'); 52 | table.string('comment'); 53 | table.integer('showNotice').defaultTo(1); 54 | table.string('order'); 55 | table.integer('multiAccount').defaultTo(0); 56 | }); 57 | await addDefaultGroup(); 58 | return; 59 | }; 60 | 61 | exports.createTable = createTable; 62 | -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/setUserGroup.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('setGroupDialog' , [ '$mdDialog', $mdDialog => { 6 | const publicInfo = {}; 7 | const hide = () => { 8 | return $mdDialog.hide() 9 | .then(success => { 10 | dialogPromise = null; 11 | return; 12 | }).catch(err => { 13 | dialogPromise = null; 14 | return; 15 | }); 16 | }; 17 | publicInfo.hide = hide; 18 | let dialogPromise = null; 19 | const isDialogShow = () => { 20 | if(dialogPromise && !dialogPromise.$$state.status) { 21 | return true; 22 | } 23 | return false; 24 | }; 25 | const dialog = { 26 | templateUrl: `${ cdn }/public/views/dialog/setUserGroup.html`, 27 | escapeToClose: false, 28 | locals: { bind: publicInfo }, 29 | bindToController: true, 30 | controller: ['$scope', '$http', 'bind', function($scope, $http, bind) { 31 | $scope.publicInfo = bind; 32 | $scope.groups = []; 33 | $scope.publicInfo.isLoading = true; 34 | $http.get('/api/admin/group').then(success => { 35 | $scope.groups = success.data; 36 | $scope.publicInfo.isLoading = false; 37 | }); 38 | $scope.publicInfo.setGroup = () => { 39 | $http.post(`/api/admin/group/${ $scope.publicInfo.groupId }/${ $scope.publicInfo.userId }`) 40 | .then(success => { 41 | $scope.publicInfo.hide(); 42 | }); 43 | }; 44 | }], 45 | clickOutsideToClose: true, 46 | }; 47 | const show = (userId, groupId) => { 48 | publicInfo.userId = userId; 49 | publicInfo.groupId = groupId; 50 | if(isDialogShow()) { 51 | return dialogPromise; 52 | } 53 | dialogPromise = $mdDialog.show(dialog); 54 | return dialogPromise; 55 | }; 56 | return { 57 | show, 58 | hide, 59 | }; 60 | }]); -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/changePassword.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('changePasswordDialog', [ '$mdDialog', 'userApi', ($mdDialog, userApi) => { 6 | const publicInfo = { 7 | status: 'show', 8 | }; 9 | let dialogPromise = null; 10 | const isDialogShow = () => { 11 | if(dialogPromise && !dialogPromise.$$state.status) { 12 | return true; 13 | } 14 | return false; 15 | }; 16 | const show = (accountId, password) => { 17 | if(isDialogShow()) { 18 | return dialogPromise; 19 | } 20 | publicInfo.status = 'show'; 21 | publicInfo.accountId = accountId; 22 | publicInfo.password = password; 23 | dialogPromise = $mdDialog.show(dialog); 24 | return dialogPromise; 25 | }; 26 | const close = () => { 27 | return $mdDialog.hide() 28 | .then(success => { 29 | dialogPromise = null; 30 | return; 31 | }).catch(err => { 32 | dialogPromise = null; 33 | return; 34 | }); 35 | }; 36 | const changePassword = () => { 37 | if(!publicInfo.password) { return; } 38 | publicInfo.status = 'loading'; 39 | userApi.changeShadowsocksPassword(publicInfo.accountId, publicInfo.password) 40 | .then(() => { 41 | publicInfo.status = 'success'; 42 | }) 43 | .catch(() => { 44 | publicInfo.status = 'error'; 45 | }); 46 | }; 47 | publicInfo.close = close; 48 | publicInfo.changePassword = changePassword; 49 | const dialog = { 50 | templateUrl: `${ cdn }/public/views/dialog/changePassword.html`, 51 | escapeToClose: false, 52 | locals: { bind: publicInfo }, 53 | bindToController: true, 54 | controller: ['$scope', 'bind', ($scope, bind) => { 55 | $scope.publicInfo = bind; 56 | }], 57 | clickOutsideToClose: false, 58 | }; 59 | return { 60 | show, 61 | }; 62 | }]); -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # 配置 2 | 3 | ## 配置首个节点 4 | 5 | 1. 创建配置文件 6 | 7 | 在`~/.ssmgr`目录下创建配置文件,支持 yaml 和 json 两种格式,使用 yaml 格式请注意保证正确的缩进 8 | 9 | ```yaml 10 | type: s 11 | shadowsocks: 12 | address: 127.0.0.1:6001 13 | manager: 14 | address: 0.0.0.0:6002 15 | password: '123456' 16 | db: 'db.sqlite' 17 | ``` 18 | 19 | ```json 20 | { 21 | "type": "s", 22 | "shadowsocks": { 23 | "address": "127.0.0.1:6001" 24 | }, 25 | "manager": { 26 | "address": "0.0.0.0:6002", 27 | "password": "123456" 28 | }, 29 | "db": "db.sqlite" 30 | } 31 | ``` 32 | 33 | 2. 运行 shadowsocks 34 | 35 | 两种版本的命令有一些差异,都要保证`--manager-address`的参数和上一步配置文件一致 36 | 37 | - libev `ss-manager -m aes-256-cfb -u --manager-address 127.0.0.1:6001` 38 | - python `ssserver -m aes-256-cfb -p 12345 -k abcedf --manager-address 127.0.0.1:6001` 39 | 40 | 41 | 3. 调用刚刚的配置文件运行 ssmgr 42 | 43 | `ssmgr -c /your/node/config/file` 44 | 45 | !> 此处需要让程序后台运行,关于后台运行的方法请参考`pm2`、`byobu`等工具 46 | 47 | ## 配置并运行Web界面 48 | 49 | 创建配置文件,将`1.1.1.1`替换成节点的实际IP地址 50 | 51 | ```yaml 52 | type: m 53 | manager: 54 | address: 1.1.1.1:6002 55 | password: '123456' 56 | plugins: 57 | flowSaver: 58 | use: true 59 | user: 60 | use: true 61 | account: 62 | use: true 63 | email: 64 | use: true 65 | type: 'smtp' 66 | username: 'username' 67 | password: 'password' 68 | host: 'smtp.your-email.com' 69 | webgui: 70 | use: true 71 | host: '0.0.0.0' 72 | port: '80' 73 | site: 'http://yourwebsite.com' 74 | db: 'webgui.sqlite' 75 | ``` 76 | 77 | !> email 部分用于给注册用户发送验证邮件,请填写正确的参数并选用支持 smtp 的 vps 78 | 79 | !> site 字段填写网站的实际访问地址 80 | 81 | 调用此配置文件运行: 82 | 83 | `ssmgr -c /your/webgui/config/file` 84 | 85 | 若一切正常,便可看到主界面: 86 | 87 | ![](/_media/home.png) 88 | 89 | !> 成功运行后,首个注册用户为管理员 90 | 91 | 92 | 93 | ## 配置更多的节点 94 | 95 | 1. 仿照上文的步骤,创建配置文件,运行`shadowsocks`和`ssmgr` 96 | 97 | 2. 在管理界面的“服务器”页面,点击右下角“+”按钮,填上对应的地址、端口、密码、加密方式 -------------------------------------------------------------------------------- /init/loadPlugins.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const config = appRequire('services/config').all(); 4 | 5 | const log4js = require('log4js'); 6 | const logger = log4js.getLogger('system'); 7 | 8 | const pluginLists = []; 9 | 10 | const loadOnePluginDb = name => { 11 | const promises = []; 12 | logger.info(`Load plugin db: [ ${ name } ]`); 13 | try { 14 | const files = fs.readdirSync(path.resolve(__dirname, `../plugins/${ name }`)); 15 | if(files.indexOf('db') >= 0) { 16 | const dbFiles = fs.readdirSync(path.resolve(__dirname, `../plugins/${ name }/db`)); 17 | dbFiles.forEach(f => { 18 | logger.info(`Load plugin db: [ ${ name }/db/${ f } ]`); 19 | promises.push(appRequire(`plugins/${ name }/db/${ f }`).createTable()); 20 | }); 21 | } 22 | } catch(err) { 23 | logger.error(err); 24 | } 25 | return Promise.all(promises).then(() => { 26 | const dependence = appRequire(`plugins/${ name }/dependence`); 27 | logger.info(`Load plugin dependence: [ ${ name } ]`); 28 | dependence.forEach(pluginName => { 29 | if(pluginLists.indexOf(pluginName) < 0) { 30 | pluginLists.push(pluginName); 31 | } 32 | }); 33 | }).catch(err => { 34 | // logger.error(err); 35 | }); 36 | }; 37 | 38 | const loadOnePlugin = name => { 39 | logger.info(`Load plugin: [ ${ name } ]`); 40 | appRequire(`plugins/${ name }/index`); 41 | }; 42 | 43 | const loadPlugins = () => { 44 | if(!config.plugins) { 45 | return; 46 | } 47 | if(config.type !== 'm') { 48 | return; 49 | } 50 | for(const name in config.plugins) { 51 | if(config.plugins[name].use) { 52 | pluginLists.push(name); 53 | } 54 | } 55 | (async () => { 56 | for(let pl of pluginLists) { 57 | await loadOnePluginDb(pl); 58 | } 59 | for(let pl of pluginLists) { 60 | loadOnePlugin(pl); 61 | } 62 | })(); 63 | }; 64 | loadPlugins(); 65 | -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/user.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('userSortDialog' , [ '$mdDialog', '$http', ($mdDialog, $http) => { 6 | const publicInfo = {}; 7 | $http.get('/api/admin/group').then(success => { 8 | publicInfo.groups = success.data; 9 | publicInfo.groups.unshift({ id: -1, name: '所有组', comment: '' }); 10 | }); 11 | const hide = () => { 12 | return $mdDialog.hide() 13 | .then(success => { 14 | dialogPromise = null; 15 | return; 16 | }).catch(err => { 17 | dialogPromise = null; 18 | return; 19 | }); 20 | }; 21 | publicInfo.hide = hide; 22 | let dialogPromise = null; 23 | const isDialogShow = () => { 24 | if(dialogPromise && !dialogPromise.$$state.status) { 25 | return true; 26 | } 27 | return false; 28 | }; 29 | const dialog = { 30 | templateUrl: `${ cdn }/public/views/admin/userSortDialog.html`, 31 | escapeToClose: false, 32 | locals: { bind: publicInfo }, 33 | bindToController: true, 34 | controller: ['$scope', '$mdDialog', '$localStorage', 'bind', '$mdMedia', function($scope, $mdDialog, $localStorage, bind, $mdMedia) { 35 | $scope.publicInfo = bind; 36 | $scope.userSort = $localStorage.admin.userSortSettings; 37 | if(!$scope.userSort.type) { 38 | $scope.userSort.type = {}; 39 | } 40 | $scope.setDialogWidth = () => { 41 | if($mdMedia('xs') || $mdMedia('sm')) { 42 | return {}; 43 | } 44 | return { 'min-width': '350px' }; 45 | }; 46 | }], 47 | clickOutsideToClose: true, 48 | }; 49 | const show = id => { 50 | publicInfo.id = id; 51 | if(isDialogShow()) { 52 | return dialogPromise; 53 | } 54 | dialogPromise = $mdDialog.show(dialog); 55 | return dialogPromise; 56 | }; 57 | return { 58 | show, 59 | hide, 60 | }; 61 | }]); -------------------------------------------------------------------------------- /plugins/group/index.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | 3 | const getGroups = () => { 4 | return knex('group').where({}).orderBy('id').then(s => s); 5 | }; 6 | 7 | const getGroupsAndUserNumber = async () => { 8 | const groups = await knex('group').select([ 9 | 'group.id as id', 10 | 'group.name as name', 11 | 'group.comment as comment', 12 | knex.raw('count(user.id) as userNumber'), 13 | ]) 14 | .leftJoin('user', 'user.group', 'group.id') 15 | .where('user.id', '>', 1) 16 | .orWhereNull('user.id') 17 | .groupBy('group.id'); 18 | return groups; 19 | }; 20 | 21 | const getOneGroup = id => { 22 | return knex('group').select().where({ id }).then(success => { 23 | if(!success.length) { return Promise.reject('group not found'); } 24 | return success[0]; 25 | }); 26 | }; 27 | 28 | const addGroup = (name, comment, showNotice, order, multiAccount) => { 29 | return knex('group').insert({ 30 | name, comment, showNotice, order, multiAccount 31 | }); 32 | }; 33 | 34 | const editGroup = (id, name, comment, showNotice, order, multiAccount) => { 35 | return knex('group').update({ 36 | name, 37 | comment, 38 | showNotice, 39 | order, 40 | multiAccount, 41 | }).where({ id }); 42 | }; 43 | 44 | const deleteGroup = async id => { 45 | if(id === 0) { return; } 46 | const users = await knex('user').where({ group: id }); 47 | if(users.length > 0) { return Promise.reject('Can not delete group'); } 48 | await knex('group').delete().where({ id }); 49 | return; 50 | }; 51 | 52 | const setUserGroup = (groupId, userId) => { 53 | return knex('user').update({ group: groupId }).where({ id: userId }); 54 | }; 55 | 56 | exports.getGroups = getGroups; 57 | exports.getGroupsAndUserNumber = getGroupsAndUserNumber; 58 | exports.getOneGroup = getOneGroup; 59 | exports.addGroup = addGroup; 60 | exports.editGroup = editGroup; 61 | exports.deleteGroup = deleteGroup; 62 | exports.setUserGroup = setUserGroup; -------------------------------------------------------------------------------- /plugins/telegram/flow.js: -------------------------------------------------------------------------------- 1 | const telegram = appRequire('plugins/telegram/index').telegram; 2 | const managerAddress = appRequire('plugins/telegram/managerAddress'); 3 | const flow = appRequire('plugins/telegram/flowSaver'); 4 | 5 | const log4js = require('log4js'); 6 | const logger = log4js.getLogger('telegram'); 7 | 8 | const flowNumber = (number) => { 9 | if(number < 1000) return number + ' B'; 10 | else if(number < 1000 * 1000) return (number / 1000).toFixed(0) + ' KB'; 11 | else if(number < 1000 * 1000 * 1000) return (number / 1000000).toFixed(1) + ' MB'; 12 | else if(number < 1000 * 1000 * 1000 * 1000) return (number / 1000000000).toFixed(3) + ' GB'; 13 | }; 14 | 15 | const getFlow = (message, time) => { 16 | const start = Date.now() - time; 17 | const end = Date.now(); 18 | flow.getFlow(managerAddress.get().host, managerAddress.get().port, start, end) 19 | .then(ports => { 20 | let str = ''; 21 | if(ports.length === 0) { 22 | str = 'No flows.'; 23 | } else { 24 | str += `${managerAddress.get().host}:${managerAddress.get().port}\n\n`; 25 | ports.forEach(port => { 26 | str += port.port + ', ' + flowNumber(port.sumFlow) + '\n'; 27 | }); 28 | } 29 | telegram.emit('send', message, str); 30 | }).catch(err => { 31 | logger.error(err); 32 | }); 33 | }; 34 | 35 | telegram.on('manager', message => { 36 | 37 | const minReg = new RegExp(/^flow(\d{0,2})min$/); 38 | const hourReg = new RegExp(/^flow(\d{0,2})hour$/); 39 | 40 | if(message.message.text === 'flow') { 41 | getFlow(message, 10 * 60 * 1000); 42 | } else if(message.message.text.match(minReg)) { 43 | const reg = message.message.text.match(minReg); 44 | const time = reg[1] * 60 * 1000; 45 | getFlow(message, time); 46 | } else if(message.message.text.match(hourReg)) { 47 | const reg = message.message.text.match(hourReg); 48 | const time = reg[1] * 60 * 60 * 1000; 49 | getFlow(message, time); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/addMacAccount.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('addMacAccountDialog' , [ '$q', '$mdDialog', '$http', ($q, $mdDialog, $http) => { 6 | const publicInfo = { mac: '' }; 7 | publicInfo.isLoading = false; 8 | let alertDialogPromise = null; 9 | const isDialogShow = () => { 10 | if(alertDialogPromise && !alertDialogPromise.$$state.status) { 11 | return true; 12 | } 13 | return false; 14 | }; 15 | const close = () => { 16 | return $mdDialog.hide().then(success => { 17 | publicInfo.isLoading = false; 18 | alertDialogPromise = null; 19 | return; 20 | }).catch(err => { 21 | publicInfo.isLoading = false; 22 | alertDialogPromise = null; 23 | return; 24 | }); 25 | }; 26 | publicInfo.close = close; 27 | const dialog = { 28 | templateUrl: `${ cdn }/public/views/dialog/addMacAccount.html`, 29 | escapeToClose: false, 30 | locals: { bind: publicInfo }, 31 | bindToController: true, 32 | controller: ['$scope', '$mdDialog', 'bind', function($scope, $mdDialog, bind) { 33 | $scope.publicInfo = bind; 34 | }], 35 | clickOutsideToClose: true, 36 | }; 37 | const show = () => { 38 | if(isDialogShow()) { 39 | publicInfo.isLoading = false; 40 | return alertDialogPromise; 41 | } 42 | alertDialogPromise = $mdDialog.show(dialog); 43 | return alertDialogPromise; 44 | }; 45 | const loading = () => { 46 | publicInfo.isLoading = true; 47 | if(!isDialogShow()) { 48 | return show(); 49 | } 50 | return $q.resolve(); 51 | }; 52 | const addMac = () => { 53 | $http.post('/api/user/account/mac', { 54 | mac: publicInfo.mac 55 | }).then(success => { 56 | close(); 57 | }).catch(err => { 58 | close(); 59 | }); 60 | }; 61 | publicInfo.addMac = addMac; 62 | return { 63 | show, 64 | close, 65 | }; 66 | }]); -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/addUser.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 9 | 普通用户 10 | 管理员 11 | 12 | 13 | 14 | 15 |
16 |
邮箱不能为空
17 |
18 |
19 | 20 | 21 | 22 |
23 |
密码不能为空
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 取消 34 | 确认 35 |
36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/refCodeList.html: -------------------------------------------------------------------------------- 1 |
当前没有生成邀请码
2 |
共 {{ total }} 个邀请码
3 |
4 |
5 | 6 | 7 |
8 |
{{ c.code }}
{{ c.email }}
9 |
10 | {{ c.visit }} / {{ c.count }} / {{ c.maxUser }} 11 |
chevron_right
12 |
13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 |
32 |
33 |
-------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/accountSortAndFilterDialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
排序方式:
5 |
6 | 7 |
8 | 端口号 ↑ 9 |
10 | 过期时间 ↑ 11 |
12 |
13 | 端口号 ↓ 14 |
15 | 过期时间 ↓ 16 |
17 |
18 |
19 |
过滤条件:
20 |
21 |
22 | 未过期 23 | 已过期 24 |
25 |
26 | 无期限 27 | MAC地址 28 |
29 |
30 |
31 |
32 | 33 | 34 | 确定 35 | 36 | 37 |
38 | -------------------------------------------------------------------------------- /plugins/webgui_telegram/flow.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const moment = require('moment'); 3 | const flow = appRequire('plugins/flowSaver/flow'); 4 | const tg = appRequire('plugins/webgui_telegram/index'); 5 | const telegram = appRequire('plugins/webgui_telegram/index').telegram; 6 | const cron = appRequire('init/cron'); 7 | const log4js = require('log4js'); 8 | const logger = log4js.getLogger('telegram'); 9 | 10 | const getUserAccount = userId => { 11 | return knex('account_plugin').where({ 12 | userId 13 | }); 14 | }; 15 | 16 | const prettyFlow = number => { 17 | if(number >= 0 && number < 1000) { 18 | return number + ' B'; 19 | } else if(number >= 1000 && number < 1000 * 1000) { 20 | return (number / 1000).toFixed(1) + ' KB'; 21 | } else if(number >= 1000 * 1000 && number < 1000 * 1000 * 1000) { 22 | return (number / (1000 * 1000)).toFixed(2) + ' MB'; 23 | } else if(number >= 1000 * 1000 * 1000 && number < 1000 * 1000 * 1000 * 1000) { 24 | return (number / (1000 * 1000 * 1000)).toFixed(3) + ' GB'; 25 | } else if(number >= 1000 * 1000 * 1000 * 1000 && number < 1000 * 1000 * 1000 * 1000 * 1000) { 26 | return (number / (1000 * 1000 * 1000 * 1000)).toFixed(3) + ' TB'; 27 | } else { 28 | return number + ''; 29 | } 30 | }; 31 | 32 | const getUsers = async () => { 33 | const users = await knex('user').where({ type: 'normal' }).whereNotNull('telegram'); 34 | users.forEach(async user => { 35 | const accounts = await getUserAccount(user.id); 36 | const start = moment().add(-1, 'd').hour(0).minute(0).second(0).millisecond(0).toDate().getTime(); 37 | const end = moment().hour(0).minute(0).second(0).millisecond(0).toDate().getTime(); 38 | accounts.forEach(async account => { 39 | const myFlow = await flow.getFlowFromSplitTime(null, account.id, start, end); 40 | const message = `昨日流量统计:[${ account.port }] ${ prettyFlow(myFlow) }`; 41 | logger.info(message); 42 | // telegram.emit('send', +user.telegram, message); 43 | tg.sendMessage(message, +user.telegram); 44 | }); 45 | }); 46 | }; 47 | 48 | cron.cron(getUsers, '0 9 * * *'); -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/newNotice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{ group.name }} 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 |
24 |
自动弹出
25 |
26 | 27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 取消 38 | 预览 39 | 保存 40 |
41 |
42 |
-------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/editNotice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{ group.name }} 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 |
24 |
自动弹出
25 |
26 | 27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 删除 38 | 预览 39 | 保存 40 |
41 |
42 |
-------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/changePassword.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | 修改登录密码 8 |
9 | 10 | 11 | 12 |
13 |
密码不能为空
14 |
15 |
16 | 17 | 18 | 19 |
20 |
密码不能为空
21 |
22 |
23 | 24 | 25 | 26 |
27 |
密码不能为空
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 确认 38 |
39 |
40 |
41 |
-------------------------------------------------------------------------------- /plugins/webgui/public/styles/default.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url(/libs/MaterialIcons-Regular.eot); 6 | /* For IE6-8 */ 7 | src: local('Material Icons'), 8 | local('MaterialIcons-Regular'), 9 | url(/libs/MaterialIcons-Regular.woff2) format('woff2'), 10 | url(/libs/MaterialIcons-Regular.woff) format('woff'), 11 | url(/libs/MaterialIcons-Regular.ttf) format('truetype'); 12 | } 13 | 14 | .material-icons { 15 | font-family: 'Material Icons'; 16 | font-weight: normal; 17 | font-style: normal; 18 | font-size: 24px; 19 | /* Preferred icon size */ 20 | display: inline-block; 21 | line-height: 1; 22 | text-transform: none; 23 | letter-spacing: normal; 24 | word-wrap: normal; 25 | white-space: nowrap; 26 | direction: ltr; 27 | /* Support for all WebKit browsers. */ 28 | -webkit-font-smoothing: antialiased; 29 | /* Support for Safari and Chrome. */ 30 | text-rendering: optimizeLegibility; 31 | /* Support for Firefox. */ 32 | -moz-osx-font-smoothing: grayscale; 33 | /* Support for IE. */ 34 | font-feature-settings: 'liga'; 35 | } 36 | 37 | .animate-show { 38 | line-height: 20px; 39 | opacity: 1; 40 | padding: 30px; 41 | /*border: 1px solid black;*/ 42 | /*background: white;*/ 43 | } 44 | 45 | .animate-show.ng-hide-add, .animate-show.ng-hide-remove { 46 | transition: all linear 0.5s; 47 | } 48 | 49 | .animate-show.ng-hide { 50 | line-height: 0; 51 | opacity: 0; 52 | padding: 30 0px; 53 | } 54 | 55 | .check-element { 56 | padding: 10px; 57 | border: 1px solid black; 58 | background: white; 59 | } 60 | 61 | /*md-sidenav, 62 | md-sidenav.md-locked-open, 63 | md-sidenav.md-closed.md-locked-open-add-active { 64 | min-width: 200px !important; 65 | width: 85vw !important; 66 | max-width: 210px !important; 67 | }*/ 68 | 69 | .search-input > input { 70 | color: #FFF; 71 | } 72 | .search-input > .md-errors-spacer { 73 | min-height: 0px; 74 | } 75 | 76 | .markdown-table table { 77 | border-collapse: collapse; 78 | } 79 | 80 | .markdown-table table, .markdown-table th, .markdown-table td { 81 | padding: 5px; 82 | border: 1px solid #999999; 83 | } 84 | -------------------------------------------------------------------------------- /plugins/flowSaver/db/server.js: -------------------------------------------------------------------------------- 1 | const knex = appRequire('init/knex').knex; 2 | const tableName = 'server'; 3 | const config = appRequire('services/config').all(); 4 | const manager = appRequire('services/manager'); 5 | const log4js = require('log4js'); 6 | const logger = log4js.getLogger('flowSaver'); 7 | 8 | const createTable = async () => { 9 | const exist = await knex.schema.hasTable(tableName); 10 | if(exist) { 11 | const hasType = await knex.schema.hasColumn(tableName, 'type'); 12 | if(!hasType) { 13 | await knex.schema.table(tableName, function(table) { 14 | table.string('type').defaultTo('Shadowsocks'); 15 | table.string('key'); 16 | table.string('net'); 17 | table.integer('wgPort'); 18 | }); 19 | } 20 | } else { 21 | await knex.schema.createTable(tableName, function(table) { 22 | table.increments('id'); 23 | table.string('type').defaultTo('Shadowsocks'); 24 | table.string('name'); 25 | table.string('host'); 26 | table.integer('port'); 27 | table.string('password'); 28 | table.float('scale').defaultTo(1); 29 | table.string('method').defaultTo('aes-256-cfb'); 30 | table.string('comment').defaultTo(''); 31 | table.integer('shift').defaultTo(0); 32 | table.string('key'); 33 | table.string('net'); 34 | table.integer('wgPort'); 35 | }); 36 | } 37 | const list = await knex('server').select(['name', 'host', 'port', 'password']); 38 | if(list.length === 0) { 39 | const host = config.manager.address.split(':')[0]; 40 | const port = +config.manager.address.split(':')[1]; 41 | const password = config.manager.password; 42 | await manager.send({ 43 | command: 'flow', 44 | options: { 45 | clear: false, 46 | }, 47 | }, { 48 | host, 49 | port, 50 | password, 51 | }).catch(() => { 52 | logger.error(`connect to server ${ password }@${ host }:${ port } fail.`); 53 | process.exit(1); 54 | }); 55 | await knex('server').insert({ 56 | name: 'default', 57 | host, 58 | port, 59 | password, 60 | }); 61 | } 62 | return; 63 | }; 64 | 65 | exports.createTable = createTable; 66 | -------------------------------------------------------------------------------- /plugins/freeAccount/README.md: -------------------------------------------------------------------------------- 1 | # freeAccount plugin 2 | 3 | This plugin creates a website to share shadowsocks for free. 4 | 5 | ## Usage 6 | 7 | ### Quick start with docker 8 | 9 | 1. Make a config folder, and create file `default.yml` `free.yml` in it: 10 | 11 | ``` 12 | default.yml: 13 | 14 | type: s 15 | shadowsocks: 16 | address: 127.0.0.1:6001 17 | manager: 18 | address: 127.0.0.1:6002 19 | password: '123456' 20 | db: 'db.sqlite' 21 | 22 | -------------------- 23 | 24 | free.yml: 25 | 26 | type: m 27 | manager: 28 | address: 127.0.0.1:6002 29 | password: '123456' 30 | plugins: 31 | freeAccount: 32 | use: true 33 | port: 12345 34 | # port value can be a range: '1000-2000,2003,2005-2009' 35 | flow: 500000000 36 | # or 500M, 500K, 500G 37 | time: 3600000 38 | # or 30m, 2h 39 | address: 'free.ssmgr.top' 40 | method: 'aes-256-cfb' 41 | listen: '0.0.0.0:80' 42 | db: 'free.sqlite' 43 | ``` 44 | 45 | 2. run this command, the ports depends on your `free.yml` file: 46 | 47 | ``` 48 | docker run --name types -idt -v /your/config/file/path:/root/.ssmgr --net=host gyteng/ssmgr ssmgr -c default.yml -r 49 | docker run --name typem -idt -v /your/config/file/path:/root/.ssmgr --net=host gyteng/ssmgr ssmgr -c free.yml 50 | ``` 51 | 52 | ### Start in normal way 53 | 54 | 1. Start `ssmgr` with type s, you can read the guide [here](https://github.com/shadowsocks/shadowsocks-manager). 55 | 56 | 2. Create config file `~/.ssmgr/free.yml`: 57 | 58 | ``` 59 | type: m 60 | manager: 61 | address: 127.0.0.1:6002 62 | password: '123456' 63 | plugins: 64 | freeAccount: 65 | use: true 66 | port: 12345 67 | # port value can be a range: '1000-2000,2003,2005-2009' 68 | flow: 500000000 69 | time: 3600000 70 | address: 'free.ssmgr.top' 71 | method: 'aes-256-cfb' 72 | listen: '0.0.0.0:80' 73 | db: 'free.sqlite' 74 | ``` 75 | 76 | 3. run `ssmgr -c free.yml`, and you can visit the website. 77 | 78 | ## Demo 79 | 80 | [https://free.ssmgr.top](https://free.ssmgr.top) -------------------------------------------------------------------------------- /plugins/webgui/public/routes/user.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.config(['$stateProvider', $stateProvider => { 6 | $stateProvider 7 | .state('user', { 8 | url: '/user', 9 | abstract: true, 10 | templateUrl: `${ cdn }/public/views/user/user.html`, 11 | resolve: { 12 | myConfig: ['$http', 'configManager', ($http, configManager) => { 13 | if(configManager.getConfig().version) { return; } 14 | return $http.get('/api/home/login').then(success => { 15 | configManager.setConfig(success.data); 16 | }); 17 | }] 18 | }, 19 | }) 20 | .state('user.index', { 21 | url: '/index', 22 | controller: 'UserIndexController', 23 | templateUrl: `${ cdn }/public/views/user/index.html`, 24 | }) 25 | .state('user.account', { 26 | url: '/account', 27 | controller: 'UserAccountController', 28 | templateUrl: `${ cdn }/public/views/user/account.html`, 29 | }) 30 | .state('user.settings', { 31 | url: '/settings', 32 | controller: 'UserSettingsController', 33 | templateUrl: `${ cdn }/public/views/user/settings.html`, 34 | }) 35 | .state('user.changePassword', { 36 | url: '/changePassword', 37 | controller: 'UserChangePasswordController', 38 | templateUrl: `${ cdn }/public/views/user/changePassword.html`, 39 | }) 40 | .state('user.telegram', { 41 | url: '/telegram', 42 | controller: 'UserTelegramController', 43 | templateUrl: `${ cdn }/public/views/user/telegram.html`, 44 | }) 45 | .state('user.ref', { 46 | url: '/ref', 47 | controller: 'UserRefController', 48 | templateUrl: `${ cdn }/public/views/user/ref.html`, 49 | }) 50 | .state('user.order', { 51 | url: '/order', 52 | controller: 'UserOrderController', 53 | templateUrl: `${ cdn }/public/views/user/order.html`, 54 | }) 55 | .state('user.macAddress', { 56 | url: '/macAddress', 57 | controller: 'UserMacAddressController', 58 | templateUrl: `${ cdn }/public/views/user/macAddress.html`, 59 | }) 60 | ; 61 | }]) 62 | ; 63 | -------------------------------------------------------------------------------- /plugins/webgui/server/adminGroup.js: -------------------------------------------------------------------------------- 1 | const group = appRequire('plugins/group/index'); 2 | 3 | exports.getGroups = (req, res, next) => { 4 | group.getGroupsAndUserNumber().then(success => { 5 | res.send(success); 6 | }).catch(err => { 7 | console.log(err); 8 | res.status(403).end(); 9 | }); 10 | }; 11 | 12 | exports.getOneGroup = (req, res, next) => { 13 | const id = +req.params.id; 14 | group.getOneGroup(id).then(success => { 15 | res.send(success); 16 | }).catch(err => { 17 | console.log(err); 18 | res.status(403).end(); 19 | }); 20 | }; 21 | 22 | exports.addGroup = (req, res, next) => { 23 | const name = req.body.name; 24 | const comment = req.body.comment; 25 | const showNotice = !!req.body.showNotice; 26 | const order = req.body.order ? JSON.stringify(req.body.order) : null; 27 | const multiAccount = !!req.body.multiAccount; 28 | group.addGroup(name, comment, showNotice, order, multiAccount).then(success => { 29 | res.send(success); 30 | }).catch(err => { 31 | console.log(err); 32 | res.status(403).end(); 33 | }); 34 | }; 35 | 36 | exports.editGroup = (req, res, next) => { 37 | const id = +req.params.id; 38 | const name = req.body.name; 39 | const comment = req.body.comment; 40 | const showNotice = !!req.body.showNotice; 41 | const order = req.body.order ? JSON.stringify(req.body.order) : null; 42 | const multiAccount = !!req.body.multiAccount; 43 | group.editGroup(id, name, comment, showNotice, order, multiAccount).then(success => { 44 | res.send('success'); 45 | }).catch(err => { 46 | console.log(err); 47 | res.status(403).end(); 48 | }); 49 | }; 50 | 51 | exports.deleteGroup = (req, res, next) => { 52 | const id = +req.params.id; 53 | group.deleteGroup(id).then(success => { 54 | res.send('success'); 55 | }).catch(err => { 56 | console.log(err); 57 | res.status(403).end(); 58 | }); 59 | }; 60 | 61 | exports.setUserGroup = (req, res, next) => { 62 | const groupId = +req.params.groupId; 63 | const userId = +req.params.userId; 64 | group.setUserGroup(groupId, userId).then(success => { 65 | res.send('success'); 66 | }).catch(err => { 67 | console.log(err); 68 | res.status(403).end(); 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /plugins/webgui/public/views/admin/userSortDialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
排序方式:
5 |
6 | 7 |
8 | 用户ID ↑ 9 |
10 | 上次登录 ↑ 11 |
12 |
13 | 用户ID ↓ 14 |
15 | 上次登录 ↓ 16 |
17 |
18 |
19 |
用户类型:
20 |
21 |
22 | 普通 23 | 管理员 24 |
25 |
26 |
分组:
27 |
28 | 29 | 30 | {{ group.name }} 31 | 32 | 33 |
34 |
35 |
36 | 37 | 38 | 确定 39 | 40 | 41 |
42 | -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/confirm.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('confirmDialog' , [ '$mdDialog', ($mdDialog) => { 6 | const publicInfo = { status: 'show' }; 7 | let dialogPromise = null; 8 | const isDialogShow = () => { 9 | if(dialogPromise && !dialogPromise.$$state.status) { 10 | return true; 11 | } 12 | return false; 13 | }; 14 | const show = (options = {}) => { 15 | publicInfo.status = 'show'; 16 | const { text, cancel, confirm, error, fn, useFnErrorMessage } = options; 17 | publicInfo.text = text; 18 | publicInfo.cancel = cancel; 19 | publicInfo.confirm = confirm; 20 | publicInfo.error = error; 21 | publicInfo.useFnErrorMessage = useFnErrorMessage; 22 | publicInfo.fn = fn; 23 | if(isDialogShow()) { 24 | return dialogPromise; 25 | } 26 | dialogPromise = $mdDialog.show(dialog); 27 | return dialogPromise; 28 | }; 29 | const cancelFn = () => { 30 | return $mdDialog.cancel().then(success => { 31 | dialogPromise = null; 32 | return; 33 | }).catch(err => { 34 | dialogPromise = null; 35 | return; 36 | }); 37 | }; 38 | const hideFn = () => { 39 | return $mdDialog.hide().then(success => { 40 | dialogPromise = null; 41 | return; 42 | }).catch(err => { 43 | dialogPromise = null; 44 | return; 45 | }); 46 | }; 47 | publicInfo.cancelFn = cancelFn; 48 | const confirmFn = () => { 49 | publicInfo.status = 'loading'; 50 | publicInfo.fn().then(success => { 51 | hideFn(); 52 | }).catch(err => { 53 | publicInfo.status = 'error'; 54 | if(publicInfo.useFnErrorMessage) { 55 | publicInfo.error = err; 56 | } 57 | }); 58 | }; 59 | publicInfo.confirmFn = confirmFn; 60 | const dialog = { 61 | templateUrl: `${ cdn }/public/views/dialog/confirm.html`, 62 | escapeToClose: false, 63 | locals: { bind: publicInfo }, 64 | bindToController: true, 65 | controller: ['$scope', 'bind', function($scope, bind) { 66 | $scope.publicInfo = bind; 67 | }], 68 | clickOutsideToClose: false, 69 | }; 70 | return { 71 | show, 72 | }; 73 | }]); -------------------------------------------------------------------------------- /plugins/webgui/public/views/user/changePassword.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | 8 |
9 | 10 | 11 | 12 |
13 |
密码不能为空
14 |
15 |
16 | 17 | 18 | 19 |
20 |
密码不能为空
21 |
22 |
23 | 24 | 25 | 26 |
27 |
密码不能为空
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 确认 38 |
39 |
40 |
41 |
-------------------------------------------------------------------------------- /init/checkConfig.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const program = require('commander'); 4 | const version = appRequire('package').version; 5 | const log = appRequire('init/log'); 6 | 7 | const log4js = require('log4js'); 8 | const logger = log4js.getLogger('system'); 9 | 10 | const ssmgrPath = path.resolve(os.homedir(), './.ssmgr/'); 11 | 12 | program 13 | .version('shadowsocks-manager ' + version) 14 | .option('-c, --config [file]', 'config file, default: ~/.ssmgr/default.yml') 15 | .option('-d, --db [file]', 'sqlite3 file, sample: ~/.ssmgr/db.sqlite') 16 | .option('-t, --type [type]', 'type, s for server side, m for manager side') 17 | .option('-s, --shadowsocks [address]', 'ss-manager address, sample: 127.0.0.1:6001') 18 | .option('-m, --manager [address]', 'manager address, sample: 0.0.0.0:6002') 19 | .option('-p, --password [password]', 'manager password, both server side and manager side must be equals') 20 | .option('-r, --run [type]', 'run shadowsocks from child_process, sample: libev / libev:aes-256-cfb / python / python:aes-256-cfb') 21 | .option('--debug', 'show debug message') 22 | .parse(process.argv); 23 | 24 | if(program.config) { global.configFile = program.config; } 25 | 26 | if(!program.debug) { 27 | log.setConsoleLevel('ERROR'); 28 | } 29 | 30 | const config = appRequire('services/config'); 31 | let logName = 'uname'; 32 | 33 | if(program.type) {config.set('type', program.type);} 34 | if(program.shadowsocks) {config.set('shadowsocks.address', program.shadowsocks);} 35 | if(program.manager) {config.set('manager.address', program.manager);} 36 | if(program.password) {config.set('manager.password', program.password);} 37 | if(program.db) { 38 | config.set('db', program.db); 39 | } 40 | if (typeof config.get('db') === 'object') { 41 | logName = config.get('db.database'); 42 | } else { 43 | const dbpath = config.get('db'); 44 | logName = path.basename(dbpath).split('.')[0]; 45 | if (dbpath[0] === '/' || dbpath[0] === '.' || dbpath[0] === '~') { 46 | config.set('db', path.resolve(dbpath)); 47 | } else { 48 | config.set('db', path.resolve(ssmgrPath, dbpath)); 49 | } 50 | } 51 | log.setFileAppenders(logName); 52 | 53 | if(program.run) { 54 | config.set('runShadowsocks', program.run); 55 | } 56 | -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/ban.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('banDialog', [ '$mdDialog', ($mdDialog) => { 6 | const publicInfo = { 7 | banTime: 30, 8 | }; 9 | const hide = () => { 10 | return $mdDialog.hide() 11 | .then(success => { 12 | dialogPromise = null; 13 | return; 14 | }).catch(err => { 15 | dialogPromise = null; 16 | return; 17 | }); 18 | }; 19 | publicInfo.hide = hide; 20 | let dialogPromise = null; 21 | const isDialogShow = () => { 22 | if(dialogPromise && !dialogPromise.$$state.status) { 23 | return true; 24 | } 25 | return false; 26 | }; 27 | const dialog = { 28 | templateUrl: `${ cdn }/public/views/dialog/ban.html`, 29 | escapeToClose: false, 30 | locals: { bind: publicInfo }, 31 | bindToController: true, 32 | controller: ['$scope', '$state', '$http', '$mdDialog', '$mdMedia', '$q', 'bind', '$filter', function($scope, $state, $http, $mdDialog, $mdMedia, $q, bind, $filter) { 33 | $scope.publicInfo = bind; 34 | $http.get(`/api/admin/account/${ $scope.publicInfo.serverId }/${ $scope.publicInfo.accountId }/ban`).then(success => { 35 | $scope.publicInfo.releaseTime = success.data.banTime; 36 | }); 37 | $scope.publicInfo.ban = () => { 38 | $http.post(`/api/admin/account/${ $scope.publicInfo.serverId }/${ $scope.publicInfo.accountId }/ban`, { 39 | time: $filter('ban')(+$scope.publicInfo.banTime) * 60 * 1000, 40 | }).then(success => { 41 | $scope.publicInfo.hide(); 42 | }); 43 | }; 44 | $scope.setDialogWidth = () => { 45 | if($mdMedia('xs') || $mdMedia('sm')) { 46 | return { 'min-width': '325px' }; 47 | } 48 | return { 'min-width': '400px' }; 49 | }; 50 | }], 51 | fullscreen: false, 52 | clickOutsideToClose: true, 53 | }; 54 | const show = (serverId, accountId) => { 55 | if(isDialogShow()) { 56 | return dialogPromise; 57 | } 58 | publicInfo.serverId = serverId; 59 | publicInfo.accountId = accountId; 60 | dialogPromise = $mdDialog.show(dialog); 61 | return dialogPromise; 62 | }; 63 | return { 64 | show, 65 | }; 66 | }]); -------------------------------------------------------------------------------- /plugins/webgui/public/routes/home.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.config(['$stateProvider', $stateProvider => { 6 | $stateProvider 7 | .state('home', { 8 | url: '/home', 9 | abstract: true, 10 | templateUrl: `${ cdn }/public/views/home/home.html`, 11 | resolve: { 12 | myConfig: ['$http', 'configManager', ($http, configManager) => { 13 | if(configManager.getConfig().version) { return; } 14 | return $http.get('/api/home/login').then(success => { 15 | configManager.setConfig(success.data); 16 | }); 17 | }] 18 | }, 19 | }) 20 | .state('home.index', { 21 | url: '/index', 22 | controller: 'HomeIndexController', 23 | templateUrl: `${ cdn }/public/views/home/index.html`, 24 | }) 25 | .state('home.login', { 26 | url: '/login', 27 | controller: 'HomeLoginController', 28 | templateUrl: `${ cdn }/public/views/home/login.html`, 29 | }) 30 | .state('home.macLogin', { 31 | url: '/login/:mac', 32 | controller: 'HomeMacLoginController', 33 | templateUrl: `${ cdn }/public/views/home/macLogin.html`, 34 | }) 35 | .state('home.telegramLogin', { 36 | url: '/login/telegram/:token', 37 | controller: 'HomeTelegramLoginController', 38 | templateUrl: `${ cdn }/public/views/home/telegramLogin.html`, 39 | }) 40 | .state('home.signup', { 41 | url: '/signup', 42 | controller: 'HomeSignupController', 43 | templateUrl: `${ cdn }/public/views/home/signup.html`, 44 | }) 45 | .state('home.resetPassword', { 46 | url: '/password/reset/:token', 47 | controller: 'HomeResetPasswordController', 48 | templateUrl: `${ cdn }/public/views/home/resetPassword.html`, 49 | }) 50 | .state('home.refInput', { 51 | url: '/ref', 52 | controller: 'HomeRefInputController', 53 | templateUrl: `${ cdn }/public/views/home/refInput.html`, 54 | }) 55 | .state('home.ref', { 56 | url: '/ref/:refId', 57 | controller: 'HomeRefController', 58 | templateUrl: `${ cdn }/public/views/home/ref.html`, 59 | }) 60 | ; 61 | } 62 | ]); 63 | 64 | -------------------------------------------------------------------------------- /plugins/webgui/public/dialogs/email.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('app'); 2 | const window = require('window'); 3 | const cdn = window.cdn || ''; 4 | 5 | app.factory('emailDialog', [ '$mdDialog', '$state', '$http', ($mdDialog, $state, $http) => { 6 | const publicInfo = {}; 7 | const hide = () => { 8 | return $mdDialog.hide() 9 | .then(success => { 10 | dialogPromise = null; 11 | return; 12 | }).catch(err => { 13 | dialogPromise = null; 14 | return; 15 | }); 16 | }; 17 | publicInfo.hide = hide; 18 | const send = (title, content) => { 19 | load(); 20 | $http.post(`/api/admin/user/${ publicInfo.userId }/sendEmail`, { 21 | title, 22 | content, 23 | }).then(success => { 24 | hide(); 25 | }).catch(() => { 26 | publicInfo.isLoading = false; 27 | }); 28 | }; 29 | publicInfo.send = send; 30 | let dialogPromise = null; 31 | const isDialogShow = () => { 32 | if(dialogPromise && !dialogPromise.$$state.status) { 33 | return true; 34 | } 35 | return false; 36 | }; 37 | const dialog = { 38 | templateUrl: `${ cdn }/public/views/dialog/email.html`, 39 | escapeToClose: false, 40 | locals: { bind: publicInfo }, 41 | bindToController: true, 42 | controller: ['$scope', '$mdMedia', '$mdDialog', '$http', '$localStorage', 'bind', function($scope, $mdMedia, $mdDialog, $http, $localStorage, bind) { 43 | $scope.publicInfo = bind; 44 | if(!$localStorage.admin.email) { 45 | $localStorage.admin.email = { 46 | title: '', content: '', 47 | }; 48 | } 49 | $scope.publicInfo.email = $localStorage.admin.email; 50 | $scope.setDialogWidth = () => { 51 | if($mdMedia('xs') || $mdMedia('sm')) { 52 | return {}; 53 | } 54 | return { 'min-width': '400px' }; 55 | }; 56 | }], 57 | fullscreen: true, 58 | clickOutsideToClose: false, 59 | }; 60 | const load = () => { 61 | publicInfo.isLoading = true; 62 | }; 63 | const show = userId => { 64 | publicInfo.isLoading = false; 65 | if(isDialogShow()) { 66 | return dialogPromise; 67 | } 68 | publicInfo.userId = userId; 69 | dialogPromise = $mdDialog.show(dialog); 70 | return dialogPromise; 71 | }; 72 | return { 73 | show, 74 | }; 75 | }]); --------------------------------------------------------------------------------