refactor plugin: shadowsocks

This commit is contained in:
jysperm
2015-04-21 18:09:31 +08:00
parent e0471d528f
commit 55fb429096
10 changed files with 172 additions and 370 deletions

View File

@@ -19,3 +19,10 @@ script: npm test
services:
- mongodb
- redis-server
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/b718f15367b0c6a71f7c
on_success: always
on_failure: always

View File

@@ -53,8 +53,8 @@ module.exports = class LinuxMonitoring
usage.memory = usage.memory / recent_usages.length / (@monitor_cycle / 1000)
Q.all [
@redis.setex 'linux:last_process_list', 3600, JSON.stringify process_list
@redis.setex 'linux:recent_resources_usage', 60, JSON.stringify recent_usages
@cache.setex 'linux:last_process_list', 3600, JSON.stringify process_list
@cache.setex 'linux:recent_resources_usage', 60, JSON.stringify recent_usages
]
monitoringCpu: (process_list) ->

View File

@@ -1,77 +1,30 @@
{_, fs} = app.libs
{pluggable, config, utils} = app
{Financials} = app.models
module.exports = class Shadowsocks
constructor: (@injector) ->
@injector.component 'shadowsocks', new ShadowsocksComponent()
shadowsocks = require './shadowsocks'
@injector.widget 'panel',
repeating:
components:
shadowsocks: every: true
generator: (account, component) ->
root.views.render __dirname + '/view/widget'
shadowsocksPlugin = module.exports = new Plugin
name: 'shadowsocks'
dependencies: ['supervisor', 'linux']
setInterval =>
@getManager().monitoring().then (usages) =>
Q.all usages.map ({port, bytes}) =>
root.billing.usagesBilling @getAccountByPort(port), 'traffic', bytes
register_hooks:
'plugin.wiki.pages':
t_category: 'plugins.shadowsocks.'
t_title: 'README.md'
language: 'zh_CN'
content_markdown: fs.readFileSync("#{__dirname}/wiki/README.md").toString()
, 5 * 60 * 1000
'view.admin.sidebars':
generator: (req, callback) ->
Financials.find
type: 'usage_billing'
'payload.service': 'shadowsocks'
created_at:
$gte: new Date Date.now() - 30 * 24 * 3600 * 1000
, (err, financials) ->
time_range =
traffic_24hours: 24 * 3600 * 1000
traffic_3days: 3 * 24 * 3600 * 1000
traffic_7days: 7 * 24 * 3600 * 1000
traffic_30days: 30 * 24 * 3600 * 1000
getManager: (node) ->
if node
return new ShadowsocksManager root.servers.byName node
else
return new ShadowsocksManager root.servers.master()
traffic_result = {}
for name, range of time_range
logs = _.filter financials, (i) ->
return i.created_at.getTime() > Date.now() - range
traffic_result[name] = _.reduce logs, (memo, i) ->
return memo + i.payload.traffic_mb
, 0
exports.render 'admin/sidebar', req, traffic_result, callback
initialize: ->
app.express.use '/plugin/shadowsocks', require './router'
started: ->
shadowsocks.initSupervisor()
if @config.monitor_cycle
setInterval shadowsocks.monitoring, config.plugins.shadowsocks.monitor_cycle
shadowsocksPlugin.registerComponent
name: 'shadowsocks'
initialize: shadowsocks.initAccount
destroy: shadowsocks.deleteAccount
register_hooks:
'view.panel.scripts':
repeating: 'once'
path: '/plugin/shadowsocks/script/panel.js'
'view.panel.styles':
repeating: 'once'
path: '/plugin/shadowsocks/style/panel.css'
'view.panel.widgets':
generator: (req, callback) ->
bucket_of_gb = 1000 * 1000 * 1000 / config.plugins.shadowsocks.billing_bucket
price_gb = config.plugins.shadowsocks.price_bucket * bucket_of_gb
shadowsocks.accountUsage req.account, (result) ->
_.extend result,
transfer_remainder: req.account.billing.balance / price_gb
exports.render 'widget', req, result, callback
getAccountByPort: (port) ->
Component.findOne
type: 'shadowsocks.shadowsocks'
'options.port': port
.then ({account_id}) ->
Account.findById account_id

View File

@@ -1,33 +0,0 @@
{utils, config} = app
{markdown, fs, path, express} = app.libs
{requireInService} = app.middleware
shadowsocks = require './shadowsocks'
module.exports = exports = express.Router()
exports.use requireInService 'shadowsocks'
exports.post '/reset_password', (req, res) ->
password = utils.randomString 10
req.account.update
$set:
'pluggable.shadowsocks.password': password
, ->
shadowsocks.updateConfigure ->
res.json {}
exports.post '/switch_method', (req, res) ->
unless req.body.method in config.plugins.shadowsocks.available_ciphers
return res.error 'invalid_method'
if req.body.method == req.account.pluggable.shadowsocks.method
return res.error 'already_in_method'
req.account.update
$set:
'pluggable.shadowsocks.method': req.body.method
, ->
shadowsocks.updateConfigure ->
res.json {}

