shadowsocks-manager
Version:
A shadowsocks manager tool for multi user and traffic control.
391 lines (375 loc) • 13.6 kB
JavaScript
const macAccount = appRequire('plugins/macAccount/index');
const account = appRequire('plugins/account/index');
const dns = require('dns');
const net = require('net');
const knex = appRequire('init/knex').knex;
const flowPlugin = appRequire('plugins/flowSaver/flow');
const moment = require('moment');
const formatMacAddress = mac => mac.replace(/-/g, '').replace(/:/g, '').toLowerCase();
exports.getMacAccount = (req, res) => {
const userId = +req.query.userId;
macAccount.getAccount(userId, -1).then(success => {
res.send(success);
}).catch(err => {
console.log(err);
res.status(403).end();
});
};
exports.addMacAccount = (req, res) => {
const mac = formatMacAddress(req.params.macAddress);
const userId = req.body.userId;
const accountId = req.body.accountId;
const serverId = req.body.serverId;
macAccount.newAccount(mac, userId, serverId, accountId).then(success => {
res.send('success');
}).catch(err => {
console.log(err);
res.status(403).end();
});
};
exports.editMacAccount = (req, res) => {
const id = req.body.id;
const mac = formatMacAddress(req.body.macAddress);
const userId = req.body.userId;
const accountId = req.body.accountId;
const serverId = req.body.serverId;
macAccount.editAccount(id, mac, serverId, accountId).then(success => {
res.send('success');
}).catch(err => {
console.log(err);
res.status(403).end();
});
};
exports.deleteMacAccount = (req, res) => {
const accountId = +req.query.id;
macAccount.deleteAccount(accountId).then(success => {
res.send('success');
}).catch(err => {
console.log(err);
res.status(403).end();
});
};
exports.getMacAccountForUser = (req, res) => {
const mac = req.params.macAddress;
const ip = req.headers['x-real-ip'] || req.connection.remoteAddress;
const noPassword = !!(+req.query.noPassword);
const noFlow = !!(+req.query.noFlow);
const type = req.query.type || 'Shadowsocks';
macAccount.getAccountForUser(mac.toLowerCase(), ip, {
type,
noPassword,
noFlow,
}).then(success => {
res.send(success);
}).catch(err => {
console.log(err);
res.status(403).end();
});
};
exports.getNoticeForUser = (req, res) => {
const mac = req.params.macAddress;
const ip = req.headers['x-real-ip'] || req.connection.remoteAddress;
macAccount
.getNoticeForUser(mac.toLowerCase(), ip)
.then(success => {
res.send(success);
}).catch(err => {
console.log(err);
res.status(403).end();
});
};
exports.banAccount = (req, res) => {
const serverId = +req.params.serverId;
const accountId = +req.params.accountId;
const time = +req.body.time;
account.banAccount({
serverId,
accountId,
time,
}).then(success => {
res.send('success');
}).catch(err => {
console.log(err);
res.status(403).end();
});
};
exports.getBanAccount = (req, res) => {
const serverId = +req.params.serverId;
const accountId = +req.params.accountId;
account.getBanAccount({
serverId,
accountId,
}).then(success => {
res.send(success);
}).catch(err => {
console.log(err);
res.status(403).end();
});
};
const isMacAddress = str => {
return str.match(/^([A-Fa-f0-9]{2}[:-]?){5}[A-Fa-f0-9]{2}$/);
};
const getAddress = (address, ip) => {
let myAddress = address;
if(address.indexOf(':') >= 0) {
const hosts = address.split(':');
const number = Math.ceil(Math.random() * (hosts.length - 1));
myAddress = hosts[number];
}
if(!ip) {
return Promise.resolve(myAddress);
}
if(net.isIP(myAddress)) {
return Promise.resolve(myAddress);
}
return new Promise((resolve, reject) => {
dns.lookup(myAddress, (err, ip, family) => {
if(err) {
return resolve(myAddress);
}
return resolve(ip);
});
});
};
const urlsafeBase64 = str => {
return Buffer.from(str).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
};
exports.getSubscribeAccountForUser = async (req, res) => {
try {
const ssr = req.query.ssr;
let type = req.query.type || 'shadowrocket';
if(ssr === '1') { type = 'ssr'; }
const resolveIp = +req.query.ip;
const showFlow = +req.query.flow;
const token = req.params.token;
const ip = req.headers['x-real-ip'] || req.connection.remoteAddress;
let subscribeAccount = {};
let trojanServers = [];
if(isMacAddress(token)) {
subscribeAccount = await macAccount.getMacAccountForSubscribe(token, ip);
} else {
const isSubscribeOn = await knex('webguiSetting').where({
key: 'account'
}).then(s => s[0]).then(s => JSON.parse(s.value).subscribe);
if(!isSubscribeOn) { return res.status(404).end(); }
const accounts = await account.getAccountForSubscribe(token, ip);
subscribeAccount = {
...accounts,
server: accounts.server.filter(server => server.type === 'Shadowsocks'),
};
trojanServers = accounts.server.filter(server => server.type === 'Trojan');
}
for(const s of subscribeAccount.server) {
s.host = await getAddress(s.host, resolveIp);
}
for(const s of trojanServers) {
if (s.host.split(':').length !== 1) {
const hosts = s.host.split(':');
const number = Math.ceil(Math.random() * (hosts.length - 1));
s.host = hosts[number];
}
}
const baseSetting = await knex('webguiSetting').where({
key: 'base'
}).then(s => s[0]).then(s => JSON.parse(s.value));
const ssdInfo = {
airport: 'ssmgr',
port: subscribeAccount.account.port,
encryption: 'aes-256-gcm',
password: subscribeAccount.account.password,
servers: subscribeAccount.server.filter(s => !s.subscribeName).map(s => {
return {
id: s.id,
server: s.host,
port: subscribeAccount.account.port + s.shift,
encryption: s.method,
remarks: s.name,
};
}),
};
if(subscribeAccount.account.type !== 1 && showFlow) {
const template = {
subscribeName: '',
name: '',
host: 'localhost',
method: 'chacha20-ietf-poly1305',
shift: 0,
id: 0,
};
const time = {
'2': 7 * 24 * 3600000,
'3': 30 * 24 * 3600000,
'4': 24 * 3600000,
'5': 3600000,
};
const expire = subscribeAccount.account.data.create + subscribeAccount.account.data.limit * time[subscribeAccount.account.type];
ssdInfo.expiry = moment(expire).format('YYYY-MM-DD HH:mm:ss');
const remain = expire - Date.now();
const insert = {
...template,
subscribeName: remain <= 0
? '已过期'
: remain >= 48 * 3600 * 1000
? moment(expire).format('YYYY-MM-DD过期')
: remain >= 3600 * 1000
? `${Math.floor(remain / (3600 * 1000))}小时后过期`
: `${Math.floor(remain / (60 * 1000))}分钟后过期`,
};
const insertFlow = { ...template };
if(subscribeAccount.account.multiServerFlow) {
const flow = subscribeAccount.account.data.flow;
const time = {
'2': 7 * 24 * 3600000,
'3': 30 * 24 * 3600000,
'4': 24 * 3600000,
'5': 3600000,
};
let from = subscribeAccount.account.data.create;
let to = subscribeAccount.account.data.create + time[subscribeAccount.account.type];
while(to <= Date.now()) {
from = to;
to = from + time[subscribeAccount.account.type];
}
const [ currentFlow ] = await flowPlugin.getServerPortFlowWithScale(0, subscribeAccount.account.id, [from, to], true);
ssdInfo.traffic_used = currentFlow / (1000 * 1000 * 1000);
ssdInfo.traffic_total = flow / (1000 * 1000 * 1000);
const toFlowString = input => {
const K = 1000;
const M = 1000 * 1000;
const G = 1000 * 1000 * 1000;
const T = 1000 * 1000 * 1000 * 1000;
const P = 1000 * 1000 * 1000 * 1000 * 1000;
if (input < K) {
return input + 'B';
} else if (input < M) {
return (input / K).toFixed(1) + 'KB';
} else if (input < G) {
return (input / M).toFixed(1) + 'MB';
} else if (input < T) {
return (input / G).toFixed(2) + 'GB';
} else if (input < P) {
return (input / T).toFixed(3) + 'TB';
} else {
return input;
}
};
insertFlow.subscribeName = `${toFlowString(currentFlow)} / ${toFlowString(flow)} (${(currentFlow*100/flow).toFixed(1)}%)`;
}
subscribeAccount.server = [
...(insertFlow.subscribeName ? [insertFlow] : []),
insert,
...subscribeAccount.server,
];
}
if(type === 'ssd') {
return res.send('ssd://' + Buffer.from(JSON.stringify(ssdInfo)).toString('base64'));
}
if(type === 'clash') {
const yaml = require('js-yaml');
const clashConfig = appRequire('plugins/webgui/server/clash');
const proxyNames = [
...subscribeAccount.server.map(server => server.subscribeName || server.name),
...trojanServers.map(server => server.subscribeName || server.name),
];
const generatedConfig = {
...clashConfig,
proxies: [
...subscribeAccount.server.map(server => ({
cipher: server.method,
name: server.subscribeName || server.name,
password: String(subscribeAccount.account.password),
port: subscribeAccount.account.port + server.shift,
server: server.host,
type: 'ss',
...(server.pluginOptions ? JSON.parse(server.pluginOptions.replace(/\${port}/g, subscribeAccount.account.port+server.shift))['clash'] : {})
})),
...trojanServers.map(server => ({
name: server.subscribeName || server.name,
type: 'trojan',
server: server.host,
port: server.tjPort,
password: `${subscribeAccount.account.port}:${subscribeAccount.account.password}`,
}))
],
'proxy-groups': clashConfig['proxy-groups'].map(group => {
if (!group.proxies.includes('placeholder')) {
return group;
} else {
let proxies = group.proxies.slice();
proxies.splice(group.proxies.indexOf('placeholder'), 1, ...proxyNames);
return {
...group,
proxies
};
}
})
};
return res.send(yaml.safeDump(generatedConfig));
}
if(type === 'mellow') {
const template = [
'[Endpoint]',
'@{SERVERS}',
'Direct, builtin, freedom, domainStrategy=UseIP',
'Dns-Out, builtin, dns',
'[EndpointGroup]',
'@{GROUP}',
'[RoutingRule]',
'DOMAIN-KEYWORD, geosite:cn, Direct',
'GEOIP, cn, Direct',
'GEOIP, private, Direct',
'FINAL, allServers',
'[Dns]',
'hijack = Dns-Out',
'[DnsServer]',
'localhost',
'8.8.8.8',
].join('\n')
.replace('@{SERVERS}', subscribeAccount.server.map(s => {
return `${s.subscribeName || s.name}, ss, ss://${s.method}:${subscribeAccount.account.password}@${s.host}:${(subscribeAccount.account.port + + s.shift)}\n`;
}).join(''))
.replace('@{GROUP}', 'allServers, ' + subscribeAccount.server.map(s => {
return `${s.subscribeName || s.name}`;
}).join(':') + ', latency, interval=300, timeout=6');
return res.send(template);
}
if(type === 'android') {
const servers = subscribeAccount.server.map(s => {
return {
id: s.id,
remarks: s.name,
server: s.host,
server_port: subscribeAccount.account.port + s.shift,
password: subscribeAccount.account.password,
method: s.method,
};
});
return res.json({
version: 1,
remarks: 'ssmgr',
servers,
});
}
if(type === 'shadowrocket') {
const ssServers = subscribeAccount.server.map(s => {
const base64string = `${s.method}:${subscribeAccount.account.password}@${s.host}:${subscribeAccount.account.port + s.shift}`;
return `ss://${Buffer.from(base64string).toString('base64')}#${encodeURIComponent(s.subscribeName || s.name)}`;
});
const tjServers = trojanServers.map(s =>
`trojan://${encodeURIComponent(`${subscribeAccount.account.port}:${subscribeAccount.account.password}`)}@${s.host}:${s.tjPort}#${encodeURIComponent(s.subscribeName || s.name)}`
);
return res.send(Buffer.from([...ssServers, ...tjServers].join('\r\n')).toString('base64'));
}
const result = subscribeAccount.server.map(s => {
if(type === 'potatso') {
return 'ss://' + Buffer.from(s.method + ':' + subscribeAccount.account.password + '@' + s.host + ':' + (subscribeAccount.account.port + s.shift)).toString('base64') + '#' + (s.subscribeName || s.name);
} else if(type === 'ssr') {
return 'ssr://' + urlsafeBase64(s.host + ':' + (subscribeAccount.account.port + s.shift) + ':origin:' + s.method + ':plain:' + urlsafeBase64(subscribeAccount.account.password) + '/?obfsparam=&remarks=' + urlsafeBase64(s.subscribeName || s.name) + '&group=' + urlsafeBase64(baseSetting.title));
}
}).join('\r\n');
return res.send(Buffer.from(result).toString('base64'));
} catch (err) {
console.log(err);
res.status(403).end();
}
};