shadowsocks-single
Version:
Yet another shadowsocks implementation for nodejs to help you bypass firewalls.
313 lines (267 loc) • 8.01 kB
JavaScript
import path from 'path';
import minimist from 'minimist';
import { spawn } from 'child_process';
import { readFileSync, accessSync } from 'fs';
import { isV4Format } from 'ip';
import { lookup } from 'dns';
import DEFAULT_CONFIG from './defaultConfig';
import { version } from '../package.json';
// import fileConfig from '../config.json';
import * as ssLocal from './ssLocal';
import * as ssServer from './ssServer';
import { getPid, writePidFile, deletePidFile } from './pid';
import { updateGFWList as _updateGFWList, GFWLIST_FILE_PATH } from './gfwlistUtils';
import { safelyKill } from './utils';
const PROXY_ARGUMENT_PAIR = {
c: 'configFilePath',
s: 'serverAddr',
p: 'serverPort',
pac_port: 'pacServerPort',
l: 'localAddr',
b: 'localPort',
k: 'password',
m: 'method',
t: 'timeout',
level: 'level',
// private
mem: '_recordMemoryUsage',
};
const GENERAL_ARGUMENT_PAIR = {
h: 'help',
help: 'help',
d: 'daemon',
pac_update_gfwlist: 'pacUpdateGFWList',
};
const SPAWN_OPTIONS = {
detached: true,
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
};
const DAEMON_COMMAND = {
start: 'start',
stop: 'stop',
restart: 'restart',
};
const log = console.log; // eslint-disable-line
function getDaemonType(isServer) {
return isServer ? 'server' : 'local';
}
function isRunning(pid) {
try {
// signal 0 to test existence
return process.kill(pid, 0);
} catch (e) {
// NOTE: 'EPERM' permissions, 'ESRCH' process group doesn't exist
return e.code !== 'ESRCH';
}
}
function getArgvOptions(argv) {
const generalOptions = {};
const proxyOptions = {};
const configPair = minimist(argv);
const optionsType = [{
options: proxyOptions,
keys: Object.keys(PROXY_ARGUMENT_PAIR),
values: PROXY_ARGUMENT_PAIR,
}, {
options: generalOptions,
keys: Object.keys(GENERAL_ARGUMENT_PAIR),
values: GENERAL_ARGUMENT_PAIR,
}];
let invalidOption = null;
Object.keys(configPair).forEach((key) => {
if (key === '_') {
return;
}
let hit = false;
optionsType.forEach((optType) => {
const i = optType.keys.indexOf(key);
if (i >= 0) {
optType.options[optType.values[optType.keys[i]]] = configPair[key]; // eslint-disable-line
hit = true;
}
});
if (!hit) {
invalidOption = key;
}
});
if (invalidOption) {
invalidOption = (invalidOption.length === 1) ? `-${invalidOption}` : `--${invalidOption}`;
} else if (generalOptions.daemon
&& Object.keys(DAEMON_COMMAND).indexOf(generalOptions.daemon) < 0) {
invalidOption = `invalid daemon command: ${generalOptions.daemon}`;
}
return {
generalOptions, proxyOptions, invalidOption,
};
}
function readConfig(_filePath) {
if (!_filePath) {
return null;
}
const filePath = path.resolve(process.cwd(), _filePath);
try {
accessSync(filePath);
} catch (e) {
throw new Error(`failed to find config file in: ${filePath}`);
}
return JSON.parse(readFileSync(filePath));
}
// export for test
export function resolveServerAddr(config, next) {
const { serverAddr } = config.proxyOptions;
if (isV4Format(serverAddr)) {
next(null, config);
} else {
lookup(serverAddr, (err, addresses) => {
if (err) {
next(new Error(`failed to resolve 'serverAddr': ${serverAddr}`), config);
} else {
// NOTE: mutate data
config.proxyOptions.serverAddr = addresses; // eslint-disable-line
next(null, config);
}
});
}
}
export function getConfig(argv = [], next) {
const { generalOptions, proxyOptions, invalidOption } = getArgvOptions(argv);
const specificFileConfig = readConfig(proxyOptions.configFilePath) || fileConfig;
const config = {
generalOptions,
invalidOption,
proxyOptions: Object.assign({}, DEFAULT_CONFIG, specificFileConfig, proxyOptions),
};
resolveServerAddr(config, next);
}
function logHelp(invalidOption) {
log(
`
${(invalidOption ? `${invalidOption}\n` : '')}shadowsocks-js ${version}
You can supply configurations via either config file or command line arguments.
Proxy options:
-c config path to config file
-s SERVER_ADDR server address, default: 0.0.0.0
-p SERVER_PORT server port, default: 8083
-l LOCAL_ADDR local binding address, default: 127.0.0.1
-b LOCAL_PORT local port, default: 1080
-k PASSWORD password
-m METHOD encryption method, default: aes-128-cfb
-t TIMEOUT timeout in seconds, default: 600
--pac_port PAC_PORT PAC file server port, default: 8090
--pac_update_gfwlist [URL] [localssjs] Update the gfwlist
for PAC server. You can specify the
request URL.
--level LOG_LEVEL log level, default: warn
example: --level verbose
General options:
-h, --help show this help message and exit
-d start/stop/restart daemon mode
`
);
}
function updateGFWList(flag) {
log('Updating gfwlist...');
const next = (err) => {
if (err) {
throw err;
} else {
log(`Updating finished. You can checkout the file here: ${GFWLIST_FILE_PATH}`);
}
};
if (typeof flag === 'string') {
_updateGFWList(flag, next);
} else {
_updateGFWList(next);
}
}
function startDaemon(isServer) {
// TODO: `node` or with path?
const child = spawn('node', [path.join(__dirname, 'daemon'), getDaemonType(isServer)]
.concat(process.argv.slice(2)), SPAWN_OPTIONS);
child.disconnect();
// do not wait for child
child.unref();
writePidFile(getDaemonType(isServer), child.pid);
log('start');
return child;
}
function stopDaemon(isServer, pid) {
if (pid) {
deletePidFile(getDaemonType(isServer));
safelyKill(pid, 'SIGHUP');
log('stop');
} else {
log('already stopped');
}
}
function runDaemon(isServer, cmd) {
let pid = getPid(getDaemonType(isServer));
const running = isRunning(pid);
if (pid && !running) {
log('previous daemon unexpectedly exited');
deletePidFile(getDaemonType(isServer));
pid = null;
}
switch (cmd) {
case DAEMON_COMMAND.start:
if (pid) {
log('already started');
} else {
startDaemon(isServer);
}
return;
case DAEMON_COMMAND.stop:
stopDaemon(isServer, pid);
return;
case DAEMON_COMMAND.restart:
stopDaemon(isServer, pid);
startDaemon(isServer);
break;
default:
}
}
export function startSSLocal(config) {
return new Promise((resolve, reject)=>{
resolveServerAddr(config, (err, newConfig) => {
if (err) {
reject(err)
return
}
newConfig.proxyOptions.listenError = (e)=>{
if (e.code == 'EADDRINUSE') {
// 端口冲突则自动重连
ssLocal.startServer(newConfig.proxyOptions, true)
} else {
// 其它错误则抛出
reject(e)
}
}
newConfig.proxyOptions.listenSuccess = resolve
ssLocal.startServer(newConfig.proxyOptions, true)
})
})
}
function runSingle(isServer, proxyOptions) {
const willLogToConsole = true;
return isServer ? ssServer.startServer(proxyOptions, willLogToConsole)
: ssLocal.startServer(proxyOptions, willLogToConsole);
}
export default function client(isServer) {
const argv = process.argv.slice(2);
getConfig(argv, (err, config) => {
if (err) {
throw err;
}
const { generalOptions, proxyOptions, invalidOption } = config;
log(config)
if (generalOptions.help || invalidOption) {
logHelp(invalidOption);
} else if (generalOptions.pacUpdateGFWList) {
updateGFWList(generalOptions.pacUpdateGFWList);
} else if (generalOptions.daemon) {
runDaemon(isServer, generalOptions.daemon);
} else {
runSingle(isServer, proxyOptions);
}
});
}