From dcfb1feb592efa8c45cad63795ef5b45b755068c Mon Sep 17 00:00:00 2001 From: jysperm Date: Mon, 10 Nov 2014 06:50:35 +0800 Subject: [PATCH] refactor part of shadowsocks core --- INSTALL.md | 5 +- core/model/Financials.coffee | 2 +- plugin/shadowsocks/index.coffee | 37 +++- plugin/shadowsocks/router.coffee | 31 ++- plugin/shadowsocks/service.coffee | 257 ++++++++---------------- plugin/shadowsocks/shadowsocks.coffee | 88 ++++++++ plugin/shadowsocks/template/config.conf | 14 +- sample/shadowsocks.config.coffee | 2 + 8 files changed, 237 insertions(+), 199 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index f0d70d6..ea5ec88 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -80,6 +80,7 @@ [program:RootPanel] command=node /home/rpadmin/RootPanel/start.js autorestart=true + redirect_stderr = true user=rpadmin service nginx restart @@ -129,14 +130,12 @@ apt-get install python-pip python-m2crypto pip install shadowsocks - mkdir /etc/shadowsocks - vi /etc/default/supervisor ulimit -n 51200 iptables -A OUTPUT -p tcp --dport 25 -j DROP - iptables -A OUTPUT -d smtp.postmarkapp.com -j ACCEPT + iptables -A OUTPUT -p tcp --dport 25 -d smtp.postmarkapp.com -j ACCEPT ### Runtime diff --git a/core/model/Financials.coffee b/core/model/Financials.coffee index 7bb8e65..3a48df0 100644 --- a/core/model/Financials.coffee +++ b/core/model/Financials.coffee @@ -10,7 +10,7 @@ Financials = mongoose.Schema type: required: true type: String - enum: ['deposit', 'billing'] + enum: ['deposit', 'billing', 'usage_billing'] amount: required: true diff --git a/plugin/shadowsocks/index.coffee b/plugin/shadowsocks/index.coffee index ad1c547..d69a2dd 100644 --- a/plugin/shadowsocks/index.coffee +++ b/plugin/shadowsocks/index.coffee @@ -1,4 +1,5 @@ -{pluggable, config} = app +{pluggable, config, utils} = app +{Financials} = app.models exports = module.exports = class ShadowSocksPlugin extends pluggable.Plugin @NAME: 'shadowsocks' @@ -25,17 +26,39 @@ if config.plugins.shadowsocks.green_style exports.registerHook 'view.panel.widgets', generator: (req, callback) -> - exports.render 'widget', req, - transfer_remainder: account.billing.balance / config.plugins.shadowsocks.price_bucket / (1000 * 1000 * 1000 / config.plugins.shadowsocks.billing_bucket) - traffic_24hours: null - traffic_7days: null - traffic_30days: null - , callback + Financials.find + account_id: account._id + type: 'usage_billing' + 'payload.service': 'shadowsocks' + , (err, financials) -> + time_range = + traffic_24hours: 24 * 3600 * 1000 + traffic_7days: 7 * 24 * 3600 * 1000 + traffic_30days: 30 * 24 * 3600 * 1000 + + result = {} + + for name, range of time_range + logs = _.filter financials, (i) -> + return i.created_at.getTime() > Date.now() - range + + result[name] = _.reduce logs, (memo, i) -> + return memo + i.payload.traffic_mb + , 0 + + _.extend result, + transfer_remainder: account.billing.balance / config.plugins.shadowsocks.price_bucket / (1000 * 1000 * 1000 / config.plugins.shadowsocks.billing_bucket) + + exports.render 'widget', req, result, callback exports.registerServiceHook 'enable', filter: (req, callback) -> + shadowsocks.initAccount req.account, callback exports.registerServiceHook 'disable', filter: (req, callback) -> + shadowsocks.deleteAccount req.account, callback app.express.use '/plugin/shadowsocks', require './router' + +setInterval shadowsocks.monitoring, config.plugins.shadowsocks.monitor_cycle diff --git a/plugin/shadowsocks/router.coffee b/plugin/shadowsocks/router.coffee index fae8bac..ed4f02c 100644 --- a/plugin/shadowsocks/router.coffee +++ b/plugin/shadowsocks/router.coffee @@ -1,19 +1,34 @@ +{utils} = app {markdown, fs, path, express} = app.libs -{renderAccount, requireInService, requireAuthenticate} = require '../../core/middleware' +{requireInService} = app.middleware + +shadowsocks = require './shadowsocks' module.exports = exports = express.Router() exports.use requireInService 'shadowsocks' exports.post '/reset_password', (req, res) -> - password = mAccount.randomString 10 + password = utils.randomString 10 - mAccount.update _id: req.account._id, + req.account.update $set: - 'attribute.plugin.shadowsocks.password': password + 'pluggable.shadowsocks.password': password , -> - req.account.attribute.plugin.shadowsocks.password = password + req.account.pluggable.shadowsocks.password = password - service.restart req.account, -> - service.restartAccount req.account, -> - res.json {} + shadowsocks.updateConfigure req.account, -> + res.json {} + +exports.post '/switch_method', (req, res) -> + unless req.body.method in config.plugins.shadowsocks.available_ciphers + return res.error 'invalid_method' + + req.account.update + $set: + 'pluggable.shadowsocks.method': method + , -> + req.account.pluggable.shadowsocks.method = method + + shadowsocks.updateConfigure req.account, -> + res.json {} diff --git a/plugin/shadowsocks/service.coffee b/plugin/shadowsocks/service.coffee index cbc2959..34384f8 100644 --- a/plugin/shadowsocks/service.coffee +++ b/plugin/shadowsocks/service.coffee @@ -1,203 +1,106 @@ -child_process = require 'child_process' -path = require 'path' -jade = require 'jade' -fs = require 'fs' +delete: (account, callback) -> + queryIptablesInfo (iptables_info) -> + port = account.attribute.plugin.shadowsocks.port -plugin = require '../../core/pluggable' + billing_traffic = iptables_info[port].bytes - account.attribute.plugin.shadowsocks.last_traffic_value + billing_traffic = iptables_info[port].bytes if billing_traffic < 0 + billing_traffic += account.attribute.plugin.shadowsocks.pending_traffic -mAccount = require '../../core/model/account' -mBalance = require '../../core/model/balance_log' + amount = billing_traffic / BILLING_BUCKET * config.plugins.shadowsocks.price_bucket -BILLING_BUCKET = config.plugins.shadowsocks.billing_bucket + mAccount.update {_id: account._id}, + $unset: + 'attribute.plugin.shadowsocks': true + $inc: + 'attribute.balance': -amount + , -> + async.parallel [ + (callback) -> + child_process.exec "sudo iptables -D OUTPUT #{iptables_info[port].num}", callback -generatePort = (callback) -> - port = 10000 + Math.floor Math.random() * 10000 + (callback) -> + child_process.exec 'sudo iptables-save | sudo tee /etc/iptables.rules', callback - mAccount.findOne - 'attribute.plugin.shadowsocks.port': port - , (err, result) -> - if result - generatePort callback - else - callback port + (callback) -> + child_process.exec "sudo rm /etc/supervisor/conf.d/#{account.username}.conf", callback -queryIptablesInfo = (callback) -> - child_process.exec 'sudo iptables -n -v -L -t filter -x --line-numbers', (err, stdout) -> - lines = stdout.split '\n' - iptables_info = {} + (callback) -> + child_process.exec 'sudo supervisorctl update', callback + ], -> + if amount > 0 + mBalance.create account, 'service_billing', -amount, + service: 'shadowsocks' + traffic_mb: billing_traffic / BILLING_BUCKET * 100 + is_force: true + , -> + callback() + else + callback() - do -> - CHAIN_OUTPUT = 'Chain OUTPUT' - is_chain_output = false +restart: (account, callback) -> + config_content = _.template (fs.readFileSync path.join(__dirname, 'template/config.conf')).toString(), account.attribute.plugin.shadowsocks - for item in lines - if is_chain_output - if item - try - [num, pkts, bytes, prot, opt, in_, out, source, destination, prot, port] = item.split /\s+/ + pluggable.writeConfig "/etc/shadowsocks/#{account.username}.json", config_content, -> + child_process.exec "sudo chmod +r /etc/shadowsocks/#{account.username}.json", -> + config_content = _.template (fs.readFileSync path.join(__dirname, 'template/supervisor.conf')).toString(), + account: account - unless num == 'num' - port = port.match(/spt:(\d+)/)[1] + pluggable.writeConfig "/etc/supervisor/conf.d/#{account.username}.conf", config_content, -> + child_process.exec 'sudo supervisorctl update', -> + callback() - iptables_info[port.toString()] = - num: parseInt num - pkts: parseInt pkts - bytes: parseInt bytes - port: parseInt port - catch e - continue +restartAccount: (account, callback) -> + child_process.exec "sudo supervisorctl restart shadowsocks-#{account.username}", -> + callback() - if item[0...CHAIN_OUTPUT.length] == CHAIN_OUTPUT - is_chain_output = true +monitoring: -> + queryIptablesInfo (iptables_info) -> + async.map _.values(iptables_info), (item, callback) -> + {port, bytes} = item - callback iptables_info + mAccount.findOne + 'attribute.plugin.shadowsocks.port': port + , (err, account) -> + unless account + return callback() -module.exports = - enable: (account, callback) -> - generatePort (port) -> - password = mAccount.randomString 10 + {pending_traffic, last_traffic_value} = account.attribute.plugin.shadowsocks - mAccount.findAndModify _id: account._id, {}, - $set: - 'attribute.plugin.shadowsocks': - port: port - password: password - pending_traffic: 0 - last_traffic_value: 0 - , new: true, (err, account) -> - child_process.exec "sudo iptables -I OUTPUT -p tcp --sport #{port}", -> - child_process.exec 'sudo iptables-save | sudo tee /etc/iptables.rules', -> - module.exports.restart account, -> - callback() + new_traffic = bytes - last_traffic_value - delete: (account, callback) -> - queryIptablesInfo (iptables_info) -> - port = account.attribute.plugin.shadowsocks.port + if new_traffic < 0 + new_traffic = bytes - billing_traffic = iptables_info[port].bytes - account.attribute.plugin.shadowsocks.last_traffic_value - billing_traffic = iptables_info[port].bytes if billing_traffic < 0 - billing_traffic += account.attribute.plugin.shadowsocks.pending_traffic + new_pending_traffic = pending_traffic + new_traffic - amount = billing_traffic / BILLING_BUCKET * config.plugins.shadowsocks.price_bucket + billing_bucket = Math.floor pending_traffic / BILLING_BUCKET - mAccount.update {_id: account._id}, - $unset: - 'attribute.plugin.shadowsocks': true - $inc: - 'attribute.balance': -amount - , -> - async.parallel [ - (callback) -> - child_process.exec "sudo iptables -D OUTPUT #{iptables_info[port].num}", callback + new_pending_traffic -= billing_bucket * BILLING_BUCKET - (callback) -> - child_process.exec 'sudo iptables-save | sudo tee /etc/iptables.rules', callback + if billing_bucket > 0 + amount = billing_bucket * config.plugins.shadowsocks.price_bucket - (callback) -> - child_process.exec "sudo rm /etc/supervisor/conf.d/#{account.username}.conf", callback - - (callback) -> - child_process.exec 'sudo supervisorctl update', callback - ], -> - if amount > 0 + mAccount.update {_id: account._id}, + $set: + 'attribute.plugin.shadowsocks.pending_traffic': new_pending_traffic + 'attribute.plugin.shadowsocks.last_traffic_value': bytes + $inc: + 'attribute.balance': -amount + , (err) -> mBalance.create account, 'service_billing', -amount, service: 'shadowsocks' - traffic_mb: billing_traffic / BILLING_BUCKET * 100 - is_force: true + traffic_mb: billing_bucket * 100 + is_force: false , -> callback() - else + else if pending_traffic != new_pending_traffic or last_traffic_value != bytes + mAccount.update {_id: account._id}, + $set: + 'attribute.plugin.shadowsocks.pending_traffic': new_pending_traffic + 'attribute.plugin.shadowsocks.last_traffic_value': bytes + , (err) -> callback() + else + callback() - restart: (account, callback) -> - config_content = _.template (fs.readFileSync path.join(__dirname, 'template/config.conf')).toString(), account.attribute.plugin.shadowsocks - - pluggable.writeConfig "/etc/shadowsocks/#{account.username}.json", config_content, -> - child_process.exec "sudo chmod +r /etc/shadowsocks/#{account.username}.json", -> - config_content = _.template (fs.readFileSync path.join(__dirname, 'template/supervisor.conf')).toString(), - account: account - - pluggable.writeConfig "/etc/supervisor/conf.d/#{account.username}.conf", config_content, -> - child_process.exec 'sudo supervisorctl update', -> - callback() - - restartAccount: (account, callback) -> - child_process.exec "sudo supervisorctl restart shadowsocks-#{account.username}", -> - callback() - - monitoring: -> - queryIptablesInfo (iptables_info) -> - async.map _.values(iptables_info), (item, callback) -> - {port, bytes} = item - - mAccount.findOne - 'attribute.plugin.shadowsocks.port': port - , (err, account) -> - unless account - return callback() - - {pending_traffic, last_traffic_value} = account.attribute.plugin.shadowsocks - - new_traffic = bytes - last_traffic_value - - if new_traffic < 0 - new_traffic = bytes - - new_pending_traffic = pending_traffic + new_traffic - - billing_bucket = Math.floor pending_traffic / BILLING_BUCKET - - new_pending_traffic -= billing_bucket * BILLING_BUCKET - - if billing_bucket > 0 - amount = billing_bucket * config.plugins.shadowsocks.price_bucket - - mAccount.update {_id: account._id}, - $set: - 'attribute.plugin.shadowsocks.pending_traffic': new_pending_traffic - 'attribute.plugin.shadowsocks.last_traffic_value': bytes - $inc: - 'attribute.balance': -amount - , (err) -> - mBalance.create account, 'service_billing', -amount, - service: 'shadowsocks' - traffic_mb: billing_bucket * 100 - is_force: false - , -> - callback() - else if pending_traffic != new_pending_traffic or last_traffic_value != bytes - mAccount.update {_id: account._id}, - $set: - 'attribute.plugin.shadowsocks.pending_traffic': new_pending_traffic - 'attribute.plugin.shadowsocks.last_traffic_value': bytes - , (err) -> - callback() - else - callback() - - , -> - - widget: (account, callback) -> - mBalance.find - account_id: account._id - type: 'service_billing' - 'attribute.service': 'shadowsocks' - .toArray (err, balance_logs) -> - time_range = - traffic_24hours: 24 * 3600 * 1000 - traffic_7days: 7 * 24 * 3600 * 1000 - traffic_30days: 30 * 24 * 3600 * 1000 - - result = {} - - for name, range of time_range - logs = _.filter balance_logs, (i) -> - return i.created_at.getTime() > Date.now() - range - - result[name] = _.reduce logs, (memo, i) -> - return memo + i.attribute.traffic_mb - , 0 - - jade.renderFile path.join(__dirname, 'view/widget.jade'), _.extend(result, account: account), (err, html) -> - throw err if err - callback html + , -> diff --git a/plugin/shadowsocks/shadowsocks.coffee b/plugin/shadowsocks/shadowsocks.coffee index e69de29..dd7577b 100644 --- a/plugin/shadowsocks/shadowsocks.coffee +++ b/plugin/shadowsocks/shadowsocks.coffee @@ -0,0 +1,88 @@ +{_, child_process} = app.libs +{logger, utils} = app +{Account} = app.models + +BILLING_BUCKET = config.plugins.shadowsocks.billing_bucket + +exports.generateConfigure = (users, options) -> + configure = + server: '0.0.0.0' + local_port: 1080 + port_password: {} + timeout: 60 + method: options.method ? 'aes-256-cfb' + workers: 2 + + for user in users + configure.port_password[user.port] = user.password + + return JSON.stringify configure + +exports.generatePort = (port) -> + port = 10000 + Math.floor Math.random() * 10000 + + Account.findOne + 'pluggable.shadowsocks.port': port + , (err, result) -> + if result + generatePort callback + else + callback port + +exports.queryIptablesInfo = (callback) -> + child_process.exec 'sudo iptables -n -v -L -t filter -x --line-numbers', (err, stdout) -> + lines = stdout.split '\n' + iptables_info = {} + + do -> + CHAIN_OUTPUT = 'Chain OUTPUT' + is_chain_output = false + + for item in lines + if is_chain_output + if item + try + [num, pkts, bytes, prot, opt, in_, out, source, destination, prot, port] = item.split /\s+/ + + unless num == 'num' + port = port.match(/spt:(\d+)/)[1] + + iptables_info[port.toString()] = + num: parseInt num + pkts: parseInt pkts + bytes: parseInt bytes + port: parseInt port + + catch e + continue + + if item[ ... CHAIN_OUTPUT.length] == CHAIN_OUTPUT + is_chain_output = true + + callback iptables_info + +exports.initAccount = (account, callback) -> + exports.generatePort (port) -> + password = utils.randomString 10 + + Account.findByIdAndUpdate account._id, + $set: + 'pluggable.shadowsocks': + port: port + method: _.first config.plugins.shadowsocks.available_ciphers + password: password + pending_traffic: 0 + last_traffic_value: 0 + , (err, account) -> + logger.error err if err + + child_process.exec "sudo iptables -I OUTPUT -p tcp --sport #{port}", -> + child_process.exec 'sudo iptables-save | sudo tee /etc/iptables.rules', -> + exports.updateConfigure account, -> + callback() + +exports.deleteAccount = (account, callback) -> + +exports.updateConfigure = (account, callback) -> + +exports.monitoring = -> diff --git a/plugin/shadowsocks/template/config.conf b/plugin/shadowsocks/template/config.conf index c8133ce..a1d849a 100644 --- a/plugin/shadowsocks/template/config.conf +++ b/plugin/shadowsocks/template/config.conf @@ -1,8 +1,16 @@ { "server": "0.0.0.0", - "server_port": <%= port %>, "local_port": 1080, - "password": "<%= password %>", + "port_password": { +<% users.forEach(function(user) { %> + "<%= user.port %>": "<%= %>" +<% }); %> + "8381": "foobar1", + "8382": "foobar2", + "8383": "foobar3", + "8384": "foobar4" + }, "timeout": 60, - "method": "aes-256-cfb" + "method": "aes-256-cfb", + "workers": 2 } diff --git a/sample/shadowsocks.config.coffee b/sample/shadowsocks.config.coffee index 21a028f..175cfbe 100644 --- a/sample/shadowsocks.config.coffee +++ b/sample/shadowsocks.config.coffee @@ -71,6 +71,8 @@ module.exports = shadowsocks: green_style: true + available_ciphers: ['aes-256-cfb', 'rc4-md5'] + billing_bucket: 100 * 1024 * 1024 monitor_cycle: 5 * 60 * 1000 price_bucket: 0.06