View File

@@ -0,0 +1,26 @@
module.exports = class ShadowsocksComponent
@generatePort: ->
port = 10000 + Math.floor Math.random() * 40000
Component.findOne
type: 'shadowsocks.shadowsocks'
'options.port': port
.then (component) ->
if component
return ShadowsocksComponent.generatePort()
else
return port
initialize: (component) ->
update: (component) ->
destroy: (component) ->
actions: [
resetPassword:
handler: ->
setCipher:
handler: ->
]

View File

@@ -1,260 +1,110 @@
{_, child_process, async, fs} = app.libs
{logger, utils, config} = app
{Account, Financials} = app.models
module.exports = class ShadowsocksManager
user: 'nobody'
supervisor = require '../supervisor/supervisor'
constructor: (@server, {@available_ciphers}) ->
@supervisor = root.plugins.byName('supervisor').getSupervisor @server.node
@cache = root.cache
ShadowsocksPlugin = require './index'
initialize: ->
Q.all @available_ciphers.map (cipher) =>
@supervisor.writeConfig "shadowsocks-#{cipher}", program cipher
BILLING_BUCKET = config.plugins.shadowsocks?.billing_bucket ? 100 * 1024 * 1024
writeConfig: (cipher, users) ->
configure = generateConfigure cipher, users
exports.initSupervisor = (callback) ->
supervisor.programsStatus (program_status) ->
async.each config.plugins.shadowsocks.available_ciphers, (method, callback) ->
program_name = "shadowsocks-#{method}"
@server.writeFile("/etc/shadowsocks/#{cipher}.json", configure, mode: 640).then =>
@supervisor.updateProgram program cipher
.then =>
@supervisor.programControl program cipher
if program_name in _.pluck program_status, 'name'
return callback()
addMonitor: ({port}) ->
@server.command("sudo iptables -I OUTPUT -p tcp --sport #{port}").then =>
@saveIptablesRules()
exports.writeSupervisorConfigure method, ->
supervisor.updateProgram {}, {program_name: program_name}, ->
callback()
removeMonitor: ({port}) ->
@server.command("sudo iptables -D OUTPUT -p tcp --sport #{port}").then =>
@saveIptablesRules()
, callback
monitoring: ->
Q.all([
@cache.getJSON 'shadowsocks:last_traffic'
@incomingTraffic()
]).then ([last_traffic_records, traffic_records]) ->
current_traffic_records = []
exports.writeSupervisorConfigure = (method, callback) ->
program_name = "shadowsocks-#{method}"
Q.all traffic_records.map ({port, bytes}) ->
{bytes: last_bytes} = _.findWhere last_traffic_records,
port: port
configure = exports.generateConfigure [],
method: method
current_traffic_records.push
port: port
bytes: bytes
filename = "/etc/shadowsocks/#{method}.json"
ShadowsocksPlugin.writeConfigFile filename, configure, {mode: 0o755}, ->
supervisor.writeConfig {username: 'nobody'},
program_name: program_name
command: "ssserver -c #{filename}"
name: program_name
autostart: true
autorestart: true
stdout_logfile: false
, ->
callback()
if bytes < last_bytes
return {
port: port
bytes: bytes
}
else
return {
port: port
bytes: bytes - last_bytes
}
exports.generateConfigure = (users, options = {}) ->
.tap =>
@cache.setex 'shadowsocks:last_traffic', 3600, JSON.stringify current_traffic_records
incomingTraffic: ->
CHAIN_OUTPUT = 'Chain OUTPUT'
records = []
@server.command('sudo iptables -n -v -L -t filter -x --line-numbers').then ({stdout}) ->
for line in _.compact stdout.split '\n'
is_chain_output = false
if is_chain_output
try
[num, pkts, bytes, prot, opt, in_, out, source, destination, proto, port] = line.split /\s+/
unless num == 'num'
port = port.match(/spt:(\d+)/)[1]
records.push
num: parseInt num
pkts: parseInt pkts
bytes: parseInt bytes
port: parseInt port
catch e
continue
if line[ ... CHAIN_OUTPUT.length] == CHAIN_OUTPUT
is_chain_output = true
return records
saveIptablesRules: ->
@server.command 'sudo iptables-save | sudo tee /etc/iptables.rules'
program = (cipher) ->
return {
name: "shadowsocks-#{cipher}"
user: @user
command: "ssserver -c /etc/shadowsocks/#{cipher}.json"
autostart: true
autorestart: true
}
generateConfigure = (cipher, users) ->
configure =
server: '0.0.0.0'
local_port: 1080
port_password: {}
timeout: 60
method: options.method ? 'aes-256-cfb'
method: cipher
workers: 2
for user in users
configure.port_password[user.port] = user.password
for {port, password} in users
configure.port_password[port] = password
return JSON.stringify configure
exports.generatePort = (callback) ->
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 ->
callback()
exports.deleteAccount = (account, callback) ->
exports.queryIptablesInfo (iptables_info) ->
{port} = account.pluggable.shadowsocks
billing_traffic = iptables_info[port].bytes - account.pluggable.shadowsocks.last_traffic_value
billing_traffic = iptables_info[port].bytes if billing_traffic < 0
billing_traffic += account.pluggable.shadowsocks.pending_traffic
amount = billing_traffic / BILLING_BUCKET * config.plugins.shadowsocks.price_bucket
account.update
$unset:
'pluggable.shadowsocks': true
$inc:
'billing.balance': -amount
, ->
async.series [
(callback) ->
child_process.exec "sudo iptables -D OUTPUT #{iptables_info[port].num}", callback
(callback) ->
child_process.exec 'sudo iptables-save | sudo tee /etc/iptables.rules', callback
(callback) ->
exports.updateConfigure callback
], ->
if amount > 0
Financials.create
account_id: account._id
type: 'usage_billing'
amount: -amount
payload:
service: 'shadowsocks'
traffic_mb: billing_traffic / (1000 * 1000)
, ->
callback()
else
callback()
exports.accountUsage = (account, 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
callback result
exports.updateConfigure = (callback) ->
async.eachSeries config.plugins.shadowsocks.available_ciphers, (method, callback) ->
Account.find
'pluggable.shadowsocks.method': method
, (err, accounts) ->
users = _.map accounts, (account) ->
return account.pluggable.shadowsocks
configure = exports.generateConfigure users,
method: method
filename = "/etc/shadowsocks/#{method}.json"
ShadowsocksPlugin.writeConfigFile filename, configure, {mode: 0o755}, ->
supervisor.updateProgram {}, {program_name: "shadowsocks-#{method}"}, ->
supervisor.programControl {}, {program_name: "shadowsocks-#{method}"}, 'restart', ->
callback()
, ->
callback()
exports.monitoring = ->
exports.queryIptablesInfo (iptables_info) ->
async.each _.values(iptables_info), (item, callback) ->
{port, bytes} = item
Account.findOne
'pluggable.shadowsocks.port': port
, (err, account) ->
unless account
return callback()
{pending_traffic, last_traffic_value} = account.pluggable.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
account.update
$set:
'pluggable.shadowsocks.pending_traffic': new_pending_traffic
'pluggable.shadowsocks.last_traffic_value': bytes
$inc:
'billing.balance': -amount
, (err) ->
logger.error err if err
Financials.create
account_id: account._id
type: 'usage_billing'
amount: -amount
payload:
service: 'shadowsocks'
traffic_mb: (billing_bucket * BILLING_BUCKET) / (1000 * 1000)
, ->
callback()
else if pending_traffic != new_pending_traffic or last_traffic_value != bytes
account.update
$set:
'pluggable.shadowsocks.pending_traffic': new_pending_traffic
'pluggable.shadowsocks.last_traffic_value': bytes
, (err) ->
callback()
else
callback()
, ->

View File

@@ -0,0 +1,17 @@
prepend sidebar
.row
header= t('')
table.table.table-hover
tbody
tr
td #{(traffic_24hours / 1000).toFixed(1)}G
td= t('24hours_ago')
tr
td #{(traffic_3days / 1000).toFixed(1)}G
td= t('3days_ago')
tr
td #{(traffic_7days / 1000).toFixed(1)}G
td= t('7days_ago')
tr
td #{(traffic_30days / 1000).toFixed(1)}G
td= t('30days_ago')

View File

@@ -1,16 +0,0 @@
.row
header= t('')
table.table.table-hover
tbody
tr
td #{(traffic_24hours / 1000).toFixed(1)}G
td= t('24hours_ago')
tr
td #{(traffic_3days / 1000).toFixed(1)}G
td= t('3days_ago')
tr
td #{(traffic_7days / 1000).toFixed(1)}G
td= t('7days_ago')
tr
td #{(traffic_30days / 1000).toFixed(1)}G
td= t('30days_ago')

View File

@@ -11,9 +11,9 @@ module.exports = class Supervisor
getSupervisor: (node) ->
if node
return new Supervisor root.servers.byName node
return new SupervisorManager root.servers.byName node
else
return new Supervisor root.servers.master()
return new SupervisorManager root.servers.master()
class SupervisorComponent
constructor: ({@getSupervisor}) ->

View File

@@ -1,7 +1,5 @@
validator = require 'validator'
{mabolo} = root
status_mapping =
STOPPED: 'stopped'
STARTING: 'running'
@@ -12,7 +10,7 @@ status_mapping =
FATAL: 'stopped'
UNKNOWN: 'stopped'
module.exports = class Supervisor
module.exports = class SupervisorManager
constructor: (@server) ->
writeConfig: (name, programs) ->
@@ -47,12 +45,12 @@ configPath = (name) ->
renderConfig = (programs) ->
renderProgram = (program) ->
configuration = """
[program:#{user}-#{program.name}]
[program:#{program.user}-#{program.name}]
user = #{program.user}
command = #{program.command}
autostart = #{program.autostart}
autorestart = #{program.autorestart}
redirect_stderr = #{program.redirect_stderr}\n
redirect_stderr = true\n
"""
if program.directory