diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe244db --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Mac +.DS_Store diff --git a/Cakefile b/Cakefile new file mode 100644 index 0000000..cfaed85 --- /dev/null +++ b/Cakefile @@ -0,0 +1,34 @@ +{spawn} = require 'child_process' + +build = (callback) -> + os = require 'os' + if os.platform() == 'win32' + coffeeCmd = 'coffee.cmd' + else + coffeeCmd = 'coffee' + coffee = spawn coffeeCmd, ['-c', '-o', '.', 'src'] + coffee.stderr.on 'data', (data) -> + process.stderr.write data.toString() + coffee.stdout.on 'data', (data) -> + console.log data.toString() + coffee.on 'exit', (code) -> + console.log 'build completed' + callback?() if code is 0 + process.exit code + +test = (callback) -> + os = require 'os' + coffee = spawn 'node', ['test.js'] + coffee.stderr.on 'data', (data) -> + process.stderr.write data.toString() + coffee.stdout.on 'data', (data) -> + console.log data.toString() + coffee.on 'exit', (code) -> + callback?() if code is 0 + process.exit code + +task 'build', 'Build ./ from src/', -> + build() + +task 'test', 'Run unit test', -> + test() diff --git a/LICENSE b/LICENSE index ba5c4ff..74ddfac 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ -MIT License - -Copyright (c) 2016 削微寒 +The MIT License (MIT) +Copyright (c) 2016 Xueweihan +Copyright (c) 2014 Zhao Xiaohong +Copyright (c) 2012-2014 clowwindy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -12,10 +13,10 @@ furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..063b78f --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: npm start diff --git a/README.md b/README.md new file mode 100644 index 0000000..00d9b20 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +> Fork 于 [shadowsocks-heroku](https://github.com/mrluanma/shadowsocks-heroku) 项目 + +# shadowsocks-heroku +[Heroku](https://www.heroku.com/) 是一个支持多种编程语言的云平台即服务,shadowsocks-heroku 则是可部署在 Heroku 平台的ss服务。 +和 [shadowsocks](https://github.com/clowwindy/shadowsocks) 不同的是 shadowsocks-heroku 使用的 WebSocket 代替原本的 sockets。 + +本项目主要介绍如何利用 heroku 部署 [shadowsocks-heroku](https://github.com/mrluanma/shadowsocks-heroku) 项目。预计十分钟后,你就可以 google 了。 + +### 一、准备 +#### 1.注册 Heroku 帐号 +Heroku 提供免费账号,具体限制如下: +- Run apps for free using your monthly pool of free dyno hours +- Unverified accounts: receive a pool of 550 free dyno hours +- Verified accounts: receive an additional 450 free dyno hours +- Dyno hours can be shared across any of your free apps +- 1 web dyno/1 worker dyno/1 one-off dyno maximum per app +- 512 MB RAM per dyno +- Free apps sleep automatically after 30 mins of inactivity to conserve your dyno hours +- Free apps wake automatically when a web request is received +- Access to the Heroku Dashboard and Heroku CLI for app management +- Custom domains for every free app (with verified account) +- Up to 5 free apps (unverified) or 100 (verified) + +用作 VPS 是够了,注册地址:https://signup.heroku.com/ + +#### 2.Fork本项目 +1. Fork 本项目到个人账号下 +![](https://github.com/521xueweihan/shadowsocks-heroku/blob/master/img/4-min.png) + +2. Clone 本项目:`git clone https://github.com/521xueweihan/shadowsocks-heroku.git` + +### 二、部署 +heroku 在创建项目的时候可以,通过关联 GitHub 账号,直接部署 GitHub 账号下的项目。具体步骤如下: + +1. 登陆 Heroku 帐号,后进入 Dashboard ——> Create New App ——> 输入 App Name +2. 完成上一步后,会跳转到 Deploy 页面,找到 Deployment method 选择 GitHub 关联上自己的 GitHub 帐号。 +3. 关联上 shadowsocks-heroku 项目,如下图所示: + ![](https://github.com/521xueweihan/shadowsocks-heroku/blob/master/img/1-min.png) +4. 点击 Deploy Branch,部署成功如下图: + ![](https://github.com/521xueweihan/shadowsocks-heroku/blob/master/img/2-min.png) + +### 三、设置加密算法和密码 +Setting 页面 ——> Reveal Config Vars,设置参数如下图: +![](https://github.com/521xueweihan/shadowsocks-heroku/blob/master/img/3-min.png) + +### 四、启动本地 client: +1. **进到本项目目录**,执行`npm install` 命令,安装依赖的库(如没有 npm ,请自行安装,[npm安装依赖慢](http://www.cnblogs.com/xueweihan/p/5491730.html)) +2. 启动本地 client,`node local.js -s 你的app名称.herokuapp.com -l 1080 -m 设置的加密算法 -k 设置的密码 -r 80` + +### 五、最后 +1. 下载:Chrome 浏览器[SwitchyOmega](https://github.com/FelisCatus/SwitchyOmega/releases/download/v2.3.21/SwitchyOmega.crx),如果下载地址失效,可以在本项目下的 download 下找到 SwitchyOmega.crx +2. 安装:打开浏览器的扩展程序页面 chrome://extensions 。把SwitchyOmega.crx文件拖放到浏览器扩展程序页面安装。 +3. 配置:SwitchyOmega:`代理协议:SOCKS5 代理服务器:127.0.0.1 代理端口:1080`,如下图: +![](https://github.com/521xueweihan/shadowsocks-heroku/blob/master/img/5-min.png) + +### 支持的加密算法 +- rc4 +- rc4-md5 +- table +- bf-cfb +- des-cfb +- rc2-cfb +- idea-cfb +- seed-cfb +- cast5-cfb +- aes-128-cfb +- aes-192-cfb +- aes-256-cfb +- camellia-256-cfb +- camellia-192-cfb +- camellia-128-cfb diff --git a/config.json b/config.json new file mode 100644 index 0000000..b67fa45 --- /dev/null +++ b/config.json @@ -0,0 +1,10 @@ +{ + "server": "127.0.0.1", + "local_address": "127.0.0.1", + "scheme": "ws", + "local_port": 1080, + "remote_port": 8080, + "password": "`try*(^^$some^$%^complex>:<>?~password/", + "timeout": 600, + "method": "rc4-md5" +} diff --git a/download/SwitchyOmega.crx b/download/SwitchyOmega.crx new file mode 100644 index 0000000..6ff682e Binary files /dev/null and b/download/SwitchyOmega.crx differ diff --git a/encrypt.js b/encrypt.js new file mode 100644 index 0000000..1b60dc2 --- /dev/null +++ b/encrypt.js @@ -0,0 +1,207 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var EVP_BytesToKey, Encryptor, bytes_to_key_results, cachedTables, create_rc4_md5_cipher, crypto, getTable, int32Max, merge_sort, method_supported, substitute; + + crypto = require("crypto"); + + merge_sort = require("./merge_sort").merge_sort; + + int32Max = Math.pow(2, 32); + + cachedTables = {}; + + getTable = function(key) { + var ah, al, decrypt_table, hash, i, md5sum, result, table; + if (cachedTables[key]) { + return cachedTables[key]; + } + console.log("calculating ciphers"); + table = new Array(256); + decrypt_table = new Array(256); + md5sum = crypto.createHash("md5"); + md5sum.update(key); + hash = new Buffer(md5sum.digest(), "binary"); + al = hash.readUInt32LE(0); + ah = hash.readUInt32LE(4); + i = 0; + while (i < 256) { + table[i] = i; + i++; + } + i = 1; + while (i < 1024) { + table = merge_sort(table, function(x, y) { + return ((ah % (x + i)) * int32Max + al) % (x + i) - ((ah % (y + i)) * int32Max + al) % (y + i); + }); + i++; + } + i = 0; + while (i < 256) { + decrypt_table[table[i]] = i; + ++i; + } + result = [table, decrypt_table]; + cachedTables[key] = result; + return result; + }; + + substitute = function(table, buf) { + var i; + i = 0; + while (i < buf.length) { + buf[i] = table[buf[i]]; + i++; + } + return buf; + }; + + bytes_to_key_results = {}; + + EVP_BytesToKey = function(password, key_len, iv_len) { + var count, d, data, i, iv, key, m, md5, ms; + if (bytes_to_key_results[password + ":" + key_len + ":" + iv_len]) { + return bytes_to_key_results[password + ":" + key_len + ":" + iv_len]; + } + m = []; + i = 0; + count = 0; + while (count < key_len + iv_len) { + md5 = crypto.createHash('md5'); + data = password; + if (i > 0) { + data = Buffer.concat([m[i - 1], password]); + } + md5.update(data); + d = md5.digest(); + m.push(d); + count += d.length; + i += 1; + } + ms = Buffer.concat(m); + key = ms.slice(0, key_len); + iv = ms.slice(key_len, key_len + iv_len); + bytes_to_key_results[password] = [key, iv]; + return [key, iv]; + }; + + method_supported = { + 'aes-128-cfb': [16, 16], + 'aes-192-cfb': [24, 16], + 'aes-256-cfb': [32, 16], + 'bf-cfb': [16, 8], + 'camellia-128-cfb': [16, 16], + 'camellia-192-cfb': [24, 16], + 'camellia-256-cfb': [32, 16], + 'cast5-cfb': [16, 8], + 'des-cfb': [8, 8], + 'idea-cfb': [16, 8], + 'rc2-cfb': [16, 8], + 'rc4': [16, 0], + 'rc4-md5': [16, 16], + 'seed-cfb': [16, 16] + }; + + create_rc4_md5_cipher = function(key, iv, op) { + var md5, rc4_key; + md5 = crypto.createHash('md5'); + md5.update(key); + md5.update(iv); + rc4_key = md5.digest(); + if (op === 1) { + return crypto.createCipheriv('rc4', rc4_key, ''); + } else { + return crypto.createDecipheriv('rc4', rc4_key, ''); + } + }; + + Encryptor = (function() { + function Encryptor(key1, method1) { + var ref; + this.key = key1; + this.method = method1; + this.iv_sent = false; + if (this.method === 'table') { + this.method = null; + } + if (this.method != null) { + this.cipher = this.get_cipher(this.key, this.method, 1, crypto.randomBytes(32)); + } else { + ref = getTable(this.key), this.encryptTable = ref[0], this.decryptTable = ref[1]; + } + } + + Encryptor.prototype.get_cipher_len = function(method) { + var m; + method = method.toLowerCase(); + m = method_supported[method]; + return m; + }; + + Encryptor.prototype.get_cipher = function(password, method, op, iv) { + var iv_, key, m, ref; + method = method.toLowerCase(); + password = new Buffer(password, 'binary'); + m = this.get_cipher_len(method); + if (m != null) { + ref = EVP_BytesToKey(password, m[0], m[1]), key = ref[0], iv_ = ref[1]; + if (iv == null) { + iv = iv_; + } + if (op === 1) { + this.cipher_iv = iv.slice(0, m[1]); + } + iv = iv.slice(0, m[1]); + if (method === 'rc4-md5') { + return create_rc4_md5_cipher(key, iv, op); + } else { + if (op === 1) { + return crypto.createCipheriv(method, key, iv); + } else { + return crypto.createDecipheriv(method, key, iv); + } + } + } + }; + + Encryptor.prototype.encrypt = function(buf) { + var result; + if (this.method != null) { + result = this.cipher.update(buf); + if (this.iv_sent) { + return result; + } else { + this.iv_sent = true; + return Buffer.concat([this.cipher_iv, result]); + } + } else { + return substitute(this.encryptTable, buf); + } + }; + + Encryptor.prototype.decrypt = function(buf) { + var decipher_iv, decipher_iv_len, result; + if (this.method != null) { + if (this.decipher == null) { + decipher_iv_len = this.get_cipher_len(this.method)[1]; + decipher_iv = buf.slice(0, decipher_iv_len); + this.decipher = this.get_cipher(this.key, this.method, 0, decipher_iv); + result = this.decipher.update(buf.slice(decipher_iv_len)); + return result; + } else { + result = this.decipher.update(buf); + return result; + } + } else { + return substitute(this.decryptTable, buf); + } + }; + + return Encryptor; + + })(); + + exports.Encryptor = Encryptor; + + exports.getTable = getTable; + +}).call(this); diff --git a/img/1-min.png b/img/1-min.png new file mode 100644 index 0000000..4f0f685 Binary files /dev/null and b/img/1-min.png differ diff --git a/img/2-min.png b/img/2-min.png new file mode 100644 index 0000000..b3aa496 Binary files /dev/null and b/img/2-min.png differ diff --git a/img/3-min.png b/img/3-min.png new file mode 100644 index 0000000..931bd71 Binary files /dev/null and b/img/3-min.png differ diff --git a/img/4-min.png b/img/4-min.png new file mode 100644 index 0000000..5c84243 Binary files /dev/null and b/img/4-min.png differ diff --git a/img/5-min.png b/img/5-min.png new file mode 100644 index 0000000..af5ea42 Binary files /dev/null and b/img/5-min.png differ diff --git a/local.js b/local.js new file mode 100644 index 0000000..2752c93 --- /dev/null +++ b/local.js @@ -0,0 +1,322 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var Encryptor, HTTPPROXY, HttpsProxyAgent, KEY, LOCAL_ADDRESS, METHOD, PORT, REMOTE_PORT, SCHEME, SERVER, WebSocket, config, configContent, configFromArgs, fs, getServer, http, inetNtoa, k, net, options, parseArgs, path, prepareServer, ref, s, server, timeout, url, v; + + net = require("net"); + + url = require("url"); + + http = require("http"); + + fs = require("fs"); + + path = require("path"); + + WebSocket = require('ws'); + + parseArgs = require("minimist"); + + HttpsProxyAgent = require('https-proxy-agent'); + + Encryptor = require("./encrypt").Encryptor; + + options = { + alias: { + 'b': 'local_address', + 'l': 'local_port', + 's': 'server', + 'r': 'remote_port', + 'k': 'password', + 'c': 'config_file', + 'm': 'method' + }, + string: ['local_address', 'server', 'password', 'config_file', 'method', 'scheme'], + "default": { + 'config_file': path.resolve(__dirname, "config.json") + } + }; + + inetNtoa = function(buf) { + return buf[0] + "." + buf[1] + "." + buf[2] + "." + buf[3]; + }; + + configFromArgs = parseArgs(process.argv.slice(2), options); + + configContent = fs.readFileSync(configFromArgs.config_file); + + config = JSON.parse(configContent); + + for (k in configFromArgs) { + v = configFromArgs[k]; + config[k] = v; + } + + SCHEME = config.scheme; + + SERVER = config.server; + + REMOTE_PORT = config.remote_port; + + LOCAL_ADDRESS = config.local_address; + + PORT = config.local_port; + + KEY = config.password; + + METHOD = config.method; + + timeout = Math.floor(config.timeout * 1000); + + if ((ref = METHOD.toLowerCase()) === "" || ref === "null" || ref === "table") { + METHOD = null; + } + + HTTPPROXY = process.env.http_proxy; + + if (HTTPPROXY) { + console.log("http proxy:", HTTPPROXY); + } + + prepareServer = function(address) { + var serverUrl; + serverUrl = url.parse(address); + serverUrl.slashes = true; + if (serverUrl.protocol == null) { + serverUrl.protocol = SCHEME; + } + if (serverUrl.hostname === null) { + serverUrl.hostname = address; + serverUrl.pathname = '/'; + } + if (serverUrl.port == null) { + serverUrl.port = REMOTE_PORT; + } + return url.format(serverUrl); + }; + + if (SERVER instanceof Array) { + SERVER = (function() { + var j, len, results; + results = []; + for (j = 0, len = SERVER.length; j < len; j++) { + s = SERVER[j]; + results.push(prepareServer(s)); + } + return results; + })(); + } else { + SERVER = prepareServer(SERVER); + } + + getServer = function() { + if (SERVER instanceof Array) { + return SERVER[Math.floor(Math.random() * SERVER.length)]; + } else { + return SERVER; + } + }; + + server = net.createServer(function(connection) { + var aServer, addrLen, addrToSend, cachedPieces, encryptor, headerLength, ping, remoteAddr, remotePort, stage, ws; + console.log("local connected"); + server.getConnections(function(err, count) { + console.log("concurrent connections:", count); + }); + encryptor = new Encryptor(KEY, METHOD); + stage = 0; + headerLength = 0; + cachedPieces = []; + addrLen = 0; + ws = null; + ping = null; + remoteAddr = null; + remotePort = null; + addrToSend = ""; + aServer = getServer(); + connection.on("data", function(data) { + var addrtype, agent, buf, cmd, e, endpoint, error, opts, parsed, ref1, reply, tempBuf; + if (stage === 5) { + data = encryptor.encrypt(data); + if (ws.readyState === WebSocket.OPEN) { + ws.send(data, { + binary: true + }); + if (ws.bufferedAmount > 0) { + connection.pause(); + } + } + return; + } + if (stage === 0) { + tempBuf = new Buffer(2); + tempBuf.write("\u0005\u0000", 0); + connection.write(tempBuf); + stage = 1; + return; + } + if (stage === 1) { + try { + cmd = data[1]; + addrtype = data[3]; + if (cmd !== 1) { + console.log("unsupported cmd:", cmd); + reply = new Buffer("\u0005\u0007\u0000\u0001", "binary"); + connection.end(reply); + return; + } + if (addrtype === 3) { + addrLen = data[4]; + } else if (addrtype !== 1) { + console.log("unsupported addrtype:", addrtype); + connection.end(); + return; + } + addrToSend = data.slice(3, 4).toString("binary"); + if (addrtype === 1) { + remoteAddr = inetNtoa(data.slice(4, 8)); + addrToSend += data.slice(4, 10).toString("binary"); + remotePort = data.readUInt16BE(8); + headerLength = 10; + } else { + remoteAddr = data.slice(5, 5 + addrLen).toString("binary"); + addrToSend += data.slice(4, 5 + addrLen + 2).toString("binary"); + remotePort = data.readUInt16BE(5 + addrLen); + headerLength = 5 + addrLen + 2; + } + buf = new Buffer(10); + buf.write("\u0005\u0000\u0000\u0001", 0, 4, "binary"); + buf.write("\u0000\u0000\u0000\u0000", 4, 4, "binary"); + buf.writeInt16BE(remotePort, 8); + connection.write(buf); + if (HTTPPROXY) { + endpoint = aServer; + parsed = url.parse(endpoint); + opts = url.parse(HTTPPROXY); + opts.secureEndpoint = (ref1 = parsed.protocol) != null ? ref1 : parsed.protocol === { + 'wss:': false + }; + agent = new HttpsProxyAgent(opts); + ws = new WebSocket(aServer, { + protocol: "binary", + agent: agent + }); + } else { + ws = new WebSocket(aServer, { + protocol: "binary" + }); + } + ws.on("open", function() { + var addrToSendBuf, i, piece; + ws._socket.on("error", function(e) { + console.log("remote " + remoteAddr + ":" + remotePort + " " + e); + connection.destroy(); + return server.getConnections(function(err, count) { + console.log("concurrent connections:", count); + }); + }); + console.log("connecting " + remoteAddr + " via " + aServer); + addrToSendBuf = new Buffer(addrToSend, "binary"); + addrToSendBuf = encryptor.encrypt(addrToSendBuf); + ws.send(addrToSendBuf, { + binary: true + }); + i = 0; + while (i < cachedPieces.length) { + piece = cachedPieces[i]; + piece = encryptor.encrypt(piece); + ws.send(piece, { + binary: true + }); + i++; + } + cachedPieces = null; + stage = 5; + ping = setInterval(function() { + return ws.ping("", null, true); + }, 50 * 1000); + ws._socket.on("drain", function() { + return connection.resume(); + }); + }); + ws.on("message", function(data, flags) { + data = encryptor.decrypt(data); + if (!connection.write(data)) { + return ws._socket.pause(); + } + }); + ws.on("close", function() { + clearInterval(ping); + console.log("remote disconnected"); + return connection.destroy(); + }); + ws.on("error", function(e) { + console.log("remote " + remoteAddr + ":" + remotePort + " error: " + e); + connection.destroy(); + return server.getConnections(function(err, count) { + console.log("concurrent connections:", count); + }); + }); + if (data.length > headerLength) { + buf = new Buffer(data.length - headerLength); + data.copy(buf, 0, headerLength); + cachedPieces.push(buf); + buf = null; + } + return stage = 4; + } catch (error) { + e = error; + console.log(e); + return connection.destroy(); + } + } else { + if (stage === 4) { + return cachedPieces.push(data); + } + } + }); + connection.on("end", function() { + console.log("local disconnected"); + if (ws) { + ws.terminate(); + } + return server.getConnections(function(err, count) { + console.log("concurrent connections:", count); + }); + }); + connection.on("error", function(e) { + console.log("local error: " + e); + if (ws) { + ws.terminate(); + } + return server.getConnections(function(err, count) { + console.log("concurrent connections:", count); + }); + }); + connection.on("drain", function() { + if (ws && ws._socket) { + return ws._socket.resume(); + } + }); + return connection.setTimeout(timeout, function() { + console.log("local timeout"); + connection.destroy(); + if (ws) { + return ws.terminate(); + } + }); + }); + + server.listen(PORT, LOCAL_ADDRESS, function() { + var address; + address = server.address(); + return console.log("server listening at", address); + }); + + server.on("error", function(e) { + if (e.code === "EADDRINUSE") { + console.log("address in use, aborting"); + } + return process.exit(1); + }); + +}).call(this); diff --git a/package.json b/package.json new file mode 100644 index 0000000..0d211bb --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "shadowsocks-heroku", + "version": "0.9.10", + "dependencies": { + "minimist": "^1.2.0", + "https-proxy-agent": "^1.0.0", + "ws": "^1.0.1" + }, + "engines": { + "node": "4.x" + }, + "scripts": { + "start": "node server.js -b 0.0.0.0" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..cb45dbb --- /dev/null +++ b/server.js @@ -0,0 +1,226 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var Encryptor, KEY, LOCAL_ADDRESS, METHOD, PORT, WebSocket, WebSocketServer, config, configContent, configFile, configFromArgs, fs, http, inetNtoa, k, net, options, parseArgs, path, ref, server, timeout, v, wss; + + net = require("net"); + + fs = require("fs"); + + path = require("path"); + + http = require("http"); + + WebSocket = require('ws'); + + WebSocketServer = WebSocket.Server; + + parseArgs = require("minimist"); + + Encryptor = require("./encrypt").Encryptor; + + options = { + alias: { + 'b': 'local_address', + 'r': 'remote_port', + 'k': 'password', + 'c': 'config_file', + 'm': 'method' + }, + string: ['local_address', 'password', 'method', 'config_file'], + "default": { + 'config_file': path.resolve(__dirname, "config.json") + } + }; + + inetNtoa = function(buf) { + return buf[0] + "." + buf[1] + "." + buf[2] + "." + buf[3]; + }; + + configFromArgs = parseArgs(process.argv.slice(2), options); + + configFile = configFromArgs.config_file; + + configContent = fs.readFileSync(configFile); + + config = JSON.parse(configContent); + + if (process.env.PORT) { + config['remote_port'] = +process.env.PORT; + } + + if (process.env.KEY) { + config['password'] = process.env.KEY; + } + + if (process.env.METHOD) { + config['method'] = process.env.METHOD; + } + + for (k in configFromArgs) { + v = configFromArgs[k]; + config[k] = v; + } + + timeout = Math.floor(config.timeout * 1000); + + LOCAL_ADDRESS = config.local_address; + + PORT = config.remote_port; + + KEY = config.password; + + METHOD = config.method; + + if ((ref = METHOD.toLowerCase()) === "" || ref === "null" || ref === "table") { + METHOD = null; + } + + server = http.createServer(function(req, res) { + res.writeHead(200, { + 'Content-Type': 'text/plain' + }); + return res.end("asdf."); + }); + + wss = new WebSocketServer({ + server: server + }); + + wss.on("connection", function(ws) { + var addrLen, cachedPieces, encryptor, headerLength, remote, remoteAddr, remotePort, stage; + console.log("server connected"); + console.log("concurrent connections:", wss.clients.length); + encryptor = new Encryptor(KEY, METHOD); + stage = 0; + headerLength = 0; + remote = null; + cachedPieces = []; + addrLen = 0; + remoteAddr = null; + remotePort = null; + ws.on("message", function(data, flags) { + var addrtype, buf, e, error; + data = encryptor.decrypt(data); + if (stage === 5) { + if (!remote.write(data)) { + ws._socket.pause(); + } + return; + } + if (stage === 0) { + try { + addrtype = data[0]; + if (addrtype === 3) { + addrLen = data[1]; + } else if (addrtype !== 1) { + console.warn("unsupported addrtype: " + addrtype); + ws.close(); + return; + } + if (addrtype === 1) { + remoteAddr = inetNtoa(data.slice(1, 5)); + remotePort = data.readUInt16BE(5); + headerLength = 7; + } else { + remoteAddr = data.slice(2, 2 + addrLen).toString("binary"); + remotePort = data.readUInt16BE(2 + addrLen); + headerLength = 2 + addrLen + 2; + } + remote = net.connect(remotePort, remoteAddr, function() { + var i, piece; + console.log("connecting", remoteAddr); + i = 0; + while (i < cachedPieces.length) { + piece = cachedPieces[i]; + remote.write(piece); + i++; + } + cachedPieces = null; + return stage = 5; + }); + remote.on("data", function(data) { + data = encryptor.encrypt(data); + if (ws.readyState === WebSocket.OPEN) { + ws.send(data, { + binary: true + }); + if (ws.bufferedAmount > 0) { + remote.pause(); + } + } + }); + remote.on("end", function() { + ws.close(); + return console.log("remote disconnected"); + }); + remote.on("drain", function() { + return ws._socket.resume(); + }); + remote.on("error", function(e) { + ws.terminate(); + return console.log("remote: " + e); + }); + remote.setTimeout(timeout, function() { + console.log("remote timeout"); + remote.destroy(); + return ws.close(); + }); + if (data.length > headerLength) { + buf = new Buffer(data.length - headerLength); + data.copy(buf, 0, headerLength); + cachedPieces.push(buf); + buf = null; + } + return stage = 4; + } catch (error) { + e = error; + console.warn(e); + if (remote) { + remote.destroy(); + } + return ws.close(); + } + } else { + if (stage === 4) { + return cachedPieces.push(data); + } + } + }); + ws.on("ping", function() { + return ws.pong('', null, true); + }); + ws._socket.on("drain", function() { + if (stage === 5) { + return remote.resume(); + } + }); + ws.on("close", function() { + console.log("server disconnected"); + console.log("concurrent connections:", wss.clients.length); + if (remote) { + return remote.destroy(); + } + }); + return ws.on("error", function(e) { + console.warn("server: " + e); + console.log("concurrent connections:", wss.clients.length); + if (remote) { + return remote.destroy(); + } + }); + }); + + server.listen(PORT, LOCAL_ADDRESS, function() { + var address; + address = server.address(); + return console.log("server listening at", address); + }); + + server.on("error", function(e) { + if (e.code === "EADDRINUSE") { + console.log("address in use, aborting"); + } + return process.exit(1); + }); + +}).call(this); diff --git a/src/encrypt.coffee b/src/encrypt.coffee new file mode 100644 index 0000000..a37514b --- /dev/null +++ b/src/encrypt.coffee @@ -0,0 +1,160 @@ +crypto = require("crypto") +merge_sort = require("./merge_sort").merge_sort +int32Max = Math.pow(2, 32) + +cachedTables = {} # password: [encryptTable, decryptTable] + +getTable = (key) -> + if cachedTables[key] + return cachedTables[key] + console.log "calculating ciphers" + table = new Array(256) + decrypt_table = new Array(256) + md5sum = crypto.createHash("md5") + md5sum.update key + hash = new Buffer(md5sum.digest(), "binary") + al = hash.readUInt32LE(0) + ah = hash.readUInt32LE(4) + i = 0 + + while i < 256 + table[i] = i + i++ + i = 1 + + while i < 1024 + table = merge_sort(table, (x, y) -> + ((ah % (x + i)) * int32Max + al) % (x + i) - ((ah % (y + i)) * int32Max + al) % (y + i) + ) + i++ + i = 0 + while i < 256 + decrypt_table[table[i]] = i + ++i + result = [table, decrypt_table] + cachedTables[key] = result + result + +substitute = (table, buf) -> + i = 0 + + while i < buf.length + buf[i] = table[buf[i]] + i++ + buf + +bytes_to_key_results = {} + +EVP_BytesToKey = (password, key_len, iv_len) -> + if bytes_to_key_results["#{password}:#{key_len}:#{iv_len}"] + return bytes_to_key_results["#{password}:#{key_len}:#{iv_len}"] + m = [] + i = 0 + count = 0 + while count < key_len + iv_len + md5 = crypto.createHash('md5') + data = password + if i > 0 + data = Buffer.concat([m[i - 1], password]) + md5.update(data) + d = md5.digest() + m.push(d) + count += d.length + i += 1 + ms = Buffer.concat(m) + key = ms.slice(0, key_len) + iv = ms.slice(key_len, key_len + iv_len) + bytes_to_key_results[password] = [key, iv] + return [key, iv] + + +method_supported = + 'aes-128-cfb': [16, 16] + 'aes-192-cfb': [24, 16] + 'aes-256-cfb': [32, 16] + 'bf-cfb': [16, 8] + 'camellia-128-cfb': [16, 16] + 'camellia-192-cfb': [24, 16] + 'camellia-256-cfb': [32, 16] + 'cast5-cfb': [16, 8] + 'des-cfb': [8, 8] + 'idea-cfb': [16, 8] + 'rc2-cfb': [16, 8] + 'rc4': [16, 0] + 'rc4-md5': [16, 16] + 'seed-cfb': [16, 16] + + +create_rc4_md5_cipher = (key, iv, op) -> + md5 = crypto.createHash('md5') + md5.update(key) + md5.update(iv) + rc4_key = md5.digest() + if op == 1 + return crypto.createCipheriv('rc4', rc4_key, '') + else + return crypto.createDecipheriv('rc4', rc4_key, '') + + +class Encryptor + constructor: (@key, @method) -> + @iv_sent = false + if @method == 'table' + @method = null + if @method? + @cipher = @get_cipher(@key, @method, 1, crypto.randomBytes(32)) + else + [@encryptTable, @decryptTable] = getTable(@key) + + get_cipher_len: (method) -> + method = method.toLowerCase() + m = method_supported[method] + return m + + get_cipher: (password, method, op, iv) -> + method = method.toLowerCase() + password = new Buffer(password, 'binary') + m = @get_cipher_len(method) + if m? + [key, iv_] = EVP_BytesToKey(password, m[0], m[1]) + if not iv? + iv = iv_ + if op == 1 + @cipher_iv = iv.slice(0, m[1]) + iv = iv.slice(0, m[1]) + if method == 'rc4-md5' + return create_rc4_md5_cipher(key, iv, op) + else + if op == 1 + return crypto.createCipheriv(method, key, iv) + else + return crypto.createDecipheriv(method, key, iv) + + encrypt: (buf) -> + if @method? + result = @cipher.update buf + if @iv_sent + return result + else + @iv_sent = true + return Buffer.concat([@cipher_iv, result]) + else + substitute @encryptTable, buf + + decrypt: (buf) -> + if @method? + if not @decipher? + decipher_iv_len = @get_cipher_len(@method)[1] + decipher_iv = buf.slice(0, decipher_iv_len) + @decipher = @get_cipher(@key, @method, 0, decipher_iv) + result = @decipher.update(buf.slice(decipher_iv_len)) + return result + else + result = @decipher.update(buf) + return result + else + substitute @decryptTable, buf + + +exports.Encryptor = Encryptor +exports.getTable = getTable diff --git a/src/local.coffee b/src/local.coffee new file mode 100644 index 0000000..7795a1a --- /dev/null +++ b/src/local.coffee @@ -0,0 +1,259 @@ +net = require("net") +url = require("url") +http = require("http") +fs = require("fs") +path = require("path") +WebSocket = require('ws') +parseArgs = require("minimist") +HttpsProxyAgent = require('https-proxy-agent') +Encryptor = require("./encrypt").Encryptor + +options = + alias: + 'b': 'local_address' + 'l': 'local_port' + 's': 'server' + 'r': 'remote_port' + 'k': 'password', + 'c': 'config_file', + 'm': 'method' + string: ['local_address', 'server', 'password', + 'config_file', 'method', 'scheme'] + default: + 'config_file': path.resolve(__dirname, "config.json") + +inetNtoa = (buf) -> + buf[0] + "." + buf[1] + "." + buf[2] + "." + buf[3] + +configFromArgs = parseArgs process.argv.slice(2), options +configContent = fs.readFileSync(configFromArgs.config_file) +config = JSON.parse(configContent) +for k, v of configFromArgs + config[k] = v + +SCHEME = config.scheme +SERVER = config.server +REMOTE_PORT = config.remote_port +LOCAL_ADDRESS = config.local_address +PORT = config.local_port +KEY = config.password +METHOD = config.method +timeout = Math.floor(config.timeout * 1000) + +if METHOD.toLowerCase() in ["", "null", "table"] + METHOD = null + +HTTPPROXY = process.env.http_proxy + +if HTTPPROXY + console.log "http proxy:", HTTPPROXY + + +prepareServer = (address) -> + serverUrl = url.parse address + serverUrl.slashes = true + serverUrl.protocol ?= SCHEME + if serverUrl.hostname is null + serverUrl.hostname = address + serverUrl.pathname = '/' + serverUrl.port ?= REMOTE_PORT + url.format(serverUrl) + +if SERVER instanceof Array + SERVER = (prepareServer(s) for s in SERVER) +else + SERVER = prepareServer(SERVER) + +getServer = -> + if SERVER instanceof Array + SERVER[Math.floor(Math.random() * SERVER.length)] + else + SERVER + +server = net.createServer (connection) -> + console.log "local connected" + server.getConnections (err, count) -> + console.log "concurrent connections:", count + return + encryptor = new Encryptor(KEY, METHOD) + stage = 0 + headerLength = 0 + cachedPieces = [] + addrLen = 0 + ws = null + ping = null + remoteAddr = null + remotePort = null + addrToSend = "" + aServer = getServer() + connection.on "data", (data) -> + if stage is 5 + # pipe sockets + data = encryptor.encrypt data + if ws.readyState is WebSocket.OPEN + ws.send data, { binary: true } + connection.pause() if ws.bufferedAmount > 0 + return + if stage is 0 + tempBuf = new Buffer(2) + tempBuf.write "\u0005\u0000", 0 + connection.write tempBuf + stage = 1 + return + if stage is 1 + try + # +----+-----+-------+------+----------+----------+ + # |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + # +----+-----+-------+------+----------+----------+ + # | 1 | 1 | X'00' | 1 | Variable | 2 | + # +----+-----+-------+------+----------+----------+ + + #cmd and addrtype + cmd = data[1] + addrtype = data[3] + unless cmd is 1 + console.log "unsupported cmd:", cmd + reply = new Buffer("\u0005\u0007\u0000\u0001", "binary") + connection.end reply + return + if addrtype is 3 + addrLen = data[4] + else unless addrtype is 1 + console.log "unsupported addrtype:", addrtype + connection.end() + return + addrToSend = data.slice(3, 4).toString("binary") + # read address and port + if addrtype is 1 + remoteAddr = inetNtoa(data.slice(4, 8)) + addrToSend += data.slice(4, 10).toString("binary") + remotePort = data.readUInt16BE(8) + headerLength = 10 + else + remoteAddr = data.slice(5, 5 + addrLen).toString("binary") + addrToSend += data.slice(4, 5 + addrLen + 2).toString("binary") + remotePort = data.readUInt16BE(5 + addrLen) + headerLength = 5 + addrLen + 2 + buf = new Buffer(10) + buf.write "\u0005\u0000\u0000\u0001", 0, 4, "binary" + buf.write "\u0000\u0000\u0000\u0000", 4, 4, "binary" + buf.writeInt16BE remotePort, 8 + connection.write buf + # connect to remote server + # ws = new WebSocket aServer, protocol: "binary" + + if HTTPPROXY + # WebSocket endpoint for the proxy to connect to + endpoint = aServer + parsed = url.parse(endpoint) + #console.log('attempting to connect to WebSocket %j', endpoint); + + # create an instance of the `HttpsProxyAgent` class with the proxy server information + opts = url.parse(HTTPPROXY) + + # IMPORTANT! Set the `secureEndpoint` option to `false` when connecting + # over "ws://", but `true` when connecting over "wss://" + opts.secureEndpoint = parsed.protocol ? parsed.protocol == 'wss:' : false + + agent = new HttpsProxyAgent(opts) + + ws = new WebSocket(aServer, { + protocol: "binary", + agent: agent + }); + else + ws = new WebSocket(aServer, { + protocol: "binary" + }); + + ws.on "open", -> + ws._socket.on "error", (e) -> + console.log "remote #{remoteAddr}:#{remotePort} #{e}" + connection.destroy() + server.getConnections (err, count) -> + console.log "concurrent connections:", count + return + + console.log "connecting #{remoteAddr} via #{aServer}" + addrToSendBuf = new Buffer(addrToSend, "binary") + addrToSendBuf = encryptor.encrypt addrToSendBuf + ws.send addrToSendBuf, { binary: true } + i = 0 + + while i < cachedPieces.length + piece = cachedPieces[i] + piece = encryptor.encrypt piece + ws.send piece, { binary: true } + i++ + cachedPieces = null # save memory + stage = 5 + + ping = setInterval(-> + ws.ping "", null, true + , 50 * 1000) + + ws._socket.on "drain", -> + connection.resume() + + return + + ws.on "message", (data, flags) -> + data = encryptor.decrypt data + ws._socket.pause() unless connection.write(data) + + ws.on "close", -> + clearInterval ping + console.log "remote disconnected" + connection.destroy() + + ws.on "error", (e) -> + console.log "remote #{remoteAddr}:#{remotePort} error: #{e}" + connection.destroy() + server.getConnections (err, count) -> + console.log "concurrent connections:", count + return + + if data.length > headerLength + buf = new Buffer(data.length - headerLength) + data.copy buf, 0, headerLength + cachedPieces.push buf + buf = null + stage = 4 + catch e + # may encounter index out of range + console.log e + connection.destroy() + else cachedPieces.push data if stage is 4 + # remote server not connected + # cache received buffers + # make sure no data is lost + + connection.on "end", -> + console.log "local disconnected" + ws.terminate() if ws + server.getConnections (err, count) -> + console.log "concurrent connections:", count + return + + connection.on "error", (e)-> + console.log "local error: #{e}" + ws.terminate() if ws + server.getConnections (err, count) -> + console.log "concurrent connections:", count + return + + connection.on "drain", -> + ws._socket.resume() if ws and ws._socket + + connection.setTimeout timeout, -> + console.log "local timeout" + connection.destroy() + ws.terminate() if ws + +server.listen PORT, LOCAL_ADDRESS, -> + address = server.address() + console.log "server listening at", address + +server.on "error", (e) -> + console.log "address in use, aborting" if e.code is "EADDRINUSE" + process.exit 1 diff --git a/src/merge_sort.coffee b/src/merge_sort.coffee new file mode 100644 index 0000000..c3bffe0 --- /dev/null +++ b/src/merge_sort.coffee @@ -0,0 +1,15 @@ +merge = (left, right, comparison) -> + result = new Array() + while (left.length > 0) and (right.length > 0) + if comparison(left[0], right[0]) <= 0 + result.push left.shift() + else + result.push right.shift() + result.push left.shift() while left.length > 0 + result.push right.shift() while right.length > 0 + result +merge_sort = (array, comparison) -> + return array if array.length < 2 + middle = Math.ceil(array.length / 2) + merge merge_sort(array.slice(0, middle), comparison), merge_sort(array.slice(middle), comparison), comparison +exports.merge_sort = merge_sort diff --git a/src/server.coffee b/src/server.coffee new file mode 100644 index 0000000..c9b65d7 --- /dev/null +++ b/src/server.coffee @@ -0,0 +1,160 @@ +net = require("net") +fs = require("fs") +path = require("path") +http = require("http") +WebSocket = require('ws') +WebSocketServer = WebSocket.Server +parseArgs = require("minimist") +Encryptor = require("./encrypt").Encryptor + +options = + alias: + 'b': 'local_address' + 'r': 'remote_port' + 'k': 'password', + 'c': 'config_file', + 'm': 'method' + string: ['local_address', 'password', 'method', 'config_file'] + default: + 'config_file': path.resolve(__dirname, "config.json") + +inetNtoa = (buf) -> + buf[0] + "." + buf[1] + "." + buf[2] + "." + buf[3] + +configFromArgs = parseArgs process.argv.slice(2), options +configFile = configFromArgs.config_file +configContent = fs.readFileSync(configFile) +config = JSON.parse(configContent) + +config['remote_port'] = +process.env.PORT if process.env.PORT +config['password'] = process.env.KEY if process.env.KEY +config['method'] = process.env.METHOD if process.env.METHOD + +for k, v of configFromArgs + config[k] = v + +timeout = Math.floor(config.timeout * 1000) +LOCAL_ADDRESS = config.local_address +PORT = config.remote_port +KEY = config.password +METHOD = config.method + +if METHOD.toLowerCase() in ["", "null", "table"] + METHOD = null + +server = http.createServer (req, res) -> + res.writeHead 200, 'Content-Type': 'text/plain' + res.end("asdf.") + +wss = new WebSocketServer server: server + +wss.on "connection", (ws) -> + console.log "server connected" + console.log "concurrent connections:", wss.clients.length + encryptor = new Encryptor(KEY, METHOD) + stage = 0 + headerLength = 0 + remote = null + cachedPieces = [] + addrLen = 0 + remoteAddr = null + remotePort = null + ws.on "message", (data, flags) -> + data = encryptor.decrypt data + if stage is 5 + ws._socket.pause() unless remote.write(data) + return + if stage is 0 + try + addrtype = data[0] + if addrtype is 3 + addrLen = data[1] + else unless addrtype is 1 + console.warn "unsupported addrtype: " + addrtype + ws.close() + return + # read address and port + if addrtype is 1 + remoteAddr = inetNtoa(data.slice(1, 5)) + remotePort = data.readUInt16BE(5) + headerLength = 7 + else + remoteAddr = data.slice(2, 2 + addrLen).toString("binary") + remotePort = data.readUInt16BE(2 + addrLen) + headerLength = 2 + addrLen + 2 + + # connect remote server + remote = net.connect(remotePort, remoteAddr, -> + console.log "connecting", remoteAddr + i = 0 + + while i < cachedPieces.length + piece = cachedPieces[i] + remote.write piece + i++ + cachedPieces = null # save memory + stage = 5 + ) + remote.on "data", (data) -> + data = encryptor.encrypt data + if ws.readyState is WebSocket.OPEN + ws.send data, { binary: true } + remote.pause() if ws.bufferedAmount > 0 + return + + remote.on "end", -> + ws.close() + console.log "remote disconnected" + + remote.on "drain", -> + ws._socket.resume() + + remote.on "error", (e)-> + ws.terminate() + console.log "remote: #{e}" + + remote.setTimeout timeout, -> + console.log "remote timeout" + remote.destroy() + ws.close() + + if data.length > headerLength + # make sure no data is lost + buf = new Buffer(data.length - headerLength) + data.copy buf, 0, headerLength + cachedPieces.push buf + buf = null + stage = 4 + catch e + # may encouter index out of range + console.warn e + remote.destroy() if remote + ws.close() + else cachedPieces.push data if stage is 4 + # remote server not connected + # cache received buffers + # make sure no data is lost + + ws.on "ping", -> + ws.pong '', null, true + + ws._socket.on "drain", -> + remote.resume() if stage is 5 + + ws.on "close", -> + console.log "server disconnected" + console.log "concurrent connections:", wss.clients.length + remote.destroy() if remote + + ws.on "error", (e) -> + console.warn "server: #{e}" + console.log "concurrent connections:", wss.clients.length + remote.destroy() if remote + +server.listen PORT, LOCAL_ADDRESS, -> + address = server.address() + console.log "server listening at", address + +server.on "error", (e) -> + console.log "address in use, aborting" if e.code is "EADDRINUSE" + process.exit 1 diff --git a/src/test.coffee b/src/test.coffee new file mode 100644 index 0000000..e79432c --- /dev/null +++ b/src/test.coffee @@ -0,0 +1,75 @@ +# test encryption +encrypt = require("./encrypt") +target = [ + [ 60, 53, 84, 138, 217, 94, 88, 23, 39, 242, 219, 35, 12, 157, 165, 181, 255, 143, 83, 247, 162, 16, 31, 209, 190, 171, 115, 65, 38, 41, 21, 245, 236, 46, 121, 62, 166, 233, 44, 154, 153, 145, 230, 49, 128, 216, 173, 29, 241, 119, 64, 229, 194, 103, 131, 110, 26, 197, 218, 59, 204, 56, 27, 34, 141, 221, 149, 239, 192, 195, 24, 155, 170, 183, 11, 254, 213, 37, 137, 226, 75, 203, 55, 19, 72, 248, 22, 129, 33, 175, 178, 10, 198, 71, 77, 36, 113, 167, 48, 2, 117, 140, 142, 66, 199, 232, 243, 32, 123, 54, 51, 82, 57, 177, 87, 251, 150, 196, 133, 5, 253, 130, 8, 184, 14, 152, 231, 3, 186, 159, 76, 89, 228, 205, 156, 96, 163, 146, 18, 91, 132, 85, 80, 109, 172, 176, 105, 13, 50, 235, 127, 0, 189, 95, 98, 136, 250, 200, 108, 179, 211, 214, 106, 168, 78, 79, 74, 210, 30, 73, 201, 151, 208, 114, 101, 174, 92, 52, 120, 240, 15, 169, 220, 182, 81, 224, 43, 185, 40, 99, 180, 17, 212, 158, 42, 90, 9, 191, 45, 6, 25, 4, 222, 67, 126, 1, 116, 124, 206, 69, 61, 7, 68, 97, 202, 63, 244, 20, 28, 58, 93, 134, 104, 144, 227, 147, 102, 118, 135, 148, 47, 238, 86, 112, 122, 70, 107, 215, 100, 139, 223, 225, 164, 237, 111, 125, 207, 160, 187, 246, 234, 161, 188, 193, 249, 252 ], + [ 151, 205, 99, 127, 201, 119, 199, 211, 122, 196, 91, 74, 12, 147, 124, 180, 21, 191, 138, 83, 217, 30, 86, 7, 70, 200, 56, 62, 218, 47, 168, 22, 107, 88, 63, 11, 95, 77, 28, 8, 188, 29, 194, 186, 38, 198, 33, 230, 98, 43, 148, 110, 177, 1, 109, 82, 61, 112, 219, 59, 0, 210, 35, 215, 50, 27, 103, 203, 212, 209, 235, 93, 84, 169, 166, 80, 130, 94, 164, 165, 142, 184, 111, 18, 2, 141, 232, 114, 6, 131, 195, 139, 176, 220, 5, 153, 135, 213, 154, 189, 238, 174, 226, 53, 222, 146, 162, 236, 158, 143, 55, 244, 233, 96, 173, 26, 206, 100, 227, 49, 178, 34, 234, 108, 207, 245, 204, 150, 44, 87, 121, 54, 140, 118, 221, 228, 155, 78, 3, 239, 101, 64, 102, 17, 223, 41, 137, 225, 229, 66, 116, 171, 125, 40, 39, 71, 134, 13, 193, 129, 247, 251, 20, 136, 242, 14, 36, 97, 163, 181, 72, 25, 144, 46, 175, 89, 145, 113, 90, 159, 190, 15, 183, 73, 123, 187, 128, 248, 252, 152, 24, 197, 68, 253, 52, 69, 117, 57, 92, 104, 157, 170, 214, 81, 60, 133, 208, 246, 172, 23, 167, 160, 192, 76, 161, 237, 45, 4, 58, 10, 182, 65, 202, 240, 185, 241, 79, 224, 132, 51, 42, 126, 105, 37, 250, 149, 32, 243, 231, 67, 179, 48, 9, 106, 216, 31, 249, 19, 85, 254, 156, 115, 255, 120, 75, 16 ] +] +tables = encrypt.getTable("foobar!") +console.log JSON.stringify(tables) +i = 0 + +while i < 256 + console.assert tables[0][i] is target[0][i] + console.assert tables[1][i] is target[1][i] + i++ + +# test proxy + +child_process = require('child_process') +local = child_process.spawn('node', ['local.js']) +server = child_process.spawn('node', ['server.js']) + +curlRunning = false + +local.on 'exit', (code)-> + server.kill() + if !curlRunning + process.exit code + +server.on 'exit', (code)-> + local.kill() + if !curlRunning + process.exit code + +localReady = false +serverReady = false +curlRunning = false + +runCurl = -> + curlRunning = true + curl = child_process.spawn 'curl', ['-v', 'http://www.example.com/', '-L', '--socks5-hostname', '127.0.0.1:1080'] + curl.on 'exit', (code)-> + local.kill() + server.kill() + if code is 0 + console.log 'Test passed' + process.exit 0 + else + console.error 'Test failed' + process.exit code + + curl.stdout.on 'data', (data) -> + console.log data.toString() + + curl.stderr.on 'data', (data) -> + console.warn data.toString() + +local.stderr.on 'data', (data) -> + console.warn data.toString() + +server.stderr.on 'data', (data) -> + console.warn data.toString() + +local.stdout.on 'data', (data) -> + console.log data.toString() + if data.toString().indexOf('listening at') >= 0 + localReady = true + if localReady and serverReady and not curlRunning + runCurl() + +server.stdout.on 'data', (data) -> + console.log data.toString() + if data.toString().indexOf('listening at') >= 0 + serverReady = true + if localReady and serverReady and not curlRunning + runCurl() diff --git a/test.js b/test.js new file mode 100644 index 0000000..891376f --- /dev/null +++ b/test.js @@ -0,0 +1,100 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var child_process, curlRunning, encrypt, i, local, localReady, runCurl, server, serverReady, tables, target; + + encrypt = require("./encrypt"); + + target = [[60, 53, 84, 138, 217, 94, 88, 23, 39, 242, 219, 35, 12, 157, 165, 181, 255, 143, 83, 247, 162, 16, 31, 209, 190, 171, 115, 65, 38, 41, 21, 245, 236, 46, 121, 62, 166, 233, 44, 154, 153, 145, 230, 49, 128, 216, 173, 29, 241, 119, 64, 229, 194, 103, 131, 110, 26, 197, 218, 59, 204, 56, 27, 34, 141, 221, 149, 239, 192, 195, 24, 155, 170, 183, 11, 254, 213, 37, 137, 226, 75, 203, 55, 19, 72, 248, 22, 129, 33, 175, 178, 10, 198, 71, 77, 36, 113, 167, 48, 2, 117, 140, 142, 66, 199, 232, 243, 32, 123, 54, 51, 82, 57, 177, 87, 251, 150, 196, 133, 5, 253, 130, 8, 184, 14, 152, 231, 3, 186, 159, 76, 89, 228, 205, 156, 96, 163, 146, 18, 91, 132, 85, 80, 109, 172, 176, 105, 13, 50, 235, 127, 0, 189, 95, 98, 136, 250, 200, 108, 179, 211, 214, 106, 168, 78, 79, 74, 210, 30, 73, 201, 151, 208, 114, 101, 174, 92, 52, 120, 240, 15, 169, 220, 182, 81, 224, 43, 185, 40, 99, 180, 17, 212, 158, 42, 90, 9, 191, 45, 6, 25, 4, 222, 67, 126, 1, 116, 124, 206, 69, 61, 7, 68, 97, 202, 63, 244, 20, 28, 58, 93, 134, 104, 144, 227, 147, 102, 118, 135, 148, 47, 238, 86, 112, 122, 70, 107, 215, 100, 139, 223, 225, 164, 237, 111, 125, 207, 160, 187, 246, 234, 161, 188, 193, 249, 252], [151, 205, 99, 127, 201, 119, 199, 211, 122, 196, 91, 74, 12, 147, 124, 180, 21, 191, 138, 83, 217, 30, 86, 7, 70, 200, 56, 62, 218, 47, 168, 22, 107, 88, 63, 11, 95, 77, 28, 8, 188, 29, 194, 186, 38, 198, 33, 230, 98, 43, 148, 110, 177, 1, 109, 82, 61, 112, 219, 59, 0, 210, 35, 215, 50, 27, 103, 203, 212, 209, 235, 93, 84, 169, 166, 80, 130, 94, 164, 165, 142, 184, 111, 18, 2, 141, 232, 114, 6, 131, 195, 139, 176, 220, 5, 153, 135, 213, 154, 189, 238, 174, 226, 53, 222, 146, 162, 236, 158, 143, 55, 244, 233, 96, 173, 26, 206, 100, 227, 49, 178, 34, 234, 108, 207, 245, 204, 150, 44, 87, 121, 54, 140, 118, 221, 228, 155, 78, 3, 239, 101, 64, 102, 17, 223, 41, 137, 225, 229, 66, 116, 171, 125, 40, 39, 71, 134, 13, 193, 129, 247, 251, 20, 136, 242, 14, 36, 97, 163, 181, 72, 25, 144, 46, 175, 89, 145, 113, 90, 159, 190, 15, 183, 73, 123, 187, 128, 248, 252, 152, 24, 197, 68, 253, 52, 69, 117, 57, 92, 104, 157, 170, 214, 81, 60, 133, 208, 246, 172, 23, 167, 160, 192, 76, 161, 237, 45, 4, 58, 10, 182, 65, 202, 240, 185, 241, 79, 224, 132, 51, 42, 126, 105, 37, 250, 149, 32, 243, 231, 67, 179, 48, 9, 106, 216, 31, 249, 19, 85, 254, 156, 115, 255, 120, 75, 16]]; + + tables = encrypt.getTable("foobar!"); + + console.log(JSON.stringify(tables)); + + i = 0; + + while (i < 256) { + console.assert(tables[0][i] === target[0][i]); + console.assert(tables[1][i] === target[1][i]); + i++; + } + + child_process = require('child_process'); + + local = child_process.spawn('node', ['local.js']); + + server = child_process.spawn('node', ['server.js']); + + curlRunning = false; + + local.on('exit', function(code) { + server.kill(); + if (!curlRunning) { + return process.exit(code); + } + }); + + server.on('exit', function(code) { + local.kill(); + if (!curlRunning) { + return process.exit(code); + } + }); + + localReady = false; + + serverReady = false; + + curlRunning = false; + + runCurl = function() { + var curl; + curlRunning = true; + curl = child_process.spawn('curl', ['-v', 'http://www.example.com/', '-L', '--socks5-hostname', '127.0.0.1:1080']); + curl.on('exit', function(code) { + local.kill(); + server.kill(); + if (code === 0) { + console.log('Test passed'); + return process.exit(0); + } else { + console.error('Test failed'); + return process.exit(code); + } + }); + curl.stdout.on('data', function(data) { + return console.log(data.toString()); + }); + return curl.stderr.on('data', function(data) { + return console.warn(data.toString()); + }); + }; + + local.stderr.on('data', function(data) { + return console.warn(data.toString()); + }); + + server.stderr.on('data', function(data) { + return console.warn(data.toString()); + }); + + local.stdout.on('data', function(data) { + console.log(data.toString()); + if (data.toString().indexOf('listening at') >= 0) { + localReady = true; + if (localReady && serverReady && !curlRunning) { + return runCurl(); + } + } + }); + + server.stdout.on('data', function(data) { + console.log(data.toString()); + if (data.toString().indexOf('listening at') >= 0) { + serverReady = true; + if (localReady && serverReady && !curlRunning) { + return runCurl(); + } + } + }); + +}).call(this);