├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── benchmark ├── README.md ├── conclude.js ├── config.js ├── createClients.js ├── createResourceServer.js ├── run.js └── utils.js ├── bin ├── localssjs └── serverssjs ├── config.json ├── lib ├── auth.js ├── cli.js ├── config.js ├── createUDPRelay.js ├── daemon.js ├── defaultConfig.js ├── encryptor.js ├── filter.js ├── gfwlistUtils.js ├── index.js ├── logger.js ├── pacServer.js ├── pid.js ├── pm.js ├── recordMemoryUsage.js ├── ssLocal.js ├── ssServer.js └── utils.js ├── pac ├── gfwlist.txt └── user.txt ├── package.json ├── src ├── auth.js ├── cli.js ├── config.js ├── createUDPRelay.js ├── daemon.js ├── defaultConfig.js ├── encryptor.js ├── filter.js ├── gfwlistUtils.js ├── index.js ├── logger.js ├── pacServer.js ├── pid.js ├── pm.js ├── recordMemoryUsage.js ├── ssLocal.js ├── ssServer.js └── utils.js ├── test ├── auth.js ├── cli.js ├── config.js ├── createUDPRelay.js ├── encryptor.js ├── filter.js ├── httpProxy.js ├── pacServer.js ├── testServer.js ├── tools │ └── writeArgv.js └── utils.js └── vendor └── ADPMatcher.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-loose"], 3 | "plugins": ["transform-async-to-generator"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test 2 | lib 3 | bin 4 | vendor 5 | pac 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb-base', 3 | rules: { 4 | 'comma-dangle': 0, 5 | 'no-underscore-dangle': 0 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | **/node_modules 3 | research 4 | **.log 5 | tmp 6 | logs 7 | TODO.md 8 | /coverage 9 | deprecated 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # .gitignore 2 | .DS_Store 3 | **/node_modules 4 | research 5 | **.log 6 | tmp 7 | logs 8 | TODO.md 9 | /coverage 10 | deprecated 11 | 12 | src 13 | benchmark 14 | test 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "6" 6 | - "8" 7 | script: 8 | - npm run travis-ci-test 9 | branches: 10 | only: 11 | - master 12 | - dev 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.4.2 4 | 5 | ## 1.4.1@alpha 6 | 7 | * Core: Support passing a `proxyOptions` in `pm` instead of creating a config object from `argv`. 8 | 9 | ## 1.4.0 10 | 11 | * Core: Allow to set log path. 12 | * Core: Store list in base64. 13 | * Core: Manage process with `pm2`. 14 | 15 | ## 1.2.0 16 | 17 | * Bug fix: Respect the "serverAddr" option and set default "serverAddr" to "0.0.0.0". 18 | 19 | ## 1.1.4 20 | 21 | * Core: Support http-proxy. 22 | 23 | ## 1.1.3 24 | 25 | * Core: Support SOCKS5 username/password authetication. 26 | 27 | ## 1.1.2 28 | 29 | * Core: Add domain resolving support. 30 | 31 | * Bug fix: The option `-c` resolved path incorrectly. 32 | 33 | * Bug fix: Windows do not accept kill signals. 34 | 35 | ## 1.1.1 36 | 37 | * Bug fix: `getDstInfo` may return buffers with zero length and throw uncaught error when reading these buffers 38 | 39 | ## 1.1.0 40 | 41 | * Core: Add .pac file server 42 | * Update rules from gfwlist 43 | * Support adding user rules 44 | 45 | * Core: Seperate log files 46 | 47 | * Extra: Add benchmark 48 | 49 | ## 1.0.4(2016-05-21) 50 | 51 | * Bug fix: typo `clientToDst.resumse()` 52 | 53 | * Babel: Enable babel loose mode. 54 | 55 | ## 1.0.3(2016-05-21) 56 | 57 | * Change: Do not log timeout warning. 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) , 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # encryptsocks 2 | 3 | [![npm-version](https://img.shields.io/npm/v/encryptsocks.svg?style=flat-square)](https://www.npmjs.com/package/encryptsocks) 4 | [![Build Status](https://travis-ci.org/oyyd/encryptsocks.svg?branch=master)](https://travis-ci.org/oyyd/encryptsocks) 5 | 6 | Encrypt your socks transmission. 7 | 8 | * [Why another Nodejs implementation? (with Benchmark)](https://github.com/oyyd/encryptsocks#why-another-nodejs-implementation) 9 | * [CLI](https://github.com/oyyd/encryptsocks#cli) 10 | * [Examples](https://github.com/oyyd/encryptsocks#examples) 11 | * [Config](https://github.com/oyyd/encryptsocks#config) 12 | * [SOCKS5 Username Password Authetication](https://github.com/oyyd/encryptsocks#socks5-username-password-authetication) 13 | 14 | ## Why another Nodejs implementation? 15 | 16 | __Nodejs is a very good choice to achieve both flexibility and good performance in this situation__. 17 | 18 | And I have found that many of who are familiar with the [original implementation](https://github.com/shadowsocks/shadowsocks-nodejs) may be curious about the memory usage so that I have finished some simple benchmarks to measure its behavior. 19 | 20 | ### Benchmark 21 | 22 | You can get the benchmark details [here](benchmark/README.md) or even test your own implementation. 23 | 24 | After some simple benchmarks that compare both the node and python implementation, my conclusion is: 25 | 26 | 1. Node has a different GC strategy but it's, of course, able to keep thousands of connections with a reasonable memory usage. [It's not a bug, it's a conscious time/space trade-off](https://github.com/nodejs/node-v0.x-archive/issues/4525). 27 | 28 | 2. Each request would cost less time to get responsed (even 50% less time in some situations). 29 | 30 | 3. Node implementation is less likely to fail requests in high concurrency situation. 31 | 32 | And the higher concurrency benchmarks may be meaningless as the bandwidth and network environment would become the actual bottleneck in the real world. 33 | 34 | **Do Please** point out my faults if I have missed something or get something wrong. 35 | 36 | ## Requirement 37 | 38 | node >= v4 39 | 40 | It's recommended to use node v6 to achieve better performance. 41 | 42 | ## Installation 43 | 44 | ``` 45 | npm i -g encryptsocks 46 | ``` 47 | 48 | ## About the daemon 49 | 50 | Encryptsocks use `pm2` as the watcher process from `1.4.0`. 51 | 52 | ## CLI 53 | 54 | Use `localssjs` (local ssjs) to start clients to communicate with applications. The `localssjs` server will also serve a [pac](https://en.wikipedia.org/wiki/PAC) file at `http://127.0.0.1:8090` (by default) for your apps to avoid unnecessary tunnel work. 55 | 56 | You may prefer to navigate [clients page](https://shadowsocks.org/en/download/clients.html) and choose clients for your devices instead of using `localssjs`. 57 | 58 | Use `serverssjs` (server ssjs) to start your remote server. 59 | 60 | Use `localssjs -h` or `serverssjs -h` to show cli options: 61 | 62 | ``` 63 | Proxy options: 64 | -c config path to config file 65 | -s SERVER_ADDR server address, default: 127.0.0.1 66 | -p SERVER_PORT server port, default: 8083 67 | -l LOCAL_ADDR local binding address, default: 127.0.0.1 68 | -b LOCAL_PORT local port, default: 1080 69 | -k PASSWORD password 70 | -m METHOD encryption method, default: aes-128-cfb 71 | -t TIMEOUT timeout in seconds, default: 600 72 | --pac_port PAC_PORT PAC file server port, default: 8090 73 | --pac_update_gfwlist [URL] [localssjs] Update the gfwlist 74 | for PAC server. You can specify the 75 | request URL. 76 | --level LOG_LEVEL log level, default: warn 77 | example: --level verbose 78 | General options: 79 | -h, --help show this help message and exit 80 | -d start/stop/restart daemon mode 81 | ``` 82 | 83 | ### Examples 84 | 85 | Start clients that bind at `1088` and will connect to `MY.SSSERVER.DOMAIN`: 86 | 87 | ``` 88 | $ localssjs -b 1088 -s MY.SSSERVER.DOMAIN 89 | ``` 90 | 91 | Start daemon: 92 | 93 | ``` 94 | $ localssjs -d start -b 1080 95 | ``` 96 | 97 | Log verbosely: 98 | 99 | ``` 100 | $ serverssjs -d start --level verbose 101 | ``` 102 | 103 | Update GFWList for your .pac file server: 104 | 105 | ``` 106 | $ localssjs --pac_update_gfwlist 107 | ``` 108 | 109 | Update GFWList for your .pac file server from a specific URL (default [url](https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt)): 110 | 111 | ``` 112 | $ localssjs --pac_update_gfwlist http://firefoxfan.cc/gfwlist/gfwlist.txt 113 | ``` 114 | 115 | ## Config 116 | 117 | ```json 118 | { 119 | "serverAddr": "127.0.0.1", 120 | "serverPort": 8083, 121 | "localAddr": "127.0.0.1", 122 | "localPort": 1080, 123 | "pacServerPort": 8090, 124 | "password": "YOUR_PASSWORD_HERE", 125 | "timeout": 600, 126 | "method": "aes-128-cfb", 127 | 128 | "level": "warn", 129 | "localAddrIPv6": "::1", 130 | "serverAddrIPv6": "::1" 131 | } 132 | ``` 133 | 134 | Specify your config file with `-c` flag: 135 | 136 | ``` 137 | $ serverssjs -c config.json 138 | ``` 139 | 140 | You can change default config in `config.json` file of your global 141 | package. 142 | 143 | ## SOCKS5 Username Password Authetication 144 | 145 | __NOTE:__ This authetication is dangerous when sniffed. 146 | 147 | Add `auth` property to your `config.json` and make `forceAuth` `true`. 148 | 149 | ```json 150 | { 151 | "auth": { 152 | "forceAuth": true, 153 | "usernamePassword": { 154 | "name": "password" 155 | } 156 | } 157 | } 158 | ``` 159 | 160 | ## [Optimizing](https://github.com/Long-live-shadowsocks/shadowsocks/wiki/Optimizing-Shadowsocks) 161 | 162 | ## Encryption methods 163 | 164 | * aes-128-cfb 165 | * aes-192-cfb 166 | * aes-256-cfb 167 | * bf-cfb 168 | * camellia-128-cfb 169 | * camellia-192-cfb 170 | * camellia-256-cfb 171 | * cast5-cfb 172 | * des-cfb 173 | * idea-cfb 174 | * rc2-cfb 175 | * rc4 176 | * rc4-md5 177 | * seed-cfb 178 | 179 | ## Test 180 | 181 | ``` 182 | $ npm test 183 | ``` 184 | 185 | ## Contribute 186 | 187 | ``` 188 | $ npm run watch 189 | ``` 190 | 191 | ## About the support to UDP relay 192 | 193 | I intend to implement UDP relay and I have implement it. 194 | but I can't find an effective way to test this in real world networking. 195 | Please create issues to help us if you know any applications that support 196 | UDP-socks well. 197 | 198 | ## License 199 | 200 | BSD 201 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | # benchmark 2 | 3 | __NOTE:__ Make sure you are using node v6. 4 | 5 | This benchmark assumes your ss client is listening on port `1080`. You can also use this to test other ss implementation. 6 | 7 | ``` 8 | $ node benchmark/run 9 | ``` 10 | 11 | Modify `config.js` to play around. You may want to `ulimit` the max number of open file descriptors. 12 | 13 | ``` 14 | $ ulimit -S -n 10000 15 | ``` 16 | 17 | ## My Samples 18 | 19 | It's meaningless to run this with a even higher `limitConnections`(concurrent) as the bandwidth would become the bottleneck in real world. 20 | 21 | ### Environment 22 | 23 | OSX 10.11, node v6(default `--max_old_space_size`), python 2.7, both use `localssjs` as the client. 24 | 25 | ### Configuration 26 | 27 | #### shadowsocks 28 | 29 | ``` 30 | $ shadowsocks -c /etc/shadowsocks.json -qq 31 | ``` 32 | 33 | __config.json__ 34 | 35 | ```json 36 | { 37 | "server":"127.0.0.1", 38 | "server_port":8083, 39 | "local_address": "127.0.0.1", 40 | "local_port":1080, 41 | "password":"YOUR_PASSWORD_HERE", 42 | "timeout":300, 43 | "method":"aes-128-cfb", 44 | "fast_open": false 45 | } 46 | ``` 47 | 48 | #### shadowsocks-js 49 | 50 | ``` 51 | $ ./bin/serverssjs 52 | ``` 53 | 54 | __config.json__ 55 | 56 | ```json 57 | { 58 | "serverAddr": "127.0.0.1", 59 | "serverPort": 8083, 60 | "localAddr": "127.0.0.1", 61 | "localPort": 1080, 62 | "password": "YOUR_PASSWORD_HERE", 63 | "timeout": 600, 64 | "method": "aes-128-cfb" 65 | } 66 | ``` 67 | 68 | ### Result 69 | 70 | #### Example 1 71 | 72 | __benchmark config.json__ 73 | 74 | ```js 75 | module.exports = { 76 | limitConnections: 100, // concurrent 77 | totalConnection: 4000, 78 | timeout: 2000, // milliseconds 79 | baseLine: false, 80 | fileSize: 500, // kb 81 | }; 82 | ``` 83 | 84 | __shadowsocks:__ 85 | 86 | ``` 87 | Total: 4000 88 | errorRates = 0% 89 | averageTime = 779.9545612325014ms 90 | timeout = 0 91 | unexpected = 0 92 | ``` 93 | 94 | __shadowsocks-js:__ 95 | 96 | ``` 97 | Total: 4000 98 | errorRates = 0% 99 | averageTime = 386.7885057545ms 100 | timeout = 0 101 | unexpected = 0 102 | ``` 103 | 104 | #### Example 2 105 | 106 | __benchmark config.json__ 107 | 108 | ```js 109 | module.exports = { 110 | limitConnections: 500, // concurrent 111 | totalConnection: 4000, 112 | timeout: 2000, // milliseconds 113 | baseLine: false, 114 | fileSize: 50, // kb 115 | }; 116 | ``` 117 | 118 | __shadowsocks:__ 119 | 120 | ``` 121 | Total: 4000 122 | errorRates = 38.95% 123 | averageTime = 762.3954610835386ms 124 | timeout = 455 125 | unexpected = 1103 126 | ``` 127 | 128 | __shadowsocks-js:__ 129 | 130 | ``` 131 | Total: 4000 132 | errorRates = 0% 133 | averageTime = 644.7608903972514ms 134 | timeout = 0 135 | unexpected = 0 136 | ``` 137 | 138 | Memory usage in this test (every single seconds): 139 | 140 | ```js 141 | [ { rss: 23248896, heapTotal: 11530240, heapUsed: 5732288 }, 142 | { rss: 51552256, heapTotal: 18870272, heapUsed: 10518800 }, 143 | { rss: 115429376, heapTotal: 30404608, heapUsed: 15605560 }, 144 | { rss: 215781376, heapTotal: 27258880, heapUsed: 10931832 }, 145 | { rss: 208715776, heapTotal: 27258880, heapUsed: 7603168 }, 146 | { rss: 220286976, heapTotal: 28307456, heapUsed: 6845672 }, 147 | { rss: 227180544, heapTotal: 29356032, heapUsed: 10088048 }, 148 | { rss: 227180544, heapTotal: 29356032, heapUsed: 10092864 }, 149 | { rss: 227184640, heapTotal: 29356032, heapUsed: 10096744 } ] 150 | ``` 151 | 152 | #### Example 3 153 | 154 | __benchmark config.json__ 155 | 156 | ```js 157 | module.exports = { 158 | limitConnections: 1000, // concurrent 159 | totalConnection: 4000, 160 | timeout: 2000, // milliseconds 161 | baseLine: false, 162 | fileSize: 5, // kb 163 | }; 164 | ``` 165 | 166 | __shadowsocks:__ 167 | 168 | ``` 169 | Total: 4000 170 | errorRates = 23.825% 171 | averageTime = 1216.345400121761ms 172 | timeout = 108 173 | unexpected = 845 174 | ``` 175 | 176 | __shadowsocks-js:__ 177 | 178 | ``` 179 | Total: 4000 180 | errorRates = 0.4% 181 | averageTime = 1032.9700106164662ms 182 | timeout = 0 183 | unexpected = 16 184 | ``` 185 | 186 | Memory usage in this test (every single seconds): 187 | 188 | ```js 189 | [ { rss: 23453696, heapTotal: 11530240, heapUsed: 5962616 }, 190 | { rss: 23490560, heapTotal: 11530240, heapUsed: 5983016 }, 191 | { rss: 23490560, heapTotal: 11530240, heapUsed: 5985952 }, 192 | { rss: 23490560, heapTotal: 11530240, heapUsed: 5987160 }, 193 | { rss: 23490560, heapTotal: 11530240, heapUsed: 5988368 }, 194 | { rss: 23490560, heapTotal: 11530240, heapUsed: 5989576 }, 195 | { rss: 23490560, heapTotal: 11530240, heapUsed: 5990784 }, 196 | { rss: 23490560, heapTotal: 11530240, heapUsed: 5991992 }, 197 | { rss: 23490560, heapTotal: 11530240, heapUsed: 5993200 }, 198 | { rss: 23490560, heapTotal: 11530240, heapUsed: 5994408 }, 199 | { rss: 23490560, heapTotal: 11530240, heapUsed: 5995616 }, 200 | { rss: 23490560, heapTotal: 11530240, heapUsed: 5996824 }, 201 | { rss: 23490560, heapTotal: 11530240, heapUsed: 5998032 }, 202 | { rss: 23490560, heapTotal: 11530240, heapUsed: 5999240 }, 203 | { rss: 23490560, heapTotal: 11530240, heapUsed: 6000448 }, 204 | { rss: 23490560, heapTotal: 11530240, heapUsed: 6001656 } ] 205 | ``` 206 | -------------------------------------------------------------------------------- /benchmark/conclude.js: -------------------------------------------------------------------------------- 1 | const ERROR_TYPES = { 2 | UNEXPECTED: 'unexpected error', 3 | TIMEOUT: 'timeout', 4 | }; 5 | 6 | function getData(data) { 7 | let validCount = 0; 8 | let averageTime = 0; 9 | let errorRates = 0; 10 | 11 | let timeout = 0; 12 | let unexpected = 0; 13 | 14 | data.forEach((item) => { 15 | if (item.err) { 16 | errorRates += 1; 17 | if (item.err === ERROR_TYPES.TIMEOUT) { 18 | timeout += 1; 19 | } else { 20 | unexpected += 1; 21 | } 22 | } else { 23 | validCount += 1; 24 | averageTime += item.time; 25 | } 26 | }); 27 | 28 | errorRates = `${errorRates / data.length * 100}%`; 29 | averageTime /= validCount; 30 | averageTime = `${averageTime}ms`; 31 | 32 | return { 33 | errorRates, averageTime, timeout, 34 | unexpected, 35 | }; 36 | } 37 | 38 | function conclude(_data) { 39 | const data = getData(_data); 40 | 41 | const res = `Total: ${_data.length}\n`; 42 | 43 | return res + Object.keys(data).map(key => { 44 | const value = data[key]; 45 | 46 | return `${key} = ${value}\n`; 47 | }).join(''); 48 | } 49 | 50 | module.exports = { 51 | ERROR_TYPES, 52 | conclude, 53 | }; 54 | -------------------------------------------------------------------------------- /benchmark/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | limitConnections: 1000, // concurrent 3 | totalConnection: 4000, 4 | timeout: 2000, // milliseconds 5 | baseLine: false, 6 | fileSize: 5, // kb 7 | 8 | _resourceAddr: '30.10.112.18', 9 | // _resourceAddr: '127.0.0.1', 10 | }; 11 | -------------------------------------------------------------------------------- /benchmark/createClients.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const { parse } = require('url'); 3 | const { timesLimit } = require('async'); 4 | const { PORT, RESPONSE } = require('./createResourceServer'); 5 | const { request } = require('socks5-http-client'); 6 | const { baseLine, limitConnections, timeout, _resourceAddr } = require('./config'); 7 | const { ERROR_TYPES } = require('./conclude'); 8 | 9 | const SOCKS_PORT = 1080; 10 | 11 | const HOST = _resourceAddr || '127.0.0.1'; 12 | const URL = `http://${HOST}:${PORT}`; 13 | 14 | function getStartTimePoint() { 15 | return process.hrtime(); 16 | } 17 | 18 | function composeData(time) { 19 | // millisecond 20 | return process.hrtime(time)[0] * 1e3 + process.hrtime(time)[1] / 1000000; 21 | } 22 | 23 | function handler(time, onEnd, incomingMsg) { 24 | let body = null; 25 | 26 | incomingMsg.on('data', data => { 27 | body = !body ? data : Buffer.concat([body, data]); 28 | }); 29 | 30 | incomingMsg.on('end', () => { 31 | let err = null; 32 | 33 | // assume it's successful if they have the 34 | // same length 35 | if (body.length !== RESPONSE.length) { 36 | err = ERROR_TYPES.UNEXPECTED; 37 | } 38 | 39 | onEnd(null, { 40 | err, 41 | time: composeData(time), 42 | }); 43 | }); 44 | 45 | incomingMsg.on('error', err => { 46 | onEnd(null, { 47 | err, 48 | time: null, 49 | }); 50 | }); 51 | } 52 | 53 | function _send(options, index, next) { 54 | let req = null; 55 | let result = null; 56 | const time = getStartTimePoint(); 57 | 58 | const _handler = handler.bind(null, time, (_err, _result) => { 59 | result = _result; 60 | }); 61 | 62 | if (baseLine) { 63 | req = http.request(options, _handler); 64 | } else { 65 | req = request(options, _handler); 66 | } 67 | 68 | req.setTimeout(timeout, () => { 69 | result = { 70 | err: ERROR_TYPES.TIMEOUT, 71 | }; 72 | req.destroy(); 73 | }); 74 | 75 | req.on('error', () => { 76 | result = { 77 | err: ERROR_TYPES.UNEXPECTED, 78 | }; 79 | }); 80 | 81 | req.on('close', () => { 82 | next(null, result || { 83 | err: ERROR_TYPES.UNEXPECTED, 84 | }); 85 | }); 86 | 87 | req.end(); 88 | 89 | return req; 90 | } 91 | 92 | function send(t, next) { 93 | const options = Object.assign({}, parse(URL), { 94 | socksPort: SOCKS_PORT, 95 | }); 96 | 97 | // NOTE: 98 | if (~options.host.indexOf(':')) { 99 | options.host = options.host.slice(0, options.host.indexOf(':')); 100 | } 101 | 102 | const _sendToTarget = _send.bind(null, options); 103 | 104 | // TODO: findout why we have to limit 105 | timesLimit(t, limitConnections, _sendToTarget, next); 106 | } 107 | 108 | module.exports = { 109 | send, SOCKS_PORT, 110 | }; 111 | -------------------------------------------------------------------------------- /benchmark/createResourceServer.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const { fileSize } = require('./config'); 3 | 4 | const PORT = 8808; 5 | const RESPONSE = Buffer.alloc(fileSize * 1024, '0'); 6 | 7 | function createServer(next) { 8 | let total = 0; 9 | 10 | const logInterval = setInterval(() => { 11 | // eslint-disable-next-line 12 | console.log(`receive total: ${total}`); 13 | }, 1000); 14 | 15 | const server = http.createServer((req, res) => { 16 | res.end(RESPONSE); 17 | }); 18 | 19 | // console.log('2'); 20 | 21 | server.on('connection', () => { 22 | total += 1; 23 | }); 24 | 25 | server.listen(PORT, next); 26 | 27 | server.on('close', () => { 28 | clearInterval(logInterval); 29 | }); 30 | 31 | console.log(`listen on ${PORT}`); // eslint-disable-line 32 | 33 | return server; 34 | } 35 | 36 | module.exports = { 37 | createServer, PORT, 38 | RESPONSE, 39 | }; 40 | 41 | if (module === require.main) { 42 | createServer(); 43 | } 44 | -------------------------------------------------------------------------------- /benchmark/run.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require('./createResourceServer'); 2 | const { send, SOCKS_PORT } = require('./createClients'); 3 | const { totalConnection } = require('./config'); 4 | const { conclude } = require('./conclude'); 5 | 6 | function log() { 7 | console.log(...arguments); // eslint-disable-line 8 | } 9 | 10 | if (module === require.main) { 11 | log(`This benchmark assumes your ss client is listening on localhost:${SOCKS_PORT}`); 12 | 13 | const server = createServer(() => { 14 | send(totalConnection, (err, data) => { 15 | if (err) { 16 | throw err; 17 | } 18 | 19 | log(conclude(data)); 20 | 21 | server.close(); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /benchmark/utils.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyyd/encryptsocks/74532faa02f8ce95c9f802d58df1acb0c0bfd483/benchmark/utils.js -------------------------------------------------------------------------------- /bin/localssjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('babel-polyfill'); 4 | 5 | var cli = require('../lib/cli').default; 6 | var isServer = false; 7 | 8 | cli(isServer); 9 | -------------------------------------------------------------------------------- /bin/serverssjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('babel-polyfill'); 4 | 5 | var cli = require('../lib/cli').default; 6 | var isServer = true; 7 | 8 | cli(isServer); 9 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverAddr": "0.0.0.0", 3 | "serverPort": 8083, 4 | "localAddr": "127.0.0.1", 5 | "localPort": 1080, 6 | "password": "YOUR_PASSWORD_HERE", 7 | "timeout": 600, 8 | "method": "aes-128-cfb", 9 | 10 | "level": "warn", 11 | "localAddrIPv6": "::1", 12 | "serverAddrIPv6": "::1", 13 | "auth": { 14 | "forceAuth": false, 15 | "usernamePassword": { 16 | "name": "password" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | 5 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 6 | 7 | exports.createAuthInfo = createAuthInfo; 8 | exports.validate = validate; 9 | function createAuthInfo(config) { 10 | var auth = config.auth; 11 | 12 | var info = { 13 | forceAuth: false 14 | }; 15 | 16 | if (auth && auth.forceAuth) { 17 | info.forceAuth = true; 18 | } 19 | 20 | if (!info.forceAuth) { 21 | return { 22 | info: info 23 | }; 24 | } 25 | 26 | var usernamePassword = auth.usernamePassword; 27 | 28 | 29 | if (!usernamePassword || (typeof usernamePassword === 'undefined' ? 'undefined' : _typeof(usernamePassword)) !== 'object') { 30 | return { 31 | info: info, 32 | error: 'expect "usernamePassword" in your config file to be an object' 33 | }; 34 | } 35 | 36 | var keys = Object.keys(usernamePassword); 37 | 38 | if (keys.length === 0) { 39 | return { 40 | info: info, 41 | warn: 'no valid username/password found in your config file' 42 | }; 43 | } 44 | 45 | info.usernamePassword = usernamePassword; 46 | 47 | return { 48 | info: info 49 | }; 50 | } 51 | 52 | function validate(info, username, password) { 53 | return info.usernamePassword[username] === password; 54 | } -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.default = client; 5 | 6 | var _package = require('../package.json'); 7 | 8 | var _ssLocal = require('./ssLocal'); 9 | 10 | var ssLocal = _interopRequireWildcard(_ssLocal); 11 | 12 | var _ssServer = require('./ssServer'); 13 | 14 | var ssServer = _interopRequireWildcard(_ssServer); 15 | 16 | var _gfwlistUtils = require('./gfwlistUtils'); 17 | 18 | var _config = require('./config'); 19 | 20 | var _pm = require('./pm'); 21 | 22 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 23 | 24 | var log = console.log; // eslint-disable-line 25 | 26 | function getDaemonType(isServer) { 27 | return isServer ? 'server' : 'local'; 28 | } 29 | 30 | function logHelp(invalidOption) { 31 | log( 32 | // eslint-disable-next-line 33 | '\n' + (invalidOption ? invalidOption + '\n' : '') + 'encryptsocks ' + _package.version + '\nYou can supply configurations via either config file or command line arguments.\n\nProxy options:\n -c CONFIG_FILE Path to the config file.\n -s SERVER_ADDR Server address. default: 0.0.0.0\n -p SERVER_PORT Server port. default: 8083\n -l LOCAL_ADDR Local binding address. default: 127.0.0.1\n -b LOCAL_PORT Local port. default: 1080\n -k PASSWORD Password.\n -m METHOD Encryption method. default: aes-128-cfb\n -t TIMEOUT Timeout in seconds. default: 600\n --pac_port PAC_PORT PAC file server port. default: 8090\n --pac_update_gfwlist [URL] [localssjs] Update the gfwlist\n for PAC server. You can specify the\n request URL.\n --log_path LOG_PATH The directory path to log. Won\'t if not set.\n --level LOG_LEVEL Log level. default: warn\n example: --level verbose\nGeneral options:\n -h, --help Show this help message and exit.\n -d start/stop/restart Run as a daemon.\n'); 34 | } 35 | 36 | function updateGFWList(flag) { 37 | log('Updating gfwlist...'); 38 | 39 | var next = function next(err) { 40 | if (err) { 41 | throw err; 42 | } else { 43 | log('Updating finished. You can checkout the file here: ' + _gfwlistUtils.GFWLIST_FILE_PATH); 44 | } 45 | }; 46 | 47 | if (typeof flag === 'string') { 48 | (0, _gfwlistUtils.updateGFWList)(flag, next); 49 | } else { 50 | (0, _gfwlistUtils.updateGFWList)(next); 51 | } 52 | } 53 | 54 | function runDaemon(isServer, cmd) { 55 | var type = getDaemonType(isServer); 56 | 57 | switch (cmd) { 58 | case _config.DAEMON_COMMAND.start: 59 | { 60 | (0, _pm.start)(type); 61 | return; 62 | } 63 | case _config.DAEMON_COMMAND.stop: 64 | { 65 | (0, _pm.stop)(type); 66 | return; 67 | } 68 | case _config.DAEMON_COMMAND.restart: 69 | { 70 | (0, _pm.restart)(type); 71 | break; 72 | } 73 | default: 74 | } 75 | } 76 | 77 | function runSingle(isServer, proxyOptions) { 78 | var willLogToConsole = true; 79 | return isServer ? ssServer.startServer(proxyOptions, willLogToConsole) : ssLocal.startServer(proxyOptions, willLogToConsole); 80 | } 81 | 82 | function client(isServer) { 83 | var argv = process.argv.slice(2); 84 | 85 | (0, _config.getConfig)(argv, function (err, config) { 86 | if (err) { 87 | throw err; 88 | } 89 | 90 | var generalOptions = config.generalOptions, 91 | proxyOptions = config.proxyOptions, 92 | invalidOption = config.invalidOption; 93 | 94 | 95 | if (generalOptions.help || invalidOption) { 96 | logHelp(invalidOption); 97 | } else if (generalOptions.pacUpdateGFWList) { 98 | updateGFWList(generalOptions.pacUpdateGFWList); 99 | } else if (generalOptions.daemon) { 100 | runDaemon(isServer, generalOptions.daemon); 101 | } else { 102 | runSingle(isServer, proxyOptions); 103 | } 104 | }); 105 | } -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.DAEMON_COMMAND = undefined; 5 | 6 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 7 | 8 | exports.stringifyProxyOptions = stringifyProxyOptions; 9 | exports.resolveServerAddr = resolveServerAddr; 10 | exports.getDefaultProxyOptions = getDefaultProxyOptions; 11 | exports.getConfig = getConfig; 12 | 13 | var _path = require('path'); 14 | 15 | var _path2 = _interopRequireDefault(_path); 16 | 17 | var _ip = require('ip'); 18 | 19 | var _dns = require('dns'); 20 | 21 | var _minimist = require('minimist'); 22 | 23 | var _minimist2 = _interopRequireDefault(_minimist); 24 | 25 | var _fs = require('fs'); 26 | 27 | var _defaultConfig = require('./defaultConfig'); 28 | 29 | var _defaultConfig2 = _interopRequireDefault(_defaultConfig); 30 | 31 | var _config = require('../config.json'); 32 | 33 | var _config2 = _interopRequireDefault(_config); 34 | 35 | var _utils = require('./utils'); 36 | 37 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 38 | 39 | var DAEMON_COMMAND = exports.DAEMON_COMMAND = { 40 | start: 'start', 41 | stop: 'stop', 42 | restart: 'restart' 43 | }; 44 | 45 | var PROXY_ARGUMENT_PAIR = { 46 | c: 'configFilePath', 47 | s: 'serverAddr', 48 | p: 'serverPort', 49 | pac_port: 'pacServerPort', 50 | l: 'localAddr', 51 | b: 'localPort', 52 | k: 'password', 53 | m: 'method', 54 | t: 'timeout', 55 | level: 'level', 56 | log_path: 'logPath', 57 | // private 58 | mem: '_recordMemoryUsage' 59 | }; 60 | 61 | var PROXY_ARGUMENT_EXTRAL_KEYS = ['localAddrIPv6', 'serverAddrIPv6', '_recordMemoryUsage']; 62 | 63 | var GENERAL_ARGUMENT_PAIR = { 64 | h: 'help', 65 | help: 'help', 66 | d: 'daemon', 67 | pac_update_gfwlist: 'pacUpdateGFWList' 68 | }; 69 | 70 | function getProxyOptionArgName(optionName) { 71 | // ignore these keys 72 | if (PROXY_ARGUMENT_EXTRAL_KEYS.indexOf(optionName) >= 0) { 73 | return null; 74 | } 75 | 76 | var result = Object.keys(PROXY_ARGUMENT_PAIR).find(function (item) { 77 | return PROXY_ARGUMENT_PAIR[item] === optionName; 78 | }); 79 | 80 | if (!result) { 81 | throw new Error('invalid optionName: "' + optionName + '"'); 82 | } 83 | 84 | return result; 85 | } 86 | 87 | function stringifyProxyOptions(proxyOptions) { 88 | if ((typeof proxyOptions === 'undefined' ? 'undefined' : _typeof(proxyOptions)) !== 'object') { 89 | throw new Error('invalid type of "proxyOptions"'); 90 | } 91 | 92 | var args = []; 93 | 94 | Object.keys(proxyOptions).forEach(function (optionName) { 95 | var value = proxyOptions[optionName]; 96 | var argName = getProxyOptionArgName(optionName); 97 | 98 | if (!argName) { 99 | return; 100 | } 101 | 102 | args.push((0, _utils.getPrefixedArgName)(argName), value); 103 | }); 104 | 105 | return args.join(' '); 106 | } 107 | 108 | function getArgvOptions(argv) { 109 | var generalOptions = {}; 110 | var proxyOptions = {}; 111 | var configPair = (0, _minimist2.default)(argv); 112 | var optionsType = [{ 113 | options: proxyOptions, 114 | keys: Object.keys(PROXY_ARGUMENT_PAIR), 115 | values: PROXY_ARGUMENT_PAIR 116 | }, { 117 | options: generalOptions, 118 | keys: Object.keys(GENERAL_ARGUMENT_PAIR), 119 | values: GENERAL_ARGUMENT_PAIR 120 | }]; 121 | 122 | var invalidOption = null; 123 | 124 | Object.keys(configPair).forEach(function (key) { 125 | if (key === '_') { 126 | return; 127 | } 128 | 129 | var hit = false; 130 | 131 | optionsType.forEach(function (optType) { 132 | var i = optType.keys.indexOf(key); 133 | 134 | if (i >= 0) { 135 | optType.options[optType.values[optType.keys[i]]] = configPair[key]; // eslint-disable-line 136 | hit = true; 137 | } 138 | }); 139 | 140 | if (!hit) { 141 | invalidOption = key; 142 | } 143 | }); 144 | 145 | if (invalidOption) { 146 | invalidOption = invalidOption.length === 1 ? '-' + invalidOption : '--' + invalidOption; 147 | } else if (generalOptions.daemon && Object.keys(DAEMON_COMMAND).indexOf(generalOptions.daemon) < 0) { 148 | invalidOption = 'invalid daemon command: ' + generalOptions.daemon; 149 | } 150 | 151 | if (proxyOptions.logPath && !_path2.default.isAbsolute(proxyOptions.logPath)) { 152 | proxyOptions.logPath = _path2.default.resolve(process.cwd(), proxyOptions.logPath); 153 | } 154 | 155 | return { 156 | generalOptions: generalOptions, proxyOptions: proxyOptions, invalidOption: invalidOption 157 | }; 158 | } 159 | 160 | function readConfig(_filePath) { 161 | if (!_filePath) { 162 | return null; 163 | } 164 | 165 | var filePath = _path2.default.resolve(process.cwd(), _filePath); 166 | 167 | try { 168 | (0, _fs.accessSync)(filePath); 169 | } catch (e) { 170 | throw new Error('failed to find config file in: ' + filePath); 171 | } 172 | 173 | return JSON.parse((0, _fs.readFileSync)(filePath)); 174 | } 175 | 176 | /** 177 | * Transform domain && ipv6 to ipv4. 178 | */ 179 | function resolveServerAddr(config, next) { 180 | var serverAddr = config.proxyOptions.serverAddr; 181 | 182 | 183 | if ((0, _ip.isV4Format)(serverAddr)) { 184 | next(null, config); 185 | } else { 186 | (0, _dns.lookup)(serverAddr, function (err, addresses) { 187 | if (err) { 188 | next(new Error('failed to resolve \'serverAddr\': ' + serverAddr), config); 189 | } else { 190 | // NOTE: mutate data 191 | config.proxyOptions.serverAddr = addresses; // eslint-disable-line 192 | next(null, config); 193 | } 194 | }); 195 | } 196 | } 197 | 198 | function getDefaultProxyOptions() { 199 | return Object.assign({}, _defaultConfig2.default); 200 | } 201 | 202 | function getConfig() { 203 | var argv = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 204 | var arg1 = arguments[1]; 205 | var arg2 = arguments[2]; 206 | 207 | var doNotResolveIpv6 = arg1; 208 | var next = arg2; 209 | 210 | if (!arg2) { 211 | doNotResolveIpv6 = false; 212 | next = arg1; 213 | } 214 | 215 | var _getArgvOptions = getArgvOptions(argv), 216 | generalOptions = _getArgvOptions.generalOptions, 217 | proxyOptions = _getArgvOptions.proxyOptions, 218 | invalidOption = _getArgvOptions.invalidOption; 219 | 220 | var specificFileConfig = readConfig(proxyOptions.configFilePath) || _config2.default; 221 | var config = { 222 | generalOptions: generalOptions, 223 | invalidOption: invalidOption, 224 | proxyOptions: Object.assign({}, _defaultConfig2.default, specificFileConfig, proxyOptions) 225 | }; 226 | 227 | if (doNotResolveIpv6) { 228 | next(null, config); 229 | return; 230 | } 231 | 232 | resolveServerAddr(config, next); 233 | } -------------------------------------------------------------------------------- /lib/createUDPRelay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.default = createUDPRelay; 5 | 6 | var _dgram = require('dgram'); 7 | 8 | var _dgram2 = _interopRequireDefault(_dgram); 9 | 10 | var _lruCache = require('lru-cache'); 11 | 12 | var _lruCache2 = _interopRequireDefault(_lruCache); 13 | 14 | var _ip = require('ip'); 15 | 16 | var _ip2 = _interopRequireDefault(_ip); 17 | 18 | var _utils = require('./utils'); 19 | 20 | var _encryptor = require('./encryptor'); 21 | 22 | var encryptor = _interopRequireWildcard(_encryptor); 23 | 24 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 25 | 26 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 27 | 28 | // SOCKS5 UDP Request 29 | // +----+------+------+----------+----------+----------+ 30 | // |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | 31 | // +----+------+------+----------+----------+----------+ 32 | // | 2 | 1 | 1 | Variable | 2 | Variable | 33 | // +----+------+------+----------+----------+----------+ 34 | // 35 | // SOCKS5 UDP Response 36 | // +----+------+------+----------+----------+----------+ 37 | // |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | 38 | // +----+------+------+----------+----------+----------+ 39 | // | 2 | 1 | 1 | Variable | 2 | Variable | 40 | // +----+------+------+----------+----------+----------+ 41 | // 42 | // UDP Request (before encrypted) 43 | // +------+----------+----------+----------+ 44 | // | ATYP | DST.ADDR | DST.PORT | DATA | 45 | // +------+----------+----------+----------+ 46 | // | 1 | Variable | 2 | Variable | 47 | // +------+----------+----------+----------+ 48 | // 49 | // UDP Response (before encrypted) 50 | // +------+----------+----------+----------+ 51 | // | ATYP | DST.ADDR | DST.PORT | DATA | 52 | // +------+----------+----------+----------+ 53 | // | 1 | Variable | 2 | Variable | 54 | // +------+----------+----------+----------+ 55 | // 56 | // UDP Request and Response (after encrypted) 57 | // +-------+--------------+ 58 | // | IV | PAYLOAD | 59 | // +-------+--------------+ 60 | // | Fixed | Variable | 61 | // +-------+--------------+ 62 | 63 | var NAME = 'udp_relay'; 64 | var LRU_OPTIONS = { 65 | max: 1000, 66 | maxAge: 10 * 1000, 67 | dispose: function dispose(key, socket) { 68 | // close socket if it's not closed 69 | if (socket) { 70 | socket.close(); 71 | } 72 | } 73 | }; 74 | var SOCKET_TYPE = ['udp4', 'udp6']; 75 | 76 | function getIndex(_ref, _ref2) { 77 | var address = _ref.address, 78 | port = _ref.port; 79 | var dstAddrStr = _ref2.dstAddrStr, 80 | dstPortNum = _ref2.dstPortNum; 81 | 82 | return address + ':' + port + '_' + dstAddrStr + ':' + dstPortNum; 83 | } 84 | 85 | function createClient(logger, _ref3, onMsg, onClose) { 86 | var atyp = _ref3.atyp; 87 | 88 | var udpType = atyp === 1 ? 'udp4' : 'udp6'; 89 | var socket = _dgram2.default.createSocket(udpType); 90 | 91 | socket.on('message', onMsg); 92 | 93 | socket.on('error', function (e) { 94 | logger.warn(NAME + ' client socket gets error: ' + e.message); 95 | }); 96 | 97 | socket.on('close', onClose); 98 | 99 | return socket; 100 | } 101 | 102 | function createUDPRelaySocket(udpType, config, isServer, logger) { 103 | var localPort = config.localPort, 104 | serverPort = config.serverPort, 105 | password = config.password, 106 | method = config.method; 107 | 108 | var serverAddr = udpType === 'udp4' ? config.serverAddr : config.serverAddrIPv6; 109 | 110 | var encrypt = encryptor.encrypt.bind(null, password, method); 111 | var decrypt = encryptor.decrypt.bind(null, password, method); 112 | var socket = _dgram2.default.createSocket(udpType); 113 | var cache = new _lruCache2.default(Object.assign({}, LRU_OPTIONS, { 114 | maxAge: config.timeout * 1000 115 | })); 116 | var listenPort = isServer ? serverPort : localPort; 117 | 118 | socket.on('message', function (_msg, rinfo) { 119 | var msg = isServer ? decrypt(_msg) : _msg; 120 | var frag = msg[2]; 121 | 122 | if (frag !== 0) { 123 | // drop those datagram that using frag 124 | return; 125 | } 126 | 127 | var dstInfo = (0, _utils.getDstInfoFromUDPMsg)(msg, isServer); 128 | var dstAddrStr = _ip2.default.toString(dstInfo.dstAddr); 129 | var dstPortNum = dstInfo.dstPort.readUInt16BE(); 130 | var index = getIndex(rinfo, { dstAddrStr: dstAddrStr, dstPortNum: dstPortNum }); 131 | 132 | logger.debug(NAME + ' receive message: ' + msg.toString('hex')); 133 | 134 | var client = cache.get(index); 135 | 136 | if (!client) { 137 | client = createClient(logger, dstInfo, function (msgStream) { 138 | // socket on message 139 | var incomeMsg = isServer ? encrypt(msgStream) : decrypt(msgStream); 140 | (0, _utils.sendDgram)(socket, incomeMsg, rinfo.port, rinfo.address); 141 | }, function () { 142 | // socket on close 143 | cache.del(index); 144 | }); 145 | cache.set(index, client); 146 | } 147 | 148 | if (isServer) { 149 | (0, _utils.sendDgram)(client, msg.slice(dstInfo.totalLength), dstPortNum, dstAddrStr); 150 | } else { 151 | (0, _utils.sendDgram)(client, 152 | // skip RSV and FLAG 153 | encrypt(msg.slice(3)), serverPort, serverAddr); 154 | } 155 | }); 156 | 157 | socket.on('error', function (err) { 158 | logger.error(NAME + ' socket err: ' + err.message); 159 | socket.close(); 160 | }); 161 | 162 | socket.on('close', function () { 163 | cache.reset(); 164 | }); 165 | 166 | socket.bind(listenPort, function () { 167 | logger.verbose(NAME + ' is listening on: ' + listenPort); 168 | }); 169 | 170 | return socket; 171 | } 172 | 173 | function close(sockets) { 174 | sockets.forEach(function (socket) { 175 | (0, _utils.closeSilently)(socket); 176 | }); 177 | } 178 | 179 | function createUDPRelay(config, isServer, logger) { 180 | var sockets = SOCKET_TYPE.map(function (udpType) { 181 | return createUDPRelaySocket(udpType, config, isServer, logger); 182 | }); 183 | 184 | return { 185 | sockets: sockets, 186 | close: close.bind(null, sockets) 187 | }; 188 | } -------------------------------------------------------------------------------- /lib/daemon.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.FORK_FILE_PATH = undefined; 5 | 6 | var _path = require('path'); 7 | 8 | var _child_process = require('child_process'); 9 | 10 | var _logger = require('./logger'); 11 | 12 | var _cli = require('./cli'); 13 | 14 | var _pid = require('./pid'); 15 | 16 | var _recordMemoryUsage = require('./recordMemoryUsage'); 17 | 18 | var _utils = require('./utils'); 19 | 20 | var NAME = 'daemon'; 21 | // TODO: 22 | // const MAX_RESTART_TIME = 5; 23 | /** 24 | * ``` 25 | * $ node lib/daemon local -d restart 26 | * ``` 27 | * 28 | * ``` 29 | * $ node lib/daemon server -d restart -k abc 30 | * ``` 31 | */ 32 | var MAX_RESTART_TIME = 1; 33 | 34 | var child = null; 35 | var logger = void 0; 36 | var shouldStop = false; 37 | 38 | // eslint-disable-next-line 39 | var FORK_FILE_PATH = exports.FORK_FILE_PATH = { 40 | local: (0, _path.join)(__dirname, 'ssLocal'), 41 | server: (0, _path.join)(__dirname, 'ssServer') 42 | }; 43 | 44 | function daemon(type, config, filePath, shouldRecordServerMemory, _restartTime) { 45 | var restartTime = _restartTime || 0; 46 | 47 | child = (0, _child_process.fork)(filePath); 48 | 49 | if (shouldRecordServerMemory) { 50 | child.on('message', _recordMemoryUsage.record); 51 | } 52 | 53 | child.send(config); 54 | 55 | setTimeout(function () { 56 | restartTime = 0; 57 | }, 60 * 1000); 58 | 59 | child.on('exit', function () { 60 | if (shouldStop) { 61 | return; 62 | } 63 | 64 | logger.warn(NAME + ': process exit.'); 65 | 66 | (0, _utils.safelyKillChild)(child, 'SIGKILL'); 67 | 68 | if (restartTime < MAX_RESTART_TIME) { 69 | daemon(type, config, filePath, shouldRecordServerMemory, restartTime + 1); 70 | } else { 71 | logger.error(NAME + ': restarted too many times, will close.', (0, _utils.createSafeAfterHandler)(logger, function () { 72 | (0, _pid.deletePidFile)(type); 73 | process.exit(1); 74 | })); 75 | } 76 | }); 77 | } 78 | 79 | if (module === require.main) { 80 | var type = process.argv[2]; 81 | var argv = process.argv.slice(3); 82 | 83 | (0, _cli.getConfig)(argv, function (err, config) { 84 | var proxyOptions = config.proxyOptions; 85 | // eslint-disable-next-line 86 | 87 | var shouldRecordServerMemory = proxyOptions['_recordMemoryUsage'] && type === 'server'; 88 | 89 | logger = (0, _logger.createLogger)(proxyOptions.level, null, 90 | // LOG_NAMES.DAEMON, 91 | false); 92 | 93 | if (err) { 94 | logger.error(NAME + ': ' + err.message); 95 | } 96 | 97 | daemon(type, proxyOptions, FORK_FILE_PATH[type], shouldRecordServerMemory); 98 | 99 | process.on('SIGHUP', function () { 100 | shouldStop = true; 101 | 102 | if (child) { 103 | (0, _utils.safelyKillChild)(child, 'SIGKILL'); 104 | } 105 | 106 | (0, _pid.deletePidFile)(type); 107 | 108 | if (shouldRecordServerMemory) { 109 | (0, _recordMemoryUsage.stopRecord)(); 110 | } 111 | 112 | process.exit(0); 113 | }); 114 | 115 | process.on('uncaughtException', function (uncaughtErr) { 116 | logger.error(NAME + ' get error:\n' + uncaughtErr.stack, (0, _utils.createSafeAfterHandler)(logger, function () { 117 | process.exit(1); 118 | })); 119 | }); 120 | }); 121 | } -------------------------------------------------------------------------------- /lib/defaultConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | 5 | var _path = require('path'); 6 | 7 | var _path2 = _interopRequireDefault(_path); 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 10 | 11 | // proxy options 12 | var DEFAULT_CONFIG = { 13 | serverAddr: '0.0.0.0', 14 | serverPort: 8083, 15 | localAddr: '127.0.0.1', 16 | localPort: 1080, 17 | password: 'YOUR_PASSWORD_HERE', 18 | pacServerPort: 8090, 19 | timeout: 600, 20 | method: 'aes-128-cfb', 21 | level: 'warn', 22 | logPath: _path2.default.resolve(__dirname, '../logs'), 23 | 24 | // ipv6 25 | localAddrIPv6: '::1', 26 | serverAddrIPv6: '::1', 27 | 28 | // dev options 29 | _recordMemoryUsage: false 30 | }; 31 | 32 | exports.default = DEFAULT_CONFIG; -------------------------------------------------------------------------------- /lib/encryptor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.getParamLength = getParamLength; 5 | exports.generateKey = generateKey; 6 | exports.createCipher = createCipher; 7 | exports.createDecipher = createDecipher; 8 | exports.encrypt = encrypt; 9 | exports.decrypt = decrypt; 10 | 11 | var _crypto = require('crypto'); 12 | 13 | var _crypto2 = _interopRequireDefault(_crypto); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | var hasOwnProperty = {}.hasOwnProperty; 18 | 19 | // directly exported from the original nodejs implementation 20 | var cryptoParamLength = { 21 | 'aes-128-cfb': [16, 16], 22 | 'aes-192-cfb': [24, 16], 23 | 'aes-256-cfb': [32, 16], 24 | 'bf-cfb': [16, 8], 25 | 'camellia-128-cfb': [16, 16], 26 | 'camellia-192-cfb': [24, 16], 27 | 'camellia-256-cfb': [32, 16], 28 | 'cast5-cfb': [16, 8], 29 | 'des-cfb': [8, 8], 30 | 'idea-cfb': [16, 8], 31 | 'rc2-cfb': [16, 8], 32 | rc4: [16, 0], 33 | 'rc4-md5': [16, 16], 34 | 'seed-cfb': [16, 16] 35 | }; 36 | 37 | var keyCache = {}; 38 | 39 | function getParamLength(methodName) { 40 | return cryptoParamLength[methodName]; 41 | } 42 | 43 | function getMD5Hash(data) { 44 | return _crypto2.default.createHash('md5').update(data).digest(); 45 | } 46 | 47 | function generateKey(methodName, secret) { 48 | var secretBuf = new Buffer(secret, 'utf8'); 49 | var tokens = []; 50 | var keyLength = getParamLength(methodName)[0]; 51 | var cacheIndex = methodName + '_' + secret; 52 | 53 | var i = 0; 54 | var hash = void 0; 55 | var length = 0; 56 | 57 | if (hasOwnProperty.call(keyCache, cacheIndex)) { 58 | return keyCache[cacheIndex]; 59 | } 60 | 61 | if (!keyLength) { 62 | // TODO: catch error 63 | throw new Error('unsupported method'); 64 | } 65 | 66 | while (length < keyLength) { 67 | hash = getMD5Hash(i === 0 ? secretBuf : Buffer.concat([tokens[i - 1], secretBuf])); 68 | tokens.push(hash); 69 | i += 1; 70 | length += hash.length; 71 | } 72 | 73 | hash = Buffer.concat(tokens).slice(0, keyLength); 74 | 75 | keyCache[cacheIndex] = hash; 76 | 77 | return hash; 78 | } 79 | 80 | function createCipher(secret, methodName, initialData, _iv) { 81 | var key = generateKey(methodName, secret); 82 | var iv = _iv || _crypto2.default.randomBytes(getParamLength(methodName)[1]); 83 | var cipher = _crypto2.default.createCipheriv(methodName, key, iv); 84 | 85 | return { 86 | cipher: cipher, 87 | data: Buffer.concat([iv, cipher.update(initialData)]) 88 | }; 89 | } 90 | 91 | function createDecipher(secret, methodName, initialData) { 92 | var ivLength = getParamLength(methodName)[1]; 93 | var iv = initialData.slice(0, ivLength); 94 | 95 | if (iv.length !== ivLength) { 96 | return null; 97 | } 98 | 99 | var key = generateKey(methodName, secret); 100 | var decipher = _crypto2.default.createDecipheriv(methodName, key, iv); 101 | var data = decipher.update(initialData.slice(ivLength)); 102 | 103 | return { 104 | decipher: decipher, 105 | data: data 106 | }; 107 | } 108 | 109 | function encrypt(secret, methodName, data, _iv) { 110 | return createCipher(secret, methodName, data, _iv).data; 111 | } 112 | 113 | function decrypt(secret, methodName, data) { 114 | return createDecipher(secret, methodName, data).data; 115 | } -------------------------------------------------------------------------------- /lib/filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.setDenyList = setDenyList; 5 | exports.filter = filter; 6 | 7 | var _utils = require('./utils'); 8 | 9 | // TODO: 10 | var defaultDenyList = [ 11 | // /google/, 12 | ]; 13 | var denyListLength = defaultDenyList.length; 14 | 15 | function setDenyList(denyList) { 16 | defaultDenyList = denyList; 17 | denyListLength = denyList.length; 18 | } 19 | 20 | function filter(dstInfo) { 21 | var dstStr = (0, _utils.getDstStr)(dstInfo); 22 | 23 | var i = void 0; 24 | 25 | for (i = 0; i < denyListLength; i += 1) { 26 | if (defaultDenyList[i].test(dstStr)) { 27 | return false; 28 | } 29 | } 30 | 31 | return true; 32 | } -------------------------------------------------------------------------------- /lib/gfwlistUtils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.GFWLIST_FILE_PATH = undefined; 5 | exports.readLine = readLine; 6 | exports.createListArrayString = createListArrayString; 7 | exports.createPACFileContent = createPACFileContent; 8 | exports.requestGFWList = requestGFWList; 9 | exports.getPACFileContent = getPACFileContent; 10 | exports.updateGFWList = updateGFWList; 11 | 12 | var _path = require('path'); 13 | 14 | var _https = require('https'); 15 | 16 | var _http = require('http'); 17 | 18 | var _url = require('url'); 19 | 20 | var _fs = require('fs'); 21 | 22 | var _uglifyJs = require('uglify-js'); 23 | 24 | // NOTE: do not use these in local server 25 | var GFWLIST_FILE_PATH = exports.GFWLIST_FILE_PATH = (0, _path.join)(__dirname, '../pac/gfwlist.txt'); 26 | var DEFAULT_CONFIG = { 27 | localAddr: '127.0.0.1', 28 | localPort: '1080' 29 | }; 30 | var TARGET_URL = 'https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt'; 31 | var LINE_DELIMER = ['\r\n', '\r', '\n']; 32 | var MINIFY_OPTIONS = { 33 | warnings: false 34 | }; 35 | 36 | var readLineLastContent = null; 37 | var readLineLastIndex = 0; 38 | 39 | function clear() { 40 | readLineLastContent = null; 41 | readLineLastIndex = 0; 42 | } 43 | 44 | function base64ToString(base64String) { 45 | return Buffer.from(base64String, 'base64').toString(); 46 | } 47 | 48 | // function stringToBase64(string) { 49 | // return (new Buffer(base64String, 'base64')).toString('base64'); 50 | // } 51 | 52 | function readLine(text, shouldStrip) { 53 | var startIndex = 0; 54 | var i = null; 55 | var delimer = null; 56 | 57 | if (text === readLineLastContent) { 58 | startIndex = readLineLastIndex; 59 | } else { 60 | readLineLastContent = text; 61 | } 62 | 63 | LINE_DELIMER.forEach(function (char) { 64 | var index = text.indexOf(char, startIndex); 65 | 66 | if (index !== -1 && (i === null || index < i)) { 67 | i = index; 68 | delimer = char; 69 | } 70 | }); 71 | 72 | if (i !== null) { 73 | readLineLastIndex = i + delimer.length; 74 | return shouldStrip ? text.slice(startIndex, i) : text.slice(startIndex, readLineLastIndex); 75 | } 76 | 77 | readLineLastIndex = 0; 78 | return null; 79 | } 80 | 81 | readLine.clear = clear; 82 | 83 | function shouldDropLine(line) { 84 | // NOTE: It's possible that gfwlist has rules that is a too long 85 | // regexp that may crush proxies like 'SwitchySharp' so we would 86 | // drop these rules here. 87 | return !line || line[0] === '!' || line[0] === '[' || line.length > 100; 88 | } 89 | 90 | var slashReg = /\//g; 91 | 92 | function encode(line) { 93 | return line.replace(slashReg, '\\/'); 94 | } 95 | 96 | function createListArrayString(text) { 97 | var list = []; 98 | var line = readLine(text, true); 99 | 100 | while (line !== null) { 101 | if (!shouldDropLine(line)) { 102 | list.push('"' + encode(line) + '"'); 103 | } 104 | 105 | line = readLine(text, true); 106 | } 107 | 108 | return 'var rules = [' + list.join(',\n') + '];'; 109 | } 110 | 111 | function createPACFileContent(text, _ref) { 112 | var localAddr = _ref.localAddr, 113 | localPort = _ref.localPort; 114 | 115 | var HOST = localAddr + ':' + localPort; 116 | var readFileOptions = { encoding: 'utf8' }; 117 | var userRulesString = (0, _fs.readFileSync)((0, _path.join)(__dirname, '../pac/user.txt'), readFileOptions); 118 | var rulesString = createListArrayString(userRulesString + '\n' + text); 119 | var SOCKS_STR = 'var proxy = "SOCKS5 ' + HOST + '; SOCKS ' + HOST + '; DIRECT;";'; 120 | var matcherString = (0, _fs.readFileSync)((0, _path.join)(__dirname, '../vendor/ADPMatcher.js'), readFileOptions); 121 | 122 | return SOCKS_STR + '\n' + rulesString + '\n' + matcherString; 123 | } 124 | 125 | function requestGFWList(targetURL, next) { 126 | var options = (0, _url.parse)(targetURL); 127 | var requestMethod = options.protocol.indexOf('https') >= 0 ? _https.request : _http.request; 128 | 129 | var req = requestMethod(options, function (res) { 130 | var data = null; 131 | 132 | res.on('data', function (chunk) { 133 | data = data ? Buffer.concat([data, chunk]) : chunk; 134 | }); 135 | 136 | res.on('end', function () { 137 | // gfwlist.txt use utf8 encoded content to present base64 content 138 | var listText = data.toString(); 139 | next(null, listText); 140 | }); 141 | }); 142 | 143 | req.on('error', function (err) { 144 | next(err); 145 | }); 146 | 147 | req.end(); 148 | } 149 | 150 | function minifyCode(code) { 151 | return (0, _uglifyJs.minify)(code, MINIFY_OPTIONS).code; 152 | } 153 | 154 | // TODO: async this 155 | function getPACFileContent(_config) { 156 | var config = _config || DEFAULT_CONFIG; 157 | var listText = base64ToString((0, _fs.readFileSync)(GFWLIST_FILE_PATH, { encoding: 'utf8' })); 158 | 159 | return minifyCode(createPACFileContent(listText, config)); 160 | } 161 | 162 | function writeGFWList(listBuffer, next) { 163 | (0, _fs.writeFile)(GFWLIST_FILE_PATH, listBuffer, next); 164 | } 165 | 166 | function updateGFWList() { 167 | var targetURL = TARGET_URL; 168 | var next = void 0; 169 | 170 | if (arguments.length === 1) { 171 | next = arguments.length <= 0 ? undefined : arguments[0]; 172 | } else if (arguments.length === 2) { 173 | targetURL = arguments.length <= 0 ? undefined : arguments[0]; 174 | next = arguments.length <= 1 ? undefined : arguments[1]; 175 | } 176 | 177 | requestGFWList(targetURL, function (err, listBuffer) { 178 | if (err) { 179 | next(err); 180 | return; 181 | } 182 | 183 | writeGFWList(listBuffer, next); 184 | }); 185 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | 5 | var _ssServer = require('./ssServer'); 6 | 7 | Object.defineProperty(exports, 'createServer', { 8 | enumerable: true, 9 | get: function get() { 10 | return _ssServer.startServer; 11 | } 12 | }); 13 | 14 | var _ssLocal = require('./ssLocal'); 15 | 16 | Object.defineProperty(exports, 'createClient', { 17 | enumerable: true, 18 | get: function get() { 19 | return _ssLocal.startServer; 20 | } 21 | }); -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.LOG_NAMES = undefined; 5 | exports.createLogger = createLogger; 6 | 7 | var _winston = require('winston'); 8 | 9 | var _winston2 = _interopRequireDefault(_winston); 10 | 11 | var _path = require('path'); 12 | 13 | var _utils = require('./utils'); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | var LOG_NAMES = exports.LOG_NAMES = { 18 | LOCAL: 'local.log', 19 | SERVER: 'server.log', 20 | DAEMON: 'daemon.log' 21 | }; 22 | 23 | var DEFAULT_LEVEL = 'warn'; 24 | var DEFAULT_COMMON_OPTIONS = { 25 | colorize: true, 26 | timestamp: true 27 | }; 28 | 29 | // TODO: to be refactored 30 | function createLogData(level, filename, willLogToConsole, notLogToFile) { 31 | var transports = []; 32 | 33 | if (filename && !notLogToFile) { 34 | transports.push(new _winston2.default.transports.File(Object.assign(DEFAULT_COMMON_OPTIONS, { 35 | level: level, 36 | filename: filename 37 | }))); 38 | } 39 | 40 | if (willLogToConsole) { 41 | transports.push(new _winston2.default.transports.Console(Object.assign(DEFAULT_COMMON_OPTIONS, { 42 | level: level 43 | }))); 44 | } 45 | 46 | return { 47 | transports: transports 48 | }; 49 | } 50 | 51 | function createLogger(proxyOptions, logName) { 52 | var willLogToConsole = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; 53 | var notLogToFile = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; 54 | 55 | var _ref = proxyOptions || {}, 56 | _ref$level = _ref.level, 57 | level = _ref$level === undefined ? DEFAULT_LEVEL : _ref$level, 58 | logPath = _ref.logPath; 59 | 60 | if (logPath) { 61 | (0, _utils.mkdirIfNotExistSync)(logPath); 62 | } 63 | 64 | var fileName = logPath ? (0, _path.resolve)(logPath, logName) : null; 65 | return new _winston2.default.Logger(createLogData(level, fileName, willLogToConsole, notLogToFile)); 66 | } -------------------------------------------------------------------------------- /lib/pacServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.createPACServer = createPACServer; 5 | 6 | var _http = require('http'); 7 | 8 | var _gfwlistUtils = require('./gfwlistUtils'); 9 | 10 | var NAME = 'pac_server'; 11 | 12 | // TODO: async this 13 | // eslint-disable-next-line 14 | function createPACServer(config, logger) { 15 | var pacFileContent = (0, _gfwlistUtils.getPACFileContent)(config); 16 | var HOST = config.localAddr + ':' + config.pacServerPort; 17 | 18 | var server = (0, _http.createServer)(function (req, res) { 19 | res.write(pacFileContent); 20 | res.end(); 21 | }); 22 | 23 | server.on('error', function (err) { 24 | logger.error(NAME + ' got error: ' + err.stack); 25 | }); 26 | 27 | server.listen(config.pacServerPort); 28 | 29 | if (logger) { 30 | logger.verbose(NAME + ' is listening on ' + HOST); 31 | } 32 | 33 | return server; 34 | } -------------------------------------------------------------------------------- /lib/pid.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.TMP_PATH = undefined; 5 | exports.getFileName = getFileName; 6 | exports.getPid = getPid; 7 | exports.writePidFile = writePidFile; 8 | exports.deletePidFile = deletePidFile; 9 | 10 | var _fs = require('fs'); 11 | 12 | var _path = require('path'); 13 | 14 | var _utils = require('./utils'); 15 | 16 | var TMP_PATH = exports.TMP_PATH = (0, _path.join)(__dirname, '../tmp'); 17 | 18 | function getFileName(type) { 19 | switch (type) { 20 | case 'local': 21 | return (0, _path.join)(TMP_PATH, 'local.pid'); 22 | case 'server': 23 | return (0, _path.join)(TMP_PATH, 'server.pid'); 24 | default: 25 | throw new Error('invalid \'type\' of filename ' + type); 26 | } 27 | } 28 | 29 | function getPid(type) { 30 | var fileName = getFileName(type); 31 | 32 | (0, _utils.mkdirIfNotExistSync)(TMP_PATH); 33 | 34 | try { 35 | (0, _fs.accessSync)(fileName); 36 | } catch (e) { 37 | return null; 38 | } 39 | 40 | return (0, _fs.readFileSync)(fileName).toString('utf8'); 41 | } 42 | 43 | function writePidFile(type, pid) { 44 | (0, _utils.mkdirIfNotExistSync)(TMP_PATH); 45 | 46 | (0, _fs.writeFileSync)(getFileName(type), pid); 47 | } 48 | 49 | function deletePidFile(type) { 50 | (0, _utils.mkdirIfNotExistSync)(TMP_PATH); 51 | 52 | try { 53 | (0, _fs.unlinkSync)(getFileName(type)); 54 | } catch (err) { 55 | // alreay unlinked 56 | } 57 | } -------------------------------------------------------------------------------- /lib/pm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.FORK_FILE_PATH = undefined; 5 | 6 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; /** 7 | * Daemon ss processes. 8 | * 1. start, stop, restart 9 | * 2. know the previous process running status 10 | * 3. log and logrotate 11 | */ 12 | 13 | 14 | exports.start = start; 15 | exports.stop = stop; 16 | exports.restart = restart; 17 | 18 | var _pm = require('pm2'); 19 | 20 | var _pm2 = _interopRequireDefault(_pm); 21 | 22 | var _path = require('path'); 23 | 24 | var _path2 = _interopRequireDefault(_path); 25 | 26 | var _pid = require('./pid'); 27 | 28 | var _config2 = require('./config'); 29 | 30 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 31 | 32 | // eslint-disable-next-line 33 | var log = console.log; 34 | 35 | // const LOG_ROTATE_OPTIONS = { 36 | // maxSize: '1KB', 37 | // retain: 7, 38 | // workerInterval: 60, 39 | // rotateInterval: '*/1 * * * *', 40 | // }; 41 | 42 | var FORK_FILE_PATH = exports.FORK_FILE_PATH = { 43 | local: _path2.default.join(__dirname, 'ssLocal.js'), 44 | server: _path2.default.join(__dirname, 'ssServer.js') 45 | }; 46 | 47 | var pm2ProcessName = { 48 | local: 'ssLocal', 49 | server: 'ssServer' 50 | }; 51 | 52 | function getArgs(extralProxyOptions) { 53 | if (typeof extralProxyOptions === 'string') { 54 | return extralProxyOptions; 55 | } 56 | 57 | // TODO: support "stringify" 58 | if ((typeof extralProxyOptions === 'undefined' ? 'undefined' : _typeof(extralProxyOptions)) === 'object') { 59 | return (0, _config2.stringifyProxyOptions)(extralProxyOptions); 60 | } 61 | 62 | return process.argv.slice(2).join(' '); 63 | } 64 | 65 | function disconnect() { 66 | return new Promise(function (resolve) { 67 | _pm2.default.disconnect(function (err) { 68 | if (err) { 69 | throw err; 70 | } 71 | 72 | resolve(); 73 | }); 74 | }); 75 | } 76 | 77 | function connect() { 78 | return new Promise(function (resolve) { 79 | _pm2.default.connect(function (err) { 80 | if (err) { 81 | throw err; 82 | } 83 | 84 | resolve(); 85 | }); 86 | }); 87 | } 88 | 89 | function handleError(err) { 90 | return disconnect().then(function () { 91 | // TODO: 92 | // eslint-disable-next-line 93 | console.error(err); 94 | }); 95 | } 96 | 97 | function getPM2Config(type, extralProxyOptions) { 98 | return connect().then(function () { 99 | var filePath = FORK_FILE_PATH[type]; 100 | var pidFileName = (0, _pid.getFileName)(type); 101 | var name = pm2ProcessName[type]; 102 | 103 | var pm2Config = { 104 | name: name, 105 | script: filePath, 106 | exec_mode: 'fork', 107 | instances: 1, 108 | output: _path2.default.resolve(__dirname, '../logs/' + name + '.log'), 109 | error: _path2.default.resolve(__dirname, '../logs/' + name + '.err'), 110 | pid: pidFileName, 111 | minUptime: 2000, 112 | maxRestarts: 3, 113 | args: getArgs(extralProxyOptions) 114 | }; 115 | 116 | return { 117 | pm2Config: pm2Config 118 | }; 119 | }); 120 | } 121 | 122 | function _start(type, extralProxyOptions) { 123 | return getPM2Config(type, extralProxyOptions).then(function (_ref) { 124 | var pm2Config = _ref.pm2Config; 125 | return new Promise(function (resolve) { 126 | _pm2.default.start(pm2Config, function (err, apps) { 127 | if (err) { 128 | throw err; 129 | } 130 | 131 | log('start'); 132 | resolve(apps); 133 | }); 134 | }); 135 | }).then(function () { 136 | return disconnect(); 137 | }); 138 | } 139 | 140 | function getRunningInfo(name) { 141 | return new Promise(function (resolve) { 142 | _pm2.default.describe(name, function (err, descriptions) { 143 | if (err) { 144 | throw err; 145 | } 146 | 147 | // TODO: there should not be more than one process 148 | // “online”, “stopping”, 149 | // “stopped”, “launching”, 150 | // “errored”, or “one-launch-status” 151 | var status = descriptions.length > 0 && descriptions[0].pm2_env.status !== 'stopped' && descriptions[0].pm2_env.status !== 'errored'; 152 | 153 | resolve(status); 154 | }); 155 | }); 156 | } 157 | 158 | function _stop(type, extralProxyOptions) { 159 | var config = null; 160 | 161 | return getPM2Config(type, extralProxyOptions).then(function (conf) { 162 | config = conf; 163 | var name = config.pm2Config.name; 164 | 165 | return getRunningInfo(name); 166 | }).then(function (isRunning) { 167 | var _config = config, 168 | pm2Config = _config.pm2Config; 169 | var name = pm2Config.name; 170 | 171 | 172 | if (!isRunning) { 173 | log('already stopped'); 174 | return; 175 | } 176 | 177 | // eslint-disable-next-line 178 | return new Promise(function (resolve) { 179 | _pm2.default.stop(name, function (err) { 180 | if (err && err.message !== 'process name not found') { 181 | throw err; 182 | } 183 | 184 | log('stop'); 185 | resolve(); 186 | }); 187 | }); 188 | }).then(function () { 189 | return disconnect(); 190 | }); 191 | } 192 | 193 | /** 194 | * @public 195 | * @param {[type]} args [description] 196 | * @return {[type]} [description] 197 | */ 198 | function start() { 199 | return _start.apply(undefined, arguments).catch(handleError); 200 | } 201 | 202 | /** 203 | * @public 204 | * @param {[type]} args [description] 205 | * @return {[type]} [description] 206 | */ 207 | function stop() { 208 | return _stop.apply(undefined, arguments).catch(handleError); 209 | } 210 | 211 | /** 212 | * @public 213 | * @param {[type]} args [description] 214 | * @return {[type]} [description] 215 | */ 216 | function restart() { 217 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 218 | args[_key] = arguments[_key]; 219 | } 220 | 221 | return _stop.apply(undefined, args).then(function () { 222 | return _start.apply(undefined, args); 223 | }).catch(handleError); 224 | } 225 | 226 | // if (module === require.main) { 227 | // restart('local', { 228 | // password: 'holic123', 229 | // serverAddr: 'kr.oyyd.net', 230 | // }); 231 | // } -------------------------------------------------------------------------------- /lib/recordMemoryUsage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.INTERVAL_TIME = undefined; 5 | exports.record = record; 6 | exports.stopRecord = stopRecord; 7 | 8 | var _fs = require('fs'); 9 | 10 | var _path = require('path'); 11 | 12 | // NOTE: do not use this in production 13 | var INTERVAL_TIME = exports.INTERVAL_TIME = 1000; 14 | 15 | var data = null; 16 | 17 | function record(frame) { 18 | data = data || []; 19 | 20 | data.push(frame); 21 | } 22 | 23 | function stopRecord() { 24 | if (data) { 25 | (0, _fs.writeFileSync)((0, _path.join)(__dirname, '../logs/memory.json'), JSON.stringify(data)); 26 | } 27 | } -------------------------------------------------------------------------------- /lib/ssLocal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.usernamePasswordAuthetication = usernamePasswordAuthetication; 5 | exports.startServer = startServer; 6 | 7 | var _ip = require('ip'); 8 | 9 | var _ip2 = _interopRequireDefault(_ip); 10 | 11 | var _net = require('net'); 12 | 13 | var _utils = require('./utils'); 14 | 15 | var _logger = require('./logger'); 16 | 17 | var _encryptor = require('./encryptor'); 18 | 19 | var _pacServer = require('./pacServer'); 20 | 21 | var _createUDPRelay = require('./createUDPRelay'); 22 | 23 | var _createUDPRelay2 = _interopRequireDefault(_createUDPRelay); 24 | 25 | var _auth = require('./auth'); 26 | 27 | var _config = require('./config'); 28 | 29 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 30 | 31 | var NAME = 'ss_local'; 32 | 33 | var logger = void 0; 34 | 35 | function handleMethod(connection, data, authInfo) { 36 | // +----+----------+----------+ 37 | // |VER | NMETHODS | METHODS | 38 | // +----+----------+----------+ 39 | // | 1 | 1 | 1 to 255 | 40 | // +----+----------+----------+ 41 | var forceAuth = authInfo.forceAuth; 42 | 43 | var buf = new Buffer(2); 44 | 45 | var method = -1; 46 | 47 | if (forceAuth && data.indexOf(0x02, 2) >= 0) { 48 | method = 2; 49 | } 50 | 51 | if (!forceAuth && data.indexOf(0x00, 2) >= 0) { 52 | method = 0; 53 | } 54 | 55 | // allow `no authetication` or any usename/password 56 | if (method === -1) { 57 | // logger.warn(`unsupported method: ${data.toString('hex')}`); 58 | buf.writeUInt16BE(0x05FF); 59 | connection.write(buf); 60 | connection.end(); 61 | return -1; 62 | } 63 | 64 | buf.writeUInt16BE(0x0500); 65 | connection.write(buf); 66 | 67 | return method === 0 ? 1 : 3; 68 | } 69 | 70 | function fetchUsernamePassword(data) { 71 | // suppose all VER is 0x01 72 | if (!(data instanceof Buffer)) { 73 | return null; 74 | } 75 | 76 | var ulen = data[1]; 77 | var username = data.slice(2, ulen + 2).toString('ascii'); 78 | var plenStart = ulen + 2; 79 | var plen = data[plenStart]; 80 | var password = data.slice(plenStart + 1, plenStart + 1 + plen).toString('ascii'); 81 | 82 | return { username: username, password: password }; 83 | } 84 | 85 | function responseAuth(success, connection) { 86 | var buf = new Buffer(2); 87 | var toWrite = success ? 0x0100 : 0x0101; 88 | var nextProcedure = success ? 2 : -1; 89 | 90 | buf.writeUInt16BE(toWrite); 91 | connection.write(buf); 92 | connection.end(); 93 | 94 | return nextProcedure; 95 | } 96 | 97 | function usernamePasswordAuthetication(connection, data, authInfo) { 98 | // +----+------+----------+------+----------+ 99 | // |VER | ULEN | UNAME | PLEN | PASSWD | 100 | // +----+------+----------+------+----------+ 101 | // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | 102 | // +----+------+----------+------+----------+ 103 | 104 | var usernamePassword = fetchUsernamePassword(data); 105 | 106 | if (!usernamePassword) { 107 | return responseAuth(false, connection); 108 | } 109 | 110 | var username = usernamePassword.username, 111 | password = usernamePassword.password; 112 | 113 | 114 | if (!(0, _auth.validate)(authInfo, username, password)) { 115 | return responseAuth(false, connection); 116 | } 117 | 118 | return responseAuth(true, connection); 119 | } 120 | 121 | function handleRequest(connection, data, _ref, dstInfo, onConnect, onDestroy, isClientConnected) { 122 | var serverAddr = _ref.serverAddr, 123 | serverPort = _ref.serverPort, 124 | password = _ref.password, 125 | method = _ref.method, 126 | localAddr = _ref.localAddr, 127 | localPort = _ref.localPort, 128 | localAddrIPv6 = _ref.localAddrIPv6; 129 | 130 | var cmd = data[1]; 131 | var clientOptions = { 132 | port: serverPort, 133 | host: serverAddr 134 | }; 135 | var isUDPRelay = cmd === 0x03; 136 | 137 | var repBuf = void 0; 138 | var tmp = null; 139 | var decipher = null; 140 | var decipheredData = null; 141 | var cipher = null; 142 | var cipheredData = null; 143 | 144 | if (cmd !== 0x01 && !isUDPRelay) { 145 | logger.warn('unsupported cmd: ' + cmd); 146 | return { 147 | stage: -1 148 | }; 149 | } 150 | 151 | // prepare data 152 | 153 | // +----+-----+-------+------+----------+----------+ 154 | // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | 155 | // +----+-----+-------+------+----------+----------+ 156 | // | 1 | 1 | X'00' | 1 | Variable | 2 | 157 | // +----+-----+-------+------+----------+----------+ 158 | 159 | if (isUDPRelay) { 160 | var isUDP4 = dstInfo.atyp === 1; 161 | 162 | repBuf = new Buffer(4); 163 | repBuf.writeUInt32BE(isUDP4 ? 0x05000001 : 0x05000004); 164 | tmp = new Buffer(2); 165 | tmp.writeUInt16BE(localPort); 166 | repBuf = Buffer.concat([repBuf, _ip2.default.toBuffer(isUDP4 ? localAddr : localAddrIPv6), tmp]); 167 | 168 | connection.write(repBuf); 169 | 170 | return { 171 | stage: -1 172 | }; 173 | } 174 | 175 | logger.verbose('connecting: ' + dstInfo.dstAddr.toString('utf8') + (':' + dstInfo.dstPort.readUInt16BE())); 176 | 177 | repBuf = new Buffer(10); 178 | repBuf.writeUInt32BE(0x05000001); 179 | repBuf.writeUInt32BE(0x00000000, 4, 4); 180 | repBuf.writeUInt16BE(0, 8, 2); 181 | 182 | tmp = (0, _encryptor.createCipher)(password, method, data.slice(3)); // skip VER, CMD, RSV 183 | cipher = tmp.cipher; 184 | cipheredData = tmp.data; 185 | 186 | // connect 187 | var clientToRemote = (0, _net.connect)(clientOptions, function () { 188 | onConnect(); 189 | }); 190 | 191 | clientToRemote.on('data', function (remoteData) { 192 | if (!decipher) { 193 | tmp = (0, _encryptor.createDecipher)(password, method, remoteData); 194 | if (!tmp) { 195 | logger.warn(NAME + ' get invalid msg'); 196 | onDestroy(); 197 | return; 198 | } 199 | decipher = tmp.decipher; 200 | decipheredData = tmp.data; 201 | } else { 202 | decipheredData = decipher.update(remoteData); 203 | } 204 | 205 | if (isClientConnected()) { 206 | (0, _utils.writeOrPause)(clientToRemote, connection, decipheredData); 207 | } else { 208 | clientToRemote.destroy(); 209 | } 210 | }); 211 | 212 | clientToRemote.on('drain', function () { 213 | connection.resume(); 214 | }); 215 | 216 | clientToRemote.on('end', function () { 217 | connection.end(); 218 | }); 219 | 220 | clientToRemote.on('error', function (e) { 221 | logger.warn('ssLocal error happened in clientToRemote when' + (' connecting to ' + (0, _utils.getDstStr)(dstInfo) + ': ' + e.message)); 222 | 223 | onDestroy(); 224 | }); 225 | 226 | clientToRemote.on('close', function (e) { 227 | if (e) { 228 | connection.destroy(); 229 | } else { 230 | connection.end(); 231 | } 232 | }); 233 | 234 | // write 235 | connection.write(repBuf); 236 | 237 | (0, _utils.writeOrPause)(connection, clientToRemote, cipheredData); 238 | 239 | return { 240 | stage: 2, 241 | cipher: cipher, 242 | clientToRemote: clientToRemote 243 | }; 244 | } 245 | 246 | function handleConnection(config, connection) { 247 | var authInfo = config.authInfo; 248 | 249 | 250 | var stage = 0; 251 | var clientToRemote = void 0; 252 | var tmp = void 0; 253 | var cipher = void 0; 254 | var dstInfo = void 0; 255 | var remoteConnected = false; 256 | var clientConnected = true; 257 | var timer = null; 258 | 259 | connection.on('data', function (data) { 260 | switch (stage) { 261 | case 0: 262 | stage = handleMethod(connection, data, authInfo); 263 | 264 | break; 265 | case 1: 266 | dstInfo = (0, _utils.getDstInfo)(data); 267 | 268 | if (!dstInfo) { 269 | logger.warn('Failed to get \'dstInfo\' from parsing data: ' + data); 270 | connection.destroy(); 271 | return; 272 | } 273 | 274 | tmp = handleRequest(connection, data, config, dstInfo, function () { 275 | // after connected 276 | remoteConnected = true; 277 | }, function () { 278 | // get invalid msg or err happened 279 | if (remoteConnected) { 280 | remoteConnected = false; 281 | clientToRemote.destroy(); 282 | } 283 | 284 | if (clientConnected) { 285 | clientConnected = false; 286 | connection.destroy(); 287 | } 288 | }, function () { 289 | return clientConnected; 290 | }); 291 | 292 | stage = tmp.stage; 293 | 294 | if (stage === 2) { 295 | clientToRemote = tmp.clientToRemote; 296 | cipher = tmp.cipher; 297 | } else { 298 | // udp relay 299 | clientConnected = false; 300 | connection.end(); 301 | } 302 | 303 | break; 304 | case 2: 305 | tmp = cipher.update(data); 306 | 307 | (0, _utils.writeOrPause)(connection, clientToRemote, tmp); 308 | 309 | break; 310 | case 3: 311 | // rfc 1929 username/password authetication 312 | stage = usernamePasswordAuthetication(connection, data, authInfo); 313 | break; 314 | default: 315 | } 316 | }); 317 | 318 | connection.on('drain', function () { 319 | if (remoteConnected) { 320 | clientToRemote.resume(); 321 | } 322 | }); 323 | 324 | connection.on('end', function () { 325 | clientConnected = false; 326 | if (remoteConnected) { 327 | clientToRemote.end(); 328 | } 329 | }); 330 | 331 | connection.on('close', function (e) { 332 | if (timer) { 333 | clearTimeout(timer); 334 | } 335 | 336 | clientConnected = false; 337 | 338 | if (remoteConnected) { 339 | if (e) { 340 | clientToRemote.destroy(); 341 | } else { 342 | clientToRemote.end(); 343 | } 344 | } 345 | }); 346 | 347 | connection.on('error', function (e) { 348 | logger.warn(NAME + ' error happened in client connection: ' + e.message); 349 | }); 350 | 351 | timer = setTimeout(function () { 352 | if (clientConnected) { 353 | connection.destroy(); 354 | } 355 | 356 | if (remoteConnected) { 357 | clientToRemote.destroy(); 358 | } 359 | }, config.timeout * 1000); 360 | } 361 | 362 | function closeAll() { 363 | (0, _utils.closeSilently)(this.server); 364 | (0, _utils.closeSilently)(this.pacServer); 365 | 366 | this.udpRelay.close(); 367 | 368 | if (this.httpProxyServer) { 369 | this.httpProxyServer.close(); 370 | } 371 | } 372 | 373 | function createServer(config) { 374 | var server = (0, _net.createServer)(handleConnection.bind(null, config)); 375 | var udpRelay = (0, _createUDPRelay2.default)(config, false, logger); 376 | var pacServer = (0, _pacServer.createPACServer)(config, logger); 377 | 378 | server.on('close', function () { 379 | logger.warn(NAME + ' server closed'); 380 | }); 381 | 382 | server.on('error', function (e) { 383 | logger.error(NAME + ' server error: ' + e.message); 384 | }); 385 | 386 | server.listen(config.localPort); 387 | 388 | logger.verbose(NAME + ' is listening on ' + config.localAddr + ':' + config.localPort); 389 | 390 | return { 391 | server: server, 392 | udpRelay: udpRelay, 393 | pacServer: pacServer, 394 | closeAll: closeAll 395 | }; 396 | } 397 | 398 | // eslint-disable-next-line 399 | function startServer(config) { 400 | var willLogToConsole = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 401 | 402 | logger = logger || (0, _logger.createLogger)(config, _logger.LOG_NAMES.LOCAL, willLogToConsole); 403 | 404 | var _createAuthInfo = (0, _auth.createAuthInfo)(config), 405 | info = _createAuthInfo.info, 406 | warn = _createAuthInfo.warn, 407 | error = _createAuthInfo.error; 408 | 409 | if (error) { 410 | logger.error(NAME + ' error: ' + error); 411 | return null; 412 | } 413 | 414 | if (warn) { 415 | logger.warn(NAME + ': ' + warn); 416 | } 417 | 418 | return createServer(Object.assign({}, config, { 419 | authInfo: info 420 | })); 421 | } 422 | 423 | if (module === require.main) { 424 | (0, _config.getConfig)(process.argv.slice(2), function (err, config) { 425 | if (err) { 426 | throw err; 427 | } 428 | 429 | var proxyOptions = config.proxyOptions; 430 | 431 | 432 | logger = (0, _logger.createLogger)(proxyOptions, _logger.LOG_NAMES.LOCAL, true, true); 433 | startServer(proxyOptions, false); 434 | }); 435 | 436 | process.on('uncaughtException', function (err) { 437 | logger.error(NAME + ' uncaughtException: ' + err.stack + ' ', (0, _utils.createSafeAfterHandler)(logger, function () { 438 | process.exit(1); 439 | })); 440 | }); 441 | } -------------------------------------------------------------------------------- /lib/ssServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.startServer = startServer; 5 | 6 | var _ip = require('ip'); 7 | 8 | var _ip2 = _interopRequireDefault(_ip); 9 | 10 | var _net = require('net'); 11 | 12 | var _utils = require('./utils'); 13 | 14 | var _logger = require('./logger'); 15 | 16 | var _encryptor = require('./encryptor'); 17 | 18 | var _createUDPRelay = require('./createUDPRelay'); 19 | 20 | var _createUDPRelay2 = _interopRequireDefault(_createUDPRelay); 21 | 22 | var _config = require('./config'); 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | var NAME = 'ss_server'; 27 | // import { INTERVAL_TIME } from './recordMemoryUsage'; 28 | 29 | 30 | var logger = void 0; 31 | 32 | function createClientToDst(connection, data, password, method, onConnect, onDestroy, isLocalConnected, connectFunc) { 33 | var dstInfo = (0, _utils.getDstInfo)(data, true); 34 | 35 | var cipher = null; 36 | var tmp = void 0; 37 | var cipheredData = void 0; 38 | var preservedData = null; 39 | 40 | if (!dstInfo) { 41 | logger.warn(NAME + ' receive invalid msg. ' + 'local method/password doesn\'t accord with the server\'s?'); 42 | return null; 43 | } 44 | 45 | var clientOptions = { 46 | port: dstInfo.dstPort.readUInt16BE(), 47 | host: dstInfo.atyp === 3 ? dstInfo.dstAddr.toString('ascii') : _ip2.default.toString(dstInfo.dstAddr) 48 | }; 49 | 50 | if (dstInfo.totalLength < data.length) { 51 | preservedData = data.slice(dstInfo.totalLength); 52 | } 53 | 54 | var clientToDst = null; 55 | 56 | if (connectFunc) { 57 | clientToDst = connectFunc(clientOptions, onConnect, data.slice(0, dstInfo.totalLength)); 58 | } else { 59 | clientToDst = (0, _net.connect)(clientOptions, onConnect); 60 | } 61 | 62 | clientToDst.on('data', function (clientData) { 63 | if (!cipher) { 64 | tmp = (0, _encryptor.createCipher)(password, method, clientData); 65 | cipher = tmp.cipher; 66 | cipheredData = tmp.data; 67 | } else { 68 | cipheredData = cipher.update(clientData); 69 | } 70 | 71 | if (isLocalConnected()) { 72 | (0, _utils.writeOrPause)(clientToDst, connection, cipheredData); 73 | } else { 74 | clientToDst.destroy(); 75 | } 76 | }); 77 | 78 | clientToDst.on('drain', function () { 79 | connection.resume(); 80 | }); 81 | 82 | clientToDst.on('end', function () { 83 | if (isLocalConnected()) { 84 | connection.end(); 85 | } 86 | }); 87 | 88 | clientToDst.on('error', function (e) { 89 | logger.warn('ssServer error happened when write to DST: ' + e.message); 90 | onDestroy(); 91 | }); 92 | 93 | clientToDst.on('close', function (e) { 94 | if (isLocalConnected()) { 95 | if (e) { 96 | connection.destroy(); 97 | } else { 98 | connection.end(); 99 | } 100 | } 101 | }); 102 | 103 | return { 104 | clientToDst: clientToDst, preservedData: preservedData 105 | }; 106 | } 107 | 108 | function handleConnection(config, connection) { 109 | var stage = 0; 110 | var clientToDst = null; 111 | var decipher = null; 112 | var tmp = void 0; 113 | var data = void 0; 114 | var localConnected = true; 115 | var dstConnected = false; 116 | var timer = null; 117 | 118 | connection.on('data', function (chunck) { 119 | try { 120 | if (!decipher) { 121 | tmp = (0, _encryptor.createDecipher)(config.password, config.method, chunck); 122 | decipher = tmp.decipher; 123 | data = tmp.data; 124 | } else { 125 | data = decipher.update(chunck); 126 | } 127 | } catch (e) { 128 | logger.warn(NAME + ' receive invalid data'); 129 | return; 130 | } 131 | 132 | switch (stage) { 133 | case 0: 134 | // TODO: should pause? or preserve data? 135 | connection.pause(); 136 | 137 | tmp = createClientToDst(connection, data, config.password, config.method, function () { 138 | dstConnected = true; 139 | connection.resume(); 140 | }, function () { 141 | if (dstConnected) { 142 | dstConnected = false; 143 | clientToDst.destroy(); 144 | } 145 | 146 | if (localConnected) { 147 | localConnected = false; 148 | connection.destroy(); 149 | } 150 | }, function () { 151 | return localConnected; 152 | }, config.connect); 153 | 154 | if (!tmp) { 155 | connection.destroy(); 156 | return; 157 | } 158 | 159 | clientToDst = tmp.clientToDst; 160 | 161 | if (tmp.preservedData) { 162 | (0, _utils.writeOrPause)(connection, clientToDst, tmp.preservedData); 163 | } 164 | 165 | stage = 1; 166 | break; 167 | case 1: 168 | (0, _utils.writeOrPause)(connection, clientToDst, data); 169 | break; 170 | default: 171 | } 172 | }); 173 | 174 | connection.on('drain', function () { 175 | clientToDst.resume(); 176 | }); 177 | 178 | connection.on('end', function () { 179 | localConnected = false; 180 | 181 | if (dstConnected) { 182 | clientToDst.end(); 183 | } 184 | }); 185 | 186 | connection.on('error', function (e) { 187 | logger.warn('ssServer error happened in the connection with ssLocal : ' + e.message); 188 | }); 189 | 190 | connection.on('close', function (e) { 191 | if (timer) { 192 | clearTimeout(timer); 193 | } 194 | 195 | localConnected = false; 196 | 197 | if (dstConnected) { 198 | if (e) { 199 | clientToDst.destroy(); 200 | } else { 201 | clientToDst.end(); 202 | } 203 | } 204 | }); 205 | 206 | timer = setTimeout(function () { 207 | if (localConnected) { 208 | connection.destroy(); 209 | } 210 | 211 | if (dstConnected) { 212 | clientToDst.destroy(); 213 | } 214 | }, config.timeout * 1000); 215 | } 216 | 217 | function createServer(config) { 218 | var serverAddr = config.serverAddr, 219 | _config$udpActive = config.udpActive, 220 | udpActive = _config$udpActive === undefined ? true : _config$udpActive; 221 | 222 | var server = (0, _net.createServer)(handleConnection.bind(null, config)).listen(config.serverPort, serverAddr); 223 | 224 | var udpRelay = null; 225 | 226 | if (udpActive) { 227 | udpRelay = (0, _createUDPRelay2.default)(config, true, logger); 228 | } 229 | 230 | server.on('close', function () { 231 | logger.warn(NAME + ' server closed'); 232 | }); 233 | 234 | server.on('error', function (e) { 235 | logger.error(NAME + ' server error: ' + e.message); 236 | }); 237 | 238 | logger.verbose(NAME + ' is listening on ' + config.serverAddr + ':' + config.serverPort); 239 | 240 | return { 241 | server: server, udpRelay: udpRelay 242 | }; 243 | } 244 | 245 | // eslint-disable-next-line 246 | function startServer(config) { 247 | var willLogToConsole = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 248 | var injectedLogger = arguments[2]; 249 | 250 | logger = logger || injectedLogger || (0, _logger.createLogger)(config, _logger.LOG_NAMES.SERVER, willLogToConsole); 251 | 252 | return createServer(config); 253 | } 254 | 255 | if (module === require.main) { 256 | (0, _config.getConfig)(process.argv.slice(2), function (err, config) { 257 | if (err) { 258 | throw err; 259 | } 260 | 261 | var proxyOptions = config.proxyOptions; 262 | 263 | 264 | logger = (0, _logger.createLogger)(proxyOptions, _logger.LOG_NAMES.SERVER, true, true); 265 | startServer(proxyOptions, false); 266 | 267 | // TODO: 268 | // NOTE: DEV only 269 | // eslint-disable-next-line 270 | // if (config._recordMemoryUsage) { 271 | // setInterval(() => { 272 | // process.send(process.memoryUsage()); 273 | // }, INTERVAL_TIME); 274 | // } 275 | }); 276 | 277 | process.on('uncaughtException', function (err) { 278 | logger.error(NAME + ' uncaughtException: ' + err.stack, (0, _utils.createSafeAfterHandler)(logger, function () { 279 | process.exit(1); 280 | })); 281 | }); 282 | } -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | exports.BufferFrom = undefined; 5 | 6 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 7 | 8 | exports.isWindows = isWindows; 9 | exports.safelyKill = safelyKill; 10 | exports.safelyKillChild = safelyKillChild; 11 | exports.fileLog = fileLog; 12 | exports.createSafeAfterHandler = createSafeAfterHandler; 13 | exports.closeSilently = closeSilently; 14 | exports.mkdirIfNotExistSync = mkdirIfNotExistSync; 15 | exports.sendDgram = sendDgram; 16 | exports.writeOrPause = writeOrPause; 17 | exports.getDstInfo = getDstInfo; 18 | exports.getDstInfoFromUDPMsg = getDstInfoFromUDPMsg; 19 | exports.formatConfig = formatConfig; 20 | exports.getDstStr = getDstStr; 21 | exports.getPrefixedArgName = getPrefixedArgName; 22 | exports.obj2Argv = obj2Argv; 23 | 24 | var _ip = require('ip'); 25 | 26 | var _ip2 = _interopRequireDefault(_ip); 27 | 28 | var _os = require('os'); 29 | 30 | var _os2 = _interopRequireDefault(_os); 31 | 32 | var _fs = require('fs'); 33 | 34 | var _path = require('path'); 35 | 36 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 37 | 38 | var DEFAULT_PATH = (0, _path.join)(__dirname, '../logs/debug.log'); 39 | var hasOwnProperty = {}.hasOwnProperty; 40 | 41 | var platform = null; 42 | 43 | var BufferFrom = exports.BufferFrom = function () { 44 | try { 45 | Buffer.from('aa', 'hex'); 46 | } catch (err) { 47 | return function () { 48 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 49 | args[_key] = arguments[_key]; 50 | } 51 | 52 | return new (Function.prototype.bind.apply(Buffer, [null].concat(args)))(); 53 | }; 54 | } 55 | 56 | return Buffer.from; 57 | }(); 58 | 59 | function isWindows() { 60 | if (!platform) { 61 | platform = _os2.default.type(); 62 | } 63 | 64 | return platform === 'Windows_NT'; 65 | } 66 | 67 | function safelyKill(pid, signal) { 68 | if (pid === null || pid === undefined) { 69 | return; 70 | } 71 | 72 | if (signal && !isWindows()) { 73 | process.kill(pid, signal); 74 | } else { 75 | process.kill(pid); 76 | } 77 | } 78 | 79 | function safelyKillChild(child, signal) { 80 | if (!child) { 81 | return; 82 | } 83 | 84 | if (signal && !isWindows()) { 85 | child.kill(signal); 86 | } else { 87 | child.kill(); 88 | } 89 | } 90 | 91 | function fileLog(content) { 92 | var path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : DEFAULT_PATH; 93 | 94 | (0, _fs.writeFileSync)(path, content); 95 | } 96 | 97 | // NOTE: https://github.com/winstonjs/winston/issues/228 98 | // Winston will log things asynchronously so we have to 99 | // make sure it has log the error before exiting this 100 | // process. 101 | // And this is disappointing. 102 | function createSafeAfterHandler(logger, next) { 103 | var numFlushes = 0; 104 | var numFlushed = 0; 105 | 106 | return function () { 107 | Object.keys(logger.transports).forEach(function (k) { 108 | // eslint-disable-next-line 109 | var stream = logger.transports[k]._stream; 110 | if (stream) { 111 | numFlushes += 1; 112 | stream.once('finish', function () { 113 | numFlushed += 1; 114 | if (numFlushes === numFlushed) { 115 | next(); 116 | } 117 | }); 118 | stream.end(); 119 | } 120 | }); 121 | 122 | if (numFlushes === 0) { 123 | next(); 124 | } 125 | }; 126 | } 127 | 128 | function closeSilently(server) { 129 | if (server) { 130 | try { 131 | server.close(); 132 | } catch (e) { 133 | // already closed 134 | } 135 | } 136 | } 137 | 138 | function mkdirIfNotExistSync(path) { 139 | try { 140 | (0, _fs.accessSync)(path); 141 | } catch (e) { 142 | (0, _fs.mkdirSync)(path); 143 | } 144 | } 145 | 146 | function sendDgram(socket, data) { 147 | for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { 148 | args[_key2 - 2] = arguments[_key2]; 149 | } 150 | 151 | socket.send.apply(socket, [data, 0, data.length].concat(args)); 152 | } 153 | 154 | function writeOrPause(fromCon, toCon, data) { 155 | var res = toCon.write(data); 156 | 157 | if (!res) { 158 | fromCon.pause(); 159 | } 160 | 161 | return res; 162 | } 163 | 164 | function parseDstInfo(data, offset) { 165 | var atyp = data[offset]; 166 | 167 | var dstAddr = void 0; 168 | var dstPort = void 0; 169 | var dstAddrLength = void 0; 170 | var dstPortIndex = void 0; 171 | var dstPortEnd = void 0; 172 | // length of non-data field 173 | var totalLength = void 0; 174 | 175 | switch (atyp) { 176 | case 0x01: 177 | dstAddrLength = 4; 178 | dstAddr = data.slice(offset + 1, offset + 5); 179 | dstPort = data.slice(offset + 5, offset + 7); 180 | totalLength = offset + 7; 181 | break; 182 | case 0x04: 183 | dstAddrLength = 16; 184 | dstAddr = data.slice(offset + 1, offset + 17); 185 | dstPort = data.slice(offset + 17, offset + 19); 186 | totalLength = offset + 19; 187 | break; 188 | case 0x03: 189 | dstAddrLength = data[offset + 1]; 190 | dstPortIndex = 2 + offset + dstAddrLength; 191 | dstAddr = data.slice(offset + 2, dstPortIndex); 192 | dstPortEnd = dstPortIndex + 2; 193 | dstPort = data.slice(dstPortIndex, dstPortEnd); 194 | totalLength = dstPortEnd; 195 | break; 196 | default: 197 | return null; 198 | } 199 | 200 | if (data.length < totalLength) { 201 | return null; 202 | } 203 | 204 | return { 205 | atyp: atyp, dstAddrLength: dstAddrLength, dstAddr: dstAddr, dstPort: dstPort, totalLength: totalLength 206 | }; 207 | } 208 | 209 | function getDstInfo(data, isServer) { 210 | // +----+-----+-------+------+----------+----------+ 211 | // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | 212 | // +----+-----+-------+------+----------+----------+ 213 | // | 1 | 1 | X'00' | 1 | Variable | 2 | 214 | // +----+-----+-------+------+----------+----------+ 215 | // Yet begin with ATYP. 216 | 217 | var offset = isServer ? 0 : 3; 218 | return parseDstInfo(data, offset); 219 | } 220 | 221 | function getDstInfoFromUDPMsg(data, isServer) { 222 | // +----+------+------+----------+----------+----------+ 223 | // |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | 224 | // +----+------+------+----------+----------+----------+ 225 | // | 2 | 1 | 1 | Variable | 2 | Variable | 226 | // +----+------+------+----------+----------+----------+ 227 | 228 | var offset = isServer ? 0 : 3; 229 | 230 | return parseDstInfo(data, offset); 231 | } 232 | 233 | var formatKeyValues = { 234 | server: 'serverAddr', 235 | server_port: 'serverPort', 236 | local_addr: 'localAddr', 237 | local_port: 'localPort', 238 | local_addr_ipv6: 'localAddrIPv6', 239 | server_addr_ipv6: 'serverAddrIPv6' 240 | }; 241 | 242 | function formatConfig(_config) { 243 | var formattedConfig = Object.assign({}, _config); 244 | 245 | Object.keys(formatKeyValues).forEach(function (key) { 246 | if (hasOwnProperty.call(formattedConfig, key)) { 247 | formattedConfig[formatKeyValues[key]] = formattedConfig[key]; 248 | delete formattedConfig[key]; 249 | } 250 | }); 251 | 252 | return formattedConfig; 253 | } 254 | 255 | function getDstStr(dstInfo) { 256 | if (!dstInfo) { 257 | return null; 258 | } 259 | 260 | switch (dstInfo.atyp) { 261 | case 1: 262 | case 4: 263 | return _ip2.default.toString(dstInfo.dstAddr) + ':' + dstInfo.dstPort.readUInt16BE(); 264 | case 3: 265 | return dstInfo.dstAddr.toString('utf8') + ':' + dstInfo.dstPort.readUInt16BE(); 266 | default: 267 | return 'WARN: invalid atyp'; 268 | } 269 | } 270 | 271 | function getPrefixedArgName(name) { 272 | return name.length === 1 ? '-' + name : '--' + name; 273 | } 274 | 275 | function obj2Argv(obj) { 276 | if ((typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) !== 'object') { 277 | throw new Error('expect an object when stringify to argv'); 278 | } 279 | 280 | var argv = ''; 281 | 282 | Object.keys(obj).forEach(function (name) { 283 | var argName = getPrefixedArgName(name); 284 | var value = obj[name]; 285 | var argValue = ''; 286 | 287 | if (typeof value === 'boolean') { 288 | if (!value) { 289 | return; 290 | } 291 | } else { 292 | argValue = String(value); 293 | } 294 | 295 | var parts = argValue.length > 0 ? argName + ' ' + argValue : '' + argName; 296 | 297 | argv = argv + ' ' + parts; 298 | }); 299 | 300 | return argv; 301 | } -------------------------------------------------------------------------------- /pac/user.txt: -------------------------------------------------------------------------------- 1 | ! NOTE: rules exported from https://github.com/vangie/gfwlist2pac 2 | ! 用户自定义的代理规则 3 | ! 4 | ! 语法与gfwlist相同,即AdBlock Plus过滤规则 5 | ! 6 | ! 简单说明如下: 7 | ! 通配符支持,如 *.example.com/* 实际书写时可省略* 如.example.com/ 意即*.example.com/* 8 | ! 正则表达式支持,以\开始和结束, 如 \[\w]+:\/\/example.com\ 9 | ! 例外规则 @@,如 @@*.example.com/* 满足@@后规则的地址不使用代理 10 | ! 匹配地址开始和结尾 |,如 |http://example.com、example.com|分别表示以http://example.com开始和以example.com结束的地址 11 | ! || 标记,如 ||example.com 则http://example.com、https://example.com、ftp://example.com等地址均满足条件 12 | ! 注释 ! 如 ! Comment 13 | ! 14 | ! 更详细说明 请访问 http://adblockplus.org/en/filters 15 | ! 16 | ! 配置该文件时需谨慎,尽量避免与gfwlist产生冲突, 17 | ! 或将一些本不需要代理的网址添加到代理列表 18 | ! 可用test目录工具进行网址测试 19 | ! 20 | 21 | ! Tip: 在最终生成的PAC文件中,用户定义规则先于gfwlist规则被处理 22 | ! 因此可以在这里添加一些常用网址规则,或能减少在访问这些网址进行查询的时间 23 | ! 如: 24 | !@@sina.com 25 | !@@163.com 26 | !twitter.com 27 | !youtube.com 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encryptsocks", 3 | "version": "1.4.4", 4 | "description": "Encrypt your socks transmission.", 5 | "keywords": [ 6 | "SOCKS5", 7 | "firewall", 8 | "http_proxy" 9 | ], 10 | "main": "./lib/index.js", 11 | "bin": { 12 | "serverssjs": "./bin/serverssjs", 13 | "localssjs": "./bin/localssjs" 14 | }, 15 | "scripts": { 16 | "build": "rm -rf lib && babel src --out-dir lib && eslint src", 17 | "dev": "babel src --watch --out-dir lib --source-maps inline", 18 | "lint": "eslint src", 19 | "test": "mocha", 20 | "test-coverage": "istanbul cover _mocha", 21 | "travis-ci-test": "mocha --fgrep '[local only]' --invert --require babel-polyfill", 22 | "travis-ci-test-coverage": "istanbul cover _mocha -- --fgrep '[local only]' --invert --require babel-polyfill", 23 | "benchmark": "node benchmark/run" 24 | }, 25 | "author": "oyyd ", 26 | "license": "BSD", 27 | "devDependencies": { 28 | "babel-cli": "^6.6.5", 29 | "babel-eslint": "^7.1.0", 30 | "babel-plugin-transform-async-to-generator": "^6.16.0", 31 | "babel-preset-es2015": "^6.9.0", 32 | "babel-preset-es2015-loose": "^7.0.0", 33 | "eslint": "^3.19.0", 34 | "eslint-config-airbnb-base": "^11.2.0", 35 | "eslint-plugin-import": "^2.3.0", 36 | "istanbul": "^0.4.3", 37 | "mocha": "^2.4.5", 38 | "socks": "^1.1.9", 39 | "socks5-http-client": "^1.0.2", 40 | "socks5-https-client": "^1.1.2" 41 | }, 42 | "dependencies": { 43 | "babel-polyfill": "^6.16.0", 44 | "ip": "^1.1.3", 45 | "lru-cache": "^4.0.1", 46 | "minimist": "^1.2.0", 47 | "pm2": "^2.5.0", 48 | "uglify-js": "^3.0.18", 49 | "winston": "^2.2.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/auth.js: -------------------------------------------------------------------------------- 1 | export function createAuthInfo(config) { 2 | const { auth } = config; 3 | const info = { 4 | forceAuth: false, 5 | }; 6 | 7 | if (auth && auth.forceAuth) { 8 | info.forceAuth = true; 9 | } 10 | 11 | if (!info.forceAuth) { 12 | return { 13 | info, 14 | }; 15 | } 16 | 17 | const { usernamePassword } = auth; 18 | 19 | if (!usernamePassword || typeof usernamePassword !== 'object') { 20 | return { 21 | info, 22 | error: 'expect "usernamePassword" in your config file to be an object', 23 | }; 24 | } 25 | 26 | const keys = Object.keys(usernamePassword); 27 | 28 | if (keys.length === 0) { 29 | return { 30 | info, 31 | warn: 'no valid username/password found in your config file', 32 | }; 33 | } 34 | 35 | info.usernamePassword = usernamePassword; 36 | 37 | return { 38 | info, 39 | }; 40 | } 41 | 42 | export function validate(info, username, password) { 43 | return info.usernamePassword[username] === password; 44 | } 45 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | import { version } from '../package.json'; 2 | import * as ssLocal from './ssLocal'; 3 | import * as ssServer from './ssServer'; 4 | import { updateGFWList as _updateGFWList, GFWLIST_FILE_PATH } from './gfwlistUtils'; 5 | import { getConfig, DAEMON_COMMAND } from './config'; 6 | import { start, stop, restart } from './pm'; 7 | 8 | const log = console.log; // eslint-disable-line 9 | 10 | function getDaemonType(isServer) { 11 | return isServer ? 'server' : 'local'; 12 | } 13 | 14 | function logHelp(invalidOption) { 15 | log( 16 | // eslint-disable-next-line 17 | ` 18 | ${(invalidOption ? `${invalidOption}\n` : '')}encryptsocks ${version} 19 | You can supply configurations via either config file or command line arguments. 20 | 21 | Proxy options: 22 | -c CONFIG_FILE Path to the config file. 23 | -s SERVER_ADDR Server address. default: 0.0.0.0 24 | -p SERVER_PORT Server port. default: 8083 25 | -l LOCAL_ADDR Local binding address. default: 127.0.0.1 26 | -b LOCAL_PORT Local port. default: 1080 27 | -k PASSWORD Password. 28 | -m METHOD Encryption method. default: aes-128-cfb 29 | -t TIMEOUT Timeout in seconds. default: 600 30 | --pac_port PAC_PORT PAC file server port. default: 8090 31 | --pac_update_gfwlist [URL] [localssjs] Update the gfwlist 32 | for PAC server. You can specify the 33 | request URL. 34 | --log_path LOG_PATH The directory path to log. Won't if not set. 35 | --level LOG_LEVEL Log level. default: warn 36 | example: --level verbose 37 | General options: 38 | -h, --help Show this help message and exit. 39 | -d start/stop/restart Run as a daemon. 40 | ` 41 | ); 42 | } 43 | 44 | function updateGFWList(flag) { 45 | log('Updating gfwlist...'); 46 | 47 | const next = (err) => { 48 | if (err) { 49 | throw err; 50 | } else { 51 | log(`Updating finished. You can checkout the file here: ${GFWLIST_FILE_PATH}`); 52 | } 53 | }; 54 | 55 | if (typeof flag === 'string') { 56 | _updateGFWList(flag, next); 57 | } else { 58 | _updateGFWList(next); 59 | } 60 | } 61 | 62 | function runDaemon(isServer, cmd) { 63 | const type = getDaemonType(isServer); 64 | 65 | switch (cmd) { 66 | case DAEMON_COMMAND.start: { 67 | start(type); 68 | return; 69 | } 70 | case DAEMON_COMMAND.stop: { 71 | stop(type); 72 | return; 73 | } 74 | case DAEMON_COMMAND.restart: { 75 | restart(type); 76 | break; 77 | } 78 | default: 79 | } 80 | } 81 | 82 | function runSingle(isServer, proxyOptions) { 83 | const willLogToConsole = true; 84 | return isServer ? ssServer.startServer(proxyOptions, willLogToConsole) 85 | : ssLocal.startServer(proxyOptions, willLogToConsole); 86 | } 87 | 88 | export default function client(isServer) { 89 | const argv = process.argv.slice(2); 90 | 91 | getConfig(argv, (err, config) => { 92 | if (err) { 93 | throw err; 94 | } 95 | 96 | const { generalOptions, proxyOptions, invalidOption } = config; 97 | 98 | if (generalOptions.help || invalidOption) { 99 | logHelp(invalidOption); 100 | } else if (generalOptions.pacUpdateGFWList) { 101 | updateGFWList(generalOptions.pacUpdateGFWList); 102 | } else if (generalOptions.daemon) { 103 | runDaemon(isServer, generalOptions.daemon); 104 | } else { 105 | runSingle(isServer, proxyOptions); 106 | } 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { isV4Format } from 'ip'; 3 | import { lookup } from 'dns'; 4 | import minimist from 'minimist'; 5 | import { readFileSync, accessSync } from 'fs'; 6 | import DEFAULT_CONFIG from './defaultConfig'; 7 | import fileConfig from '../config.json'; 8 | import { getPrefixedArgName } from './utils'; 9 | 10 | export const DAEMON_COMMAND = { 11 | start: 'start', 12 | stop: 'stop', 13 | restart: 'restart', 14 | }; 15 | 16 | const PROXY_ARGUMENT_PAIR = { 17 | c: 'configFilePath', 18 | s: 'serverAddr', 19 | p: 'serverPort', 20 | pac_port: 'pacServerPort', 21 | l: 'localAddr', 22 | b: 'localPort', 23 | k: 'password', 24 | m: 'method', 25 | t: 'timeout', 26 | level: 'level', 27 | log_path: 'logPath', 28 | // private 29 | mem: '_recordMemoryUsage', 30 | }; 31 | 32 | const PROXY_ARGUMENT_EXTRAL_KEYS = [ 33 | 'localAddrIPv6', 34 | 'serverAddrIPv6', 35 | '_recordMemoryUsage', 36 | ]; 37 | 38 | const GENERAL_ARGUMENT_PAIR = { 39 | h: 'help', 40 | help: 'help', 41 | d: 'daemon', 42 | pac_update_gfwlist: 'pacUpdateGFWList', 43 | }; 44 | 45 | function getProxyOptionArgName(optionName) { 46 | // ignore these keys 47 | if (PROXY_ARGUMENT_EXTRAL_KEYS.indexOf(optionName) >= 0) { 48 | return null; 49 | } 50 | 51 | const result = Object.keys(PROXY_ARGUMENT_PAIR) 52 | .find(item => PROXY_ARGUMENT_PAIR[item] === optionName); 53 | 54 | if (!result) { 55 | throw new Error(`invalid optionName: "${optionName}"`); 56 | } 57 | 58 | return result; 59 | } 60 | 61 | export function stringifyProxyOptions(proxyOptions) { 62 | if (typeof proxyOptions !== 'object') { 63 | throw new Error('invalid type of "proxyOptions"'); 64 | } 65 | 66 | const args = []; 67 | 68 | Object.keys(proxyOptions).forEach((optionName) => { 69 | const value = proxyOptions[optionName]; 70 | const argName = getProxyOptionArgName(optionName); 71 | 72 | if (!argName) { 73 | return; 74 | } 75 | 76 | args.push(getPrefixedArgName(argName), value); 77 | }); 78 | 79 | return args.join(' '); 80 | } 81 | 82 | function getArgvOptions(argv) { 83 | const generalOptions = {}; 84 | const proxyOptions = {}; 85 | const configPair = minimist(argv); 86 | const optionsType = [{ 87 | options: proxyOptions, 88 | keys: Object.keys(PROXY_ARGUMENT_PAIR), 89 | values: PROXY_ARGUMENT_PAIR, 90 | }, { 91 | options: generalOptions, 92 | keys: Object.keys(GENERAL_ARGUMENT_PAIR), 93 | values: GENERAL_ARGUMENT_PAIR, 94 | }]; 95 | 96 | let invalidOption = null; 97 | 98 | Object.keys(configPair).forEach((key) => { 99 | if (key === '_') { 100 | return; 101 | } 102 | 103 | let hit = false; 104 | 105 | optionsType.forEach((optType) => { 106 | const i = optType.keys.indexOf(key); 107 | 108 | if (i >= 0) { 109 | optType.options[optType.values[optType.keys[i]]] = configPair[key]; // eslint-disable-line 110 | hit = true; 111 | } 112 | }); 113 | 114 | if (!hit) { 115 | invalidOption = key; 116 | } 117 | }); 118 | 119 | if (invalidOption) { 120 | invalidOption = (invalidOption.length === 1) ? `-${invalidOption}` : `--${invalidOption}`; 121 | } else if (generalOptions.daemon 122 | && Object.keys(DAEMON_COMMAND).indexOf(generalOptions.daemon) < 0) { 123 | invalidOption = `invalid daemon command: ${generalOptions.daemon}`; 124 | } 125 | 126 | if (proxyOptions.logPath && !path.isAbsolute(proxyOptions.logPath)) { 127 | proxyOptions.logPath = 128 | path.resolve(process.cwd(), proxyOptions.logPath); 129 | } 130 | 131 | return { 132 | generalOptions, proxyOptions, invalidOption, 133 | }; 134 | } 135 | 136 | function readConfig(_filePath) { 137 | if (!_filePath) { 138 | return null; 139 | } 140 | 141 | const filePath = path.resolve(process.cwd(), _filePath); 142 | 143 | try { 144 | accessSync(filePath); 145 | } catch (e) { 146 | throw new Error(`failed to find config file in: ${filePath}`); 147 | } 148 | 149 | return JSON.parse(readFileSync(filePath)); 150 | } 151 | 152 | /** 153 | * Transform domain && ipv6 to ipv4. 154 | */ 155 | export function resolveServerAddr(config, next) { 156 | const { serverAddr } = config.proxyOptions; 157 | 158 | if (isV4Format(serverAddr)) { 159 | next(null, config); 160 | } else { 161 | lookup(serverAddr, (err, addresses) => { 162 | if (err) { 163 | next(new Error(`failed to resolve 'serverAddr': ${serverAddr}`), config); 164 | } else { 165 | // NOTE: mutate data 166 | config.proxyOptions.serverAddr = addresses; // eslint-disable-line 167 | next(null, config); 168 | } 169 | }); 170 | } 171 | } 172 | 173 | export function getDefaultProxyOptions() { 174 | return Object.assign({}, DEFAULT_CONFIG); 175 | } 176 | 177 | export function getConfig(argv = [], arg1, arg2) { 178 | let doNotResolveIpv6 = arg1; 179 | let next = arg2; 180 | 181 | if (!arg2) { 182 | doNotResolveIpv6 = false; 183 | next = arg1; 184 | } 185 | 186 | const { generalOptions, proxyOptions, invalidOption } = getArgvOptions(argv); 187 | const specificFileConfig = readConfig(proxyOptions.configFilePath) || fileConfig; 188 | const config = { 189 | generalOptions, 190 | invalidOption, 191 | proxyOptions: Object.assign({}, DEFAULT_CONFIG, specificFileConfig, proxyOptions), 192 | }; 193 | 194 | if (doNotResolveIpv6) { 195 | next(null, config); 196 | return; 197 | } 198 | 199 | resolveServerAddr(config, next); 200 | } 201 | -------------------------------------------------------------------------------- /src/createUDPRelay.js: -------------------------------------------------------------------------------- 1 | import dgram from 'dgram'; 2 | import LRU from 'lru-cache'; 3 | import ip from 'ip'; 4 | import { getDstInfoFromUDPMsg, sendDgram, closeSilently } from './utils'; 5 | import * as encryptor from './encryptor'; 6 | 7 | // SOCKS5 UDP Request 8 | // +----+------+------+----------+----------+----------+ 9 | // |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | 10 | // +----+------+------+----------+----------+----------+ 11 | // | 2 | 1 | 1 | Variable | 2 | Variable | 12 | // +----+------+------+----------+----------+----------+ 13 | // 14 | // SOCKS5 UDP Response 15 | // +----+------+------+----------+----------+----------+ 16 | // |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | 17 | // +----+------+------+----------+----------+----------+ 18 | // | 2 | 1 | 1 | Variable | 2 | Variable | 19 | // +----+------+------+----------+----------+----------+ 20 | // 21 | // UDP Request (before encrypted) 22 | // +------+----------+----------+----------+ 23 | // | ATYP | DST.ADDR | DST.PORT | DATA | 24 | // +------+----------+----------+----------+ 25 | // | 1 | Variable | 2 | Variable | 26 | // +------+----------+----------+----------+ 27 | // 28 | // UDP Response (before encrypted) 29 | // +------+----------+----------+----------+ 30 | // | ATYP | DST.ADDR | DST.PORT | DATA | 31 | // +------+----------+----------+----------+ 32 | // | 1 | Variable | 2 | Variable | 33 | // +------+----------+----------+----------+ 34 | // 35 | // UDP Request and Response (after encrypted) 36 | // +-------+--------------+ 37 | // | IV | PAYLOAD | 38 | // +-------+--------------+ 39 | // | Fixed | Variable | 40 | // +-------+--------------+ 41 | 42 | const NAME = 'udp_relay'; 43 | const LRU_OPTIONS = { 44 | max: 1000, 45 | maxAge: 10 * 1000, 46 | dispose: (key, socket) => { 47 | // close socket if it's not closed 48 | if (socket) { 49 | socket.close(); 50 | } 51 | }, 52 | }; 53 | const SOCKET_TYPE = ['udp4', 'udp6']; 54 | 55 | function getIndex({ address, port }, { dstAddrStr, dstPortNum }) { 56 | return `${address}:${port}_${dstAddrStr}:${dstPortNum}`; 57 | } 58 | 59 | function createClient(logger, { atyp /* , dstAddr, dstPort */ }, onMsg, onClose) { 60 | const udpType = (atyp === 1 ? 'udp4' : 'udp6'); 61 | const socket = dgram.createSocket(udpType); 62 | 63 | socket.on('message', onMsg); 64 | 65 | socket.on('error', (e) => { 66 | logger.warn(`${NAME} client socket gets error: ${e.message}`); 67 | }); 68 | 69 | socket.on('close', onClose); 70 | 71 | return socket; 72 | } 73 | 74 | function createUDPRelaySocket(udpType, config, isServer, logger) { 75 | const { 76 | localPort, serverPort, 77 | password, method, 78 | } = config; 79 | const serverAddr = udpType === 'udp4' ? config.serverAddr : config.serverAddrIPv6; 80 | 81 | const encrypt = encryptor.encrypt.bind(null, password, method); 82 | const decrypt = encryptor.decrypt.bind(null, password, method); 83 | const socket = dgram.createSocket(udpType); 84 | const cache = new LRU(Object.assign({}, LRU_OPTIONS, { 85 | maxAge: config.timeout * 1000, 86 | })); 87 | const listenPort = (isServer ? serverPort : localPort); 88 | 89 | socket.on('message', (_msg, rinfo) => { 90 | const msg = isServer ? decrypt(_msg) : _msg; 91 | const frag = msg[2]; 92 | 93 | if (frag !== 0) { 94 | // drop those datagram that using frag 95 | return; 96 | } 97 | 98 | const dstInfo = getDstInfoFromUDPMsg(msg, isServer); 99 | const dstAddrStr = ip.toString(dstInfo.dstAddr); 100 | const dstPortNum = dstInfo.dstPort.readUInt16BE(); 101 | const index = getIndex(rinfo, { dstAddrStr, dstPortNum }); 102 | 103 | logger.debug(`${NAME} receive message: ${msg.toString('hex')}`); 104 | 105 | let client = cache.get(index); 106 | 107 | if (!client) { 108 | client = createClient(logger, dstInfo, (msgStream) => { 109 | // socket on message 110 | const incomeMsg = (isServer ? encrypt(msgStream) : decrypt(msgStream)); 111 | sendDgram(socket, incomeMsg, rinfo.port, rinfo.address); 112 | }, () => { 113 | // socket on close 114 | cache.del(index); 115 | }); 116 | cache.set(index, client); 117 | } 118 | 119 | if (isServer) { 120 | sendDgram( 121 | client, msg.slice(dstInfo.totalLength), 122 | dstPortNum, dstAddrStr 123 | ); 124 | } else { 125 | sendDgram( 126 | client, 127 | // skip RSV and FLAG 128 | encrypt(msg.slice(3)), 129 | serverPort, serverAddr 130 | ); 131 | } 132 | }); 133 | 134 | socket.on('error', (err) => { 135 | logger.error(`${NAME} socket err: ${err.message}`); 136 | socket.close(); 137 | }); 138 | 139 | socket.on('close', () => { 140 | cache.reset(); 141 | }); 142 | 143 | socket.bind(listenPort, () => { 144 | logger.verbose(`${NAME} is listening on: ${listenPort}`); 145 | }); 146 | 147 | return socket; 148 | } 149 | 150 | function close(sockets) { 151 | sockets.forEach((socket) => { 152 | closeSilently(socket); 153 | }); 154 | } 155 | 156 | export default function createUDPRelay(config, isServer, logger) { 157 | const sockets = SOCKET_TYPE.map(udpType => 158 | createUDPRelaySocket(udpType, config, isServer, logger)); 159 | 160 | return { 161 | sockets, 162 | close: close.bind(null, sockets), 163 | }; 164 | } 165 | -------------------------------------------------------------------------------- /src/daemon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ``` 3 | * $ node lib/daemon local -d restart 4 | * ``` 5 | * 6 | * ``` 7 | * $ node lib/daemon server -d restart -k abc 8 | * ``` 9 | */ 10 | import { join } from 'path'; 11 | import { fork } from 'child_process'; 12 | import { createLogger } from './logger'; 13 | import { getConfig } from './cli'; 14 | import { deletePidFile } from './pid'; 15 | import { record, stopRecord } from './recordMemoryUsage'; 16 | import { createSafeAfterHandler, safelyKillChild } from './utils'; 17 | 18 | const NAME = 'daemon'; 19 | // TODO: 20 | // const MAX_RESTART_TIME = 5; 21 | const MAX_RESTART_TIME = 1; 22 | 23 | let child = null; 24 | let logger; 25 | let shouldStop = false; 26 | 27 | // eslint-disable-next-line 28 | export const FORK_FILE_PATH = { 29 | local: join(__dirname, 'ssLocal'), 30 | server: join(__dirname, 'ssServer'), 31 | }; 32 | 33 | function daemon(type, config, filePath, shouldRecordServerMemory, _restartTime) { 34 | let restartTime = _restartTime || 0; 35 | 36 | child = fork(filePath); 37 | 38 | if (shouldRecordServerMemory) { 39 | child.on('message', record); 40 | } 41 | 42 | child.send(config); 43 | 44 | setTimeout(() => { 45 | restartTime = 0; 46 | }, 60 * 1000); 47 | 48 | child.on('exit', () => { 49 | if (shouldStop) { 50 | return; 51 | } 52 | 53 | logger.warn(`${NAME}: process exit.`); 54 | 55 | safelyKillChild(child, 'SIGKILL'); 56 | 57 | if (restartTime < MAX_RESTART_TIME) { 58 | daemon(type, config, filePath, shouldRecordServerMemory, restartTime + 1); 59 | } else { 60 | logger.error( 61 | `${NAME}: restarted too many times, will close.`, 62 | createSafeAfterHandler(logger, () => { 63 | deletePidFile(type); 64 | process.exit(1); 65 | }) 66 | ); 67 | } 68 | }); 69 | } 70 | 71 | if (module === require.main) { 72 | const type = process.argv[2]; 73 | const argv = process.argv.slice(3); 74 | 75 | getConfig(argv, (err, config) => { 76 | const { proxyOptions } = config; 77 | // eslint-disable-next-line 78 | const shouldRecordServerMemory = proxyOptions['_recordMemoryUsage'] && type === 'server'; 79 | 80 | logger = createLogger( 81 | proxyOptions.level, 82 | null, 83 | // LOG_NAMES.DAEMON, 84 | false 85 | ); 86 | 87 | if (err) { 88 | logger.error(`${NAME}: ${err.message}`); 89 | } 90 | 91 | daemon(type, proxyOptions, FORK_FILE_PATH[type], shouldRecordServerMemory); 92 | 93 | process.on('SIGHUP', () => { 94 | shouldStop = true; 95 | 96 | if (child) { 97 | safelyKillChild(child, 'SIGKILL'); 98 | } 99 | 100 | deletePidFile(type); 101 | 102 | if (shouldRecordServerMemory) { 103 | stopRecord(); 104 | } 105 | 106 | process.exit(0); 107 | }); 108 | 109 | process.on('uncaughtException', (uncaughtErr) => { 110 | logger.error(`${NAME} get error:\n${uncaughtErr.stack}`, 111 | createSafeAfterHandler(logger, () => { 112 | process.exit(1); 113 | })); 114 | }); 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /src/defaultConfig.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | // proxy options 4 | const DEFAULT_CONFIG = { 5 | serverAddr: '0.0.0.0', 6 | serverPort: 8083, 7 | localAddr: '127.0.0.1', 8 | localPort: 1080, 9 | password: 'YOUR_PASSWORD_HERE', 10 | pacServerPort: 8090, 11 | timeout: 600, 12 | method: 'aes-128-cfb', 13 | level: 'warn', 14 | logPath: path.resolve(__dirname, '../logs'), 15 | 16 | // ipv6 17 | localAddrIPv6: '::1', 18 | serverAddrIPv6: '::1', 19 | 20 | // dev options 21 | _recordMemoryUsage: false, 22 | }; 23 | 24 | export default DEFAULT_CONFIG; 25 | -------------------------------------------------------------------------------- /src/encryptor.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | const hasOwnProperty = {}.hasOwnProperty; 4 | 5 | // directly exported from the original nodejs implementation 6 | const cryptoParamLength = { 7 | 'aes-128-cfb': [16, 16], 8 | 'aes-192-cfb': [24, 16], 9 | 'aes-256-cfb': [32, 16], 10 | 'bf-cfb': [16, 8], 11 | 'camellia-128-cfb': [16, 16], 12 | 'camellia-192-cfb': [24, 16], 13 | 'camellia-256-cfb': [32, 16], 14 | 'cast5-cfb': [16, 8], 15 | 'des-cfb': [8, 8], 16 | 'idea-cfb': [16, 8], 17 | 'rc2-cfb': [16, 8], 18 | rc4: [16, 0], 19 | 'rc4-md5': [16, 16], 20 | 'seed-cfb': [16, 16], 21 | }; 22 | 23 | const keyCache = {}; 24 | 25 | export function getParamLength(methodName) { 26 | return cryptoParamLength[methodName]; 27 | } 28 | 29 | function getMD5Hash(data) { 30 | return crypto.createHash('md5').update(data).digest(); 31 | } 32 | 33 | export function generateKey(methodName, secret) { 34 | const secretBuf = new Buffer(secret, 'utf8'); 35 | const tokens = []; 36 | const keyLength = getParamLength(methodName)[0]; 37 | const cacheIndex = `${methodName}_${secret}`; 38 | 39 | let i = 0; 40 | let hash; 41 | let length = 0; 42 | 43 | if (hasOwnProperty.call(keyCache, cacheIndex)) { 44 | return keyCache[cacheIndex]; 45 | } 46 | 47 | if (!keyLength) { 48 | // TODO: catch error 49 | throw new Error('unsupported method'); 50 | } 51 | 52 | while (length < keyLength) { 53 | hash = getMD5Hash((i === 0) ? secretBuf : Buffer.concat([tokens[i - 1], secretBuf])); 54 | tokens.push(hash); 55 | i += 1; 56 | length += hash.length; 57 | } 58 | 59 | hash = Buffer.concat(tokens).slice(0, keyLength); 60 | 61 | keyCache[cacheIndex] = hash; 62 | 63 | return hash; 64 | } 65 | 66 | export function createCipher(secret, methodName, initialData, _iv) { 67 | const key = generateKey(methodName, secret); 68 | const iv = _iv || crypto.randomBytes(getParamLength(methodName)[1]); 69 | const cipher = crypto.createCipheriv(methodName, key, iv); 70 | 71 | return { 72 | cipher, 73 | data: Buffer.concat([iv, cipher.update(initialData)]), 74 | }; 75 | } 76 | 77 | export function createDecipher(secret, methodName, initialData) { 78 | const ivLength = getParamLength(methodName)[1]; 79 | const iv = initialData.slice(0, ivLength); 80 | 81 | if (iv.length !== ivLength) { 82 | return null; 83 | } 84 | 85 | const key = generateKey(methodName, secret); 86 | const decipher = crypto.createDecipheriv(methodName, key, iv); 87 | const data = decipher.update(initialData.slice(ivLength)); 88 | 89 | return { 90 | decipher, 91 | data, 92 | }; 93 | } 94 | 95 | export function encrypt(secret, methodName, data, _iv) { 96 | return createCipher(secret, methodName, data, _iv).data; 97 | } 98 | 99 | export function decrypt(secret, methodName, data) { 100 | return createDecipher(secret, methodName, data).data; 101 | } 102 | -------------------------------------------------------------------------------- /src/filter.js: -------------------------------------------------------------------------------- 1 | import { getDstStr } from './utils'; 2 | 3 | // TODO: 4 | let defaultDenyList = [ 5 | // /google/, 6 | ]; 7 | let denyListLength = defaultDenyList.length; 8 | 9 | export function setDenyList(denyList) { 10 | defaultDenyList = denyList; 11 | denyListLength = denyList.length; 12 | } 13 | 14 | export function filter(dstInfo) { 15 | const dstStr = getDstStr(dstInfo); 16 | 17 | let i; 18 | 19 | for (i = 0; i < denyListLength; i += 1) { 20 | if (defaultDenyList[i].test(dstStr)) { 21 | return false; 22 | } 23 | } 24 | 25 | return true; 26 | } 27 | -------------------------------------------------------------------------------- /src/gfwlistUtils.js: -------------------------------------------------------------------------------- 1 | // NOTE: do not use these in local server 2 | import { join } from 'path'; 3 | import { request } from 'https'; 4 | import { request as httpRequest } from 'http'; 5 | import { parse } from 'url'; 6 | import { writeFile, readFileSync } from 'fs'; 7 | import { minify } from 'uglify-js'; 8 | 9 | export const GFWLIST_FILE_PATH = join(__dirname, '../pac/gfwlist.txt'); 10 | const DEFAULT_CONFIG = { 11 | localAddr: '127.0.0.1', 12 | localPort: '1080', 13 | }; 14 | const TARGET_URL = 'https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt'; 15 | const LINE_DELIMER = ['\r\n', '\r', '\n']; 16 | const MINIFY_OPTIONS = { 17 | warnings: false, 18 | }; 19 | 20 | let readLineLastContent = null; 21 | let readLineLastIndex = 0; 22 | 23 | function clear() { 24 | readLineLastContent = null; 25 | readLineLastIndex = 0; 26 | } 27 | 28 | function base64ToString(base64String) { 29 | return Buffer.from(base64String, 'base64').toString(); 30 | } 31 | 32 | // function stringToBase64(string) { 33 | // return (new Buffer(base64String, 'base64')).toString('base64'); 34 | // } 35 | 36 | export function readLine(text, shouldStrip) { 37 | let startIndex = 0; 38 | let i = null; 39 | let delimer = null; 40 | 41 | if (text === readLineLastContent) { 42 | startIndex = readLineLastIndex; 43 | } else { 44 | readLineLastContent = text; 45 | } 46 | 47 | LINE_DELIMER.forEach((char) => { 48 | const index = text.indexOf(char, startIndex); 49 | 50 | if (index !== -1 && (i === null || index < i)) { 51 | i = index; 52 | delimer = char; 53 | } 54 | }); 55 | 56 | if (i !== null) { 57 | readLineLastIndex = i + delimer.length; 58 | return shouldStrip ? text.slice(startIndex, i) : text.slice(startIndex, readLineLastIndex); 59 | } 60 | 61 | readLineLastIndex = 0; 62 | return null; 63 | } 64 | 65 | readLine.clear = clear; 66 | 67 | function shouldDropLine(line) { 68 | // NOTE: It's possible that gfwlist has rules that is a too long 69 | // regexp that may crush proxies like 'SwitchySharp' so we would 70 | // drop these rules here. 71 | return !line || line[0] === '!' || line[0] === '[' || line.length > 100; 72 | } 73 | 74 | const slashReg = /\//g; 75 | 76 | function encode(line) { 77 | return line.replace(slashReg, '\\/'); 78 | } 79 | 80 | export function createListArrayString(text) { 81 | const list = []; 82 | let line = readLine(text, true); 83 | 84 | while (line !== null) { 85 | if (!shouldDropLine(line)) { 86 | list.push(`"${encode(line)}"`); 87 | } 88 | 89 | line = readLine(text, true); 90 | } 91 | 92 | return `var rules = [${list.join(',\n')}];`; 93 | } 94 | 95 | export function createPACFileContent(text, { localAddr, localPort }) { 96 | const HOST = `${localAddr}:${localPort}`; 97 | const readFileOptions = { encoding: 'utf8' }; 98 | const userRulesString = readFileSync(join(__dirname, '../pac/user.txt'), readFileOptions); 99 | const rulesString = createListArrayString(`${userRulesString}\n${text}`); 100 | const SOCKS_STR = `var proxy = "SOCKS5 ${HOST}; SOCKS ${HOST}; DIRECT;";`; 101 | const matcherString = readFileSync(join(__dirname, '../vendor/ADPMatcher.js'), readFileOptions); 102 | 103 | return `${SOCKS_STR}\n${rulesString}\n${matcherString}`; 104 | } 105 | 106 | export function requestGFWList(targetURL, next) { 107 | const options = parse(targetURL); 108 | const requestMethod = (options.protocol.indexOf('https') >= 0 ? request : httpRequest); 109 | 110 | const req = requestMethod(options, (res) => { 111 | let data = null; 112 | 113 | res.on('data', (chunk) => { 114 | data = data ? Buffer.concat([data, chunk]) : chunk; 115 | }); 116 | 117 | res.on('end', () => { 118 | // gfwlist.txt use utf8 encoded content to present base64 content 119 | const listText = data.toString(); 120 | next(null, listText); 121 | }); 122 | }); 123 | 124 | req.on('error', (err) => { 125 | next(err); 126 | }); 127 | 128 | req.end(); 129 | } 130 | 131 | function minifyCode(code) { 132 | return minify(code, MINIFY_OPTIONS).code; 133 | } 134 | 135 | // TODO: async this 136 | export function getPACFileContent(_config) { 137 | const config = _config || DEFAULT_CONFIG; 138 | const listText = base64ToString(readFileSync(GFWLIST_FILE_PATH, { encoding: 'utf8' })); 139 | 140 | return minifyCode(createPACFileContent(listText, config)); 141 | } 142 | 143 | function writeGFWList(listBuffer, next) { 144 | writeFile(GFWLIST_FILE_PATH, listBuffer, next); 145 | } 146 | 147 | export function updateGFWList(...args) { 148 | let targetURL = TARGET_URL; 149 | let next; 150 | 151 | if (args.length === 1) { 152 | next = args[0]; 153 | } else if (args.length === 2) { 154 | targetURL = args[0]; 155 | next = args[1]; 156 | } 157 | 158 | requestGFWList(targetURL, (err, listBuffer) => { 159 | if (err) { 160 | next(err); 161 | return; 162 | } 163 | 164 | writeGFWList(listBuffer, next); 165 | }); 166 | } 167 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { startServer as createServer } from './ssServer'; 2 | export { startServer as createClient } from './ssLocal'; 3 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import { resolve } from 'path'; 3 | import { mkdirIfNotExistSync } from './utils'; 4 | 5 | export const LOG_NAMES = { 6 | LOCAL: 'local.log', 7 | SERVER: 'server.log', 8 | DAEMON: 'daemon.log', 9 | }; 10 | 11 | const DEFAULT_LEVEL = 'warn'; 12 | const DEFAULT_COMMON_OPTIONS = { 13 | colorize: true, 14 | timestamp: true, 15 | }; 16 | 17 | // TODO: to be refactored 18 | function createLogData(level, filename, willLogToConsole, notLogToFile) { 19 | const transports = []; 20 | 21 | if (filename && !notLogToFile) { 22 | transports.push( 23 | new winston.transports.File(Object.assign( 24 | DEFAULT_COMMON_OPTIONS, { 25 | level, 26 | filename, 27 | } 28 | )) 29 | ); 30 | } 31 | 32 | if (willLogToConsole) { 33 | transports.push( 34 | new winston.transports.Console(Object.assign(DEFAULT_COMMON_OPTIONS, { 35 | level, 36 | })) 37 | ); 38 | } 39 | 40 | return { 41 | transports, 42 | }; 43 | } 44 | 45 | export function createLogger( 46 | proxyOptions, 47 | logName, 48 | willLogToConsole = false, 49 | notLogToFile = false, 50 | ) { 51 | const { level = DEFAULT_LEVEL, logPath } = (proxyOptions || {}); 52 | 53 | if (logPath) { 54 | mkdirIfNotExistSync(logPath); 55 | } 56 | 57 | const fileName = logPath ? resolve(logPath, logName) : null; 58 | return new winston.Logger(createLogData( 59 | level, fileName, willLogToConsole, notLogToFile, 60 | )); 61 | } 62 | -------------------------------------------------------------------------------- /src/pacServer.js: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { getPACFileContent } from './gfwlistUtils'; 3 | 4 | const NAME = 'pac_server'; 5 | 6 | // TODO: async this 7 | // eslint-disable-next-line 8 | export function createPACServer(config, logger) { 9 | const pacFileContent = getPACFileContent(config); 10 | const HOST = `${config.localAddr}:${config.pacServerPort}`; 11 | 12 | const server = createServer((req, res) => { 13 | res.write(pacFileContent); 14 | res.end(); 15 | }); 16 | 17 | server.on('error', (err) => { 18 | logger.error(`${NAME} got error: ${err.stack}`); 19 | }); 20 | 21 | server.listen(config.pacServerPort); 22 | 23 | if (logger) { 24 | logger.verbose(`${NAME} is listening on ${HOST}`); 25 | } 26 | 27 | return server; 28 | } 29 | -------------------------------------------------------------------------------- /src/pid.js: -------------------------------------------------------------------------------- 1 | import { unlinkSync, writeFileSync, accessSync, readFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { mkdirIfNotExistSync } from './utils'; 4 | 5 | export const TMP_PATH = join(__dirname, '../tmp'); 6 | 7 | export function getFileName(type) { 8 | switch (type) { 9 | case 'local': 10 | return join(TMP_PATH, 'local.pid'); 11 | case 'server': 12 | return join(TMP_PATH, 'server.pid'); 13 | default: 14 | throw new Error(`invalid 'type' of filename ${type}`); 15 | } 16 | } 17 | 18 | export function getPid(type) { 19 | const fileName = getFileName(type); 20 | 21 | mkdirIfNotExistSync(TMP_PATH); 22 | 23 | try { 24 | accessSync(fileName); 25 | } catch (e) { 26 | return null; 27 | } 28 | 29 | return readFileSync(fileName).toString('utf8'); 30 | } 31 | 32 | export function writePidFile(type, pid) { 33 | mkdirIfNotExistSync(TMP_PATH); 34 | 35 | writeFileSync(getFileName(type), pid); 36 | } 37 | 38 | export function deletePidFile(type) { 39 | mkdirIfNotExistSync(TMP_PATH); 40 | 41 | try { 42 | unlinkSync(getFileName(type)); 43 | } catch (err) { 44 | // alreay unlinked 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/pm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Daemon ss processes. 3 | * 1. start, stop, restart 4 | * 2. know the previous process running status 5 | * 3. log and logrotate 6 | */ 7 | import pm2 from 'pm2'; 8 | import path from 'path'; 9 | import { getFileName } from './pid'; 10 | import { stringifyProxyOptions } from './config'; 11 | 12 | // eslint-disable-next-line 13 | const log = console.log 14 | 15 | // const LOG_ROTATE_OPTIONS = { 16 | // maxSize: '1KB', 17 | // retain: 7, 18 | // workerInterval: 60, 19 | // rotateInterval: '*/1 * * * *', 20 | // }; 21 | 22 | export const FORK_FILE_PATH = { 23 | local: path.join(__dirname, 'ssLocal.js'), 24 | server: path.join(__dirname, 'ssServer.js'), 25 | }; 26 | 27 | const pm2ProcessName = { 28 | local: 'ssLocal', 29 | server: 'ssServer', 30 | }; 31 | 32 | function getArgs(extralProxyOptions) { 33 | if (typeof extralProxyOptions === 'string') { 34 | return extralProxyOptions; 35 | } 36 | 37 | // TODO: support "stringify" 38 | if (typeof extralProxyOptions === 'object') { 39 | return stringifyProxyOptions(extralProxyOptions); 40 | } 41 | 42 | return process.argv.slice(2).join(' '); 43 | } 44 | 45 | function disconnect() { 46 | return new Promise((resolve) => { 47 | pm2.disconnect((err) => { 48 | if (err) { 49 | throw err; 50 | } 51 | 52 | resolve(); 53 | }); 54 | }); 55 | } 56 | 57 | function connect() { 58 | return new Promise((resolve) => { 59 | pm2.connect((err) => { 60 | if (err) { 61 | throw err; 62 | } 63 | 64 | resolve(); 65 | }); 66 | }); 67 | } 68 | 69 | function handleError(err) { 70 | return disconnect().then(() => { 71 | // TODO: 72 | // eslint-disable-next-line 73 | console.error(err); 74 | }); 75 | } 76 | 77 | function getPM2Config(type, extralProxyOptions) { 78 | return connect().then(() => { 79 | const filePath = FORK_FILE_PATH[type]; 80 | const pidFileName = getFileName(type); 81 | const name = pm2ProcessName[type]; 82 | 83 | const pm2Config = { 84 | name, 85 | script: filePath, 86 | exec_mode: 'fork', 87 | instances: 1, 88 | output: path.resolve(__dirname, `../logs/${name}.log`), 89 | error: path.resolve(__dirname, `../logs/${name}.err`), 90 | pid: pidFileName, 91 | minUptime: 2000, 92 | maxRestarts: 3, 93 | args: getArgs(extralProxyOptions), 94 | }; 95 | 96 | return { 97 | pm2Config, 98 | }; 99 | }); 100 | } 101 | 102 | function _start(type, extralProxyOptions) { 103 | return getPM2Config(type, extralProxyOptions) 104 | .then(({ pm2Config }) => new Promise((resolve) => { 105 | pm2.start(pm2Config, (err, apps) => { 106 | if (err) { 107 | throw err; 108 | } 109 | 110 | log('start'); 111 | resolve(apps); 112 | }); 113 | })) 114 | .then(() => disconnect()); 115 | } 116 | 117 | function getRunningInfo(name) { 118 | return new Promise((resolve) => { 119 | pm2.describe(name, (err, descriptions) => { 120 | if (err) { 121 | throw err; 122 | } 123 | 124 | // TODO: there should not be more than one process 125 | // “online”, “stopping”, 126 | // “stopped”, “launching”, 127 | // “errored”, or “one-launch-status” 128 | const status = descriptions.length > 0 129 | && descriptions[0].pm2_env.status !== 'stopped' 130 | && descriptions[0].pm2_env.status !== 'errored'; 131 | 132 | resolve(status); 133 | }); 134 | }); 135 | } 136 | 137 | function _stop(type, extralProxyOptions) { 138 | let config = null; 139 | 140 | return getPM2Config(type, extralProxyOptions) 141 | .then((conf) => { 142 | config = conf; 143 | const { name } = config.pm2Config; 144 | return getRunningInfo(name); 145 | }).then((isRunning) => { 146 | const { pm2Config } = config; 147 | const { name } = pm2Config; 148 | 149 | if (!isRunning) { 150 | log('already stopped'); 151 | return; 152 | } 153 | 154 | // eslint-disable-next-line 155 | return new Promise((resolve) => { 156 | pm2.stop(name, (err) => { 157 | if (err && err.message !== 'process name not found') { 158 | throw err; 159 | } 160 | 161 | log('stop'); 162 | resolve(); 163 | }); 164 | }); 165 | }) 166 | .then(() => disconnect()); 167 | } 168 | 169 | /** 170 | * @public 171 | * @param {[type]} args [description] 172 | * @return {[type]} [description] 173 | */ 174 | export function start(...args) { 175 | return _start(...args).catch(handleError); 176 | } 177 | 178 | /** 179 | * @public 180 | * @param {[type]} args [description] 181 | * @return {[type]} [description] 182 | */ 183 | export function stop(...args) { 184 | return _stop(...args).catch(handleError); 185 | } 186 | 187 | /** 188 | * @public 189 | * @param {[type]} args [description] 190 | * @return {[type]} [description] 191 | */ 192 | export function restart(...args) { 193 | return _stop(...args).then(() => _start(...args)).catch(handleError); 194 | } 195 | 196 | // if (module === require.main) { 197 | // restart('local', { 198 | // password: 'holic123', 199 | // serverAddr: 'kr.oyyd.net', 200 | // }); 201 | // } 202 | -------------------------------------------------------------------------------- /src/recordMemoryUsage.js: -------------------------------------------------------------------------------- 1 | // NOTE: do not use this in production 2 | import { writeFileSync } from 'fs'; 3 | import { join } from 'path'; 4 | 5 | export const INTERVAL_TIME = 1000; 6 | 7 | let data = null; 8 | 9 | export function record(frame) { 10 | data = data || []; 11 | 12 | data.push(frame); 13 | } 14 | 15 | export function stopRecord() { 16 | if (data) { 17 | writeFileSync(join(__dirname, '../logs/memory.json'), JSON.stringify(data)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ssLocal.js: -------------------------------------------------------------------------------- 1 | import ip from 'ip'; 2 | import { createServer as _createServer, connect } from 'net'; 3 | import { getDstInfo, writeOrPause, getDstStr, closeSilently, 4 | createSafeAfterHandler } from './utils'; 5 | import { createLogger, LOG_NAMES } from './logger'; 6 | import { createCipher, createDecipher } from './encryptor'; 7 | import { createPACServer } from './pacServer'; 8 | import createUDPRelay from './createUDPRelay'; 9 | import { createAuthInfo, validate } from './auth'; 10 | import { getConfig } from './config'; 11 | 12 | const NAME = 'ss_local'; 13 | 14 | let logger; 15 | 16 | function handleMethod(connection, data, authInfo) { 17 | // +----+----------+----------+ 18 | // |VER | NMETHODS | METHODS | 19 | // +----+----------+----------+ 20 | // | 1 | 1 | 1 to 255 | 21 | // +----+----------+----------+ 22 | const { forceAuth } = authInfo; 23 | const buf = new Buffer(2); 24 | 25 | let method = -1; 26 | 27 | if (forceAuth && data.indexOf(0x02, 2) >= 0) { 28 | method = 2; 29 | } 30 | 31 | if (!forceAuth && data.indexOf(0x00, 2) >= 0) { 32 | method = 0; 33 | } 34 | 35 | // allow `no authetication` or any usename/password 36 | if (method === -1) { 37 | // logger.warn(`unsupported method: ${data.toString('hex')}`); 38 | buf.writeUInt16BE(0x05FF); 39 | connection.write(buf); 40 | connection.end(); 41 | return -1; 42 | } 43 | 44 | buf.writeUInt16BE(0x0500); 45 | connection.write(buf); 46 | 47 | return method === 0 ? 1 : 3; 48 | } 49 | 50 | function fetchUsernamePassword(data) { 51 | // suppose all VER is 0x01 52 | if (!(data instanceof Buffer)) { 53 | return null; 54 | } 55 | 56 | const ulen = data[1]; 57 | const username = data.slice(2, ulen + 2).toString('ascii'); 58 | const plenStart = ulen + 2; 59 | const plen = data[plenStart]; 60 | const password = data.slice(plenStart + 1, plenStart + 1 + plen).toString('ascii'); 61 | 62 | return { username, password }; 63 | } 64 | 65 | function responseAuth(success, connection) { 66 | const buf = new Buffer(2); 67 | const toWrite = success ? 0x0100 : 0x0101; 68 | const nextProcedure = success ? 2 : -1; 69 | 70 | buf.writeUInt16BE(toWrite); 71 | connection.write(buf); 72 | connection.end(); 73 | 74 | return nextProcedure; 75 | } 76 | 77 | export function usernamePasswordAuthetication(connection, data, authInfo) { 78 | // +----+------+----------+------+----------+ 79 | // |VER | ULEN | UNAME | PLEN | PASSWD | 80 | // +----+------+----------+------+----------+ 81 | // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | 82 | // +----+------+----------+------+----------+ 83 | 84 | const usernamePassword = fetchUsernamePassword(data); 85 | 86 | if (!usernamePassword) { 87 | return responseAuth(false, connection); 88 | } 89 | 90 | const { username, password } = usernamePassword; 91 | 92 | if (!validate(authInfo, username, password)) { 93 | return responseAuth(false, connection); 94 | } 95 | 96 | return responseAuth(true, connection); 97 | } 98 | 99 | function handleRequest( 100 | connection, data, 101 | { serverAddr, serverPort, password, method, localAddr, localPort, 102 | localAddrIPv6 }, 103 | dstInfo, onConnect, onDestroy, isClientConnected 104 | ) { 105 | const cmd = data[1]; 106 | const clientOptions = { 107 | port: serverPort, 108 | host: serverAddr, 109 | }; 110 | const isUDPRelay = (cmd === 0x03); 111 | 112 | let repBuf; 113 | let tmp = null; 114 | let decipher = null; 115 | let decipheredData = null; 116 | let cipher = null; 117 | let cipheredData = null; 118 | 119 | if (cmd !== 0x01 && !isUDPRelay) { 120 | logger.warn(`unsupported cmd: ${cmd}`); 121 | return { 122 | stage: -1, 123 | }; 124 | } 125 | 126 | // prepare data 127 | 128 | // +----+-----+-------+------+----------+----------+ 129 | // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | 130 | // +----+-----+-------+------+----------+----------+ 131 | // | 1 | 1 | X'00' | 1 | Variable | 2 | 132 | // +----+-----+-------+------+----------+----------+ 133 | 134 | if (isUDPRelay) { 135 | const isUDP4 = dstInfo.atyp === 1; 136 | 137 | repBuf = new Buffer(4); 138 | repBuf.writeUInt32BE(isUDP4 ? 0x05000001 : 0x05000004); 139 | tmp = new Buffer(2); 140 | tmp.writeUInt16BE(localPort); 141 | repBuf = Buffer.concat([repBuf, ip.toBuffer(isUDP4 ? localAddr : localAddrIPv6), tmp]); 142 | 143 | connection.write(repBuf); 144 | 145 | return { 146 | stage: -1, 147 | }; 148 | } 149 | 150 | logger.verbose(`connecting: ${dstInfo.dstAddr.toString('utf8')}` 151 | + `:${dstInfo.dstPort.readUInt16BE()}`); 152 | 153 | repBuf = new Buffer(10); 154 | repBuf.writeUInt32BE(0x05000001); 155 | repBuf.writeUInt32BE(0x00000000, 4, 4); 156 | repBuf.writeUInt16BE(0, 8, 2); 157 | 158 | tmp = createCipher(password, method, 159 | data.slice(3)); // skip VER, CMD, RSV 160 | cipher = tmp.cipher; 161 | cipheredData = tmp.data; 162 | 163 | // connect 164 | const clientToRemote = connect(clientOptions, () => { 165 | onConnect(); 166 | }); 167 | 168 | clientToRemote.on('data', (remoteData) => { 169 | if (!decipher) { 170 | tmp = createDecipher(password, method, remoteData); 171 | if (!tmp) { 172 | logger.warn(`${NAME} get invalid msg`); 173 | onDestroy(); 174 | return; 175 | } 176 | decipher = tmp.decipher; 177 | decipheredData = tmp.data; 178 | } else { 179 | decipheredData = decipher.update(remoteData); 180 | } 181 | 182 | if (isClientConnected()) { 183 | writeOrPause(clientToRemote, connection, decipheredData); 184 | } else { 185 | clientToRemote.destroy(); 186 | } 187 | }); 188 | 189 | clientToRemote.on('drain', () => { 190 | connection.resume(); 191 | }); 192 | 193 | clientToRemote.on('end', () => { 194 | connection.end(); 195 | }); 196 | 197 | clientToRemote.on('error', (e) => { 198 | logger.warn('ssLocal error happened in clientToRemote when' 199 | + ` connecting to ${getDstStr(dstInfo)}: ${e.message}`); 200 | 201 | onDestroy(); 202 | }); 203 | 204 | clientToRemote.on('close', (e) => { 205 | if (e) { 206 | connection.destroy(); 207 | } else { 208 | connection.end(); 209 | } 210 | }); 211 | 212 | // write 213 | connection.write(repBuf); 214 | 215 | writeOrPause(connection, clientToRemote, cipheredData); 216 | 217 | return { 218 | stage: 2, 219 | cipher, 220 | clientToRemote, 221 | }; 222 | } 223 | 224 | function handleConnection(config, connection) { 225 | const { authInfo } = config; 226 | 227 | let stage = 0; 228 | let clientToRemote; 229 | let tmp; 230 | let cipher; 231 | let dstInfo; 232 | let remoteConnected = false; 233 | let clientConnected = true; 234 | let timer = null; 235 | 236 | connection.on('data', (data) => { 237 | switch (stage) { 238 | case 0: 239 | stage = handleMethod(connection, data, authInfo); 240 | 241 | break; 242 | case 1: 243 | dstInfo = getDstInfo(data); 244 | 245 | if (!dstInfo) { 246 | logger.warn(`Failed to get 'dstInfo' from parsing data: ${data}`); 247 | connection.destroy(); 248 | return; 249 | } 250 | 251 | tmp = handleRequest( 252 | connection, data, config, dstInfo, 253 | () => { 254 | // after connected 255 | remoteConnected = true; 256 | }, 257 | () => { 258 | // get invalid msg or err happened 259 | if (remoteConnected) { 260 | remoteConnected = false; 261 | clientToRemote.destroy(); 262 | } 263 | 264 | if (clientConnected) { 265 | clientConnected = false; 266 | connection.destroy(); 267 | } 268 | }, 269 | () => clientConnected 270 | ); 271 | 272 | stage = tmp.stage; 273 | 274 | if (stage === 2) { 275 | clientToRemote = tmp.clientToRemote; 276 | cipher = tmp.cipher; 277 | } else { 278 | // udp relay 279 | clientConnected = false; 280 | connection.end(); 281 | } 282 | 283 | break; 284 | case 2: 285 | tmp = cipher.update(data); 286 | 287 | writeOrPause(connection, clientToRemote, tmp); 288 | 289 | break; 290 | case 3: 291 | // rfc 1929 username/password authetication 292 | stage = usernamePasswordAuthetication(connection, data, authInfo); 293 | break; 294 | default: 295 | } 296 | }); 297 | 298 | connection.on('drain', () => { 299 | if (remoteConnected) { 300 | clientToRemote.resume(); 301 | } 302 | }); 303 | 304 | connection.on('end', () => { 305 | clientConnected = false; 306 | if (remoteConnected) { 307 | clientToRemote.end(); 308 | } 309 | }); 310 | 311 | connection.on('close', (e) => { 312 | if (timer) { 313 | clearTimeout(timer); 314 | } 315 | 316 | clientConnected = false; 317 | 318 | if (remoteConnected) { 319 | if (e) { 320 | clientToRemote.destroy(); 321 | } else { 322 | clientToRemote.end(); 323 | } 324 | } 325 | }); 326 | 327 | connection.on('error', (e) => { 328 | logger.warn(`${NAME} error happened in client connection: ${e.message}`); 329 | }); 330 | 331 | timer = setTimeout(() => { 332 | if (clientConnected) { 333 | connection.destroy(); 334 | } 335 | 336 | if (remoteConnected) { 337 | clientToRemote.destroy(); 338 | } 339 | }, config.timeout * 1000); 340 | } 341 | 342 | function closeAll() { 343 | closeSilently(this.server); 344 | closeSilently(this.pacServer); 345 | 346 | this.udpRelay.close(); 347 | 348 | if (this.httpProxyServer) { 349 | this.httpProxyServer.close(); 350 | } 351 | } 352 | 353 | function createServer(config) { 354 | const server = _createServer(handleConnection.bind(null, config)); 355 | const udpRelay = createUDPRelay(config, false, logger); 356 | const pacServer = createPACServer(config, logger); 357 | 358 | server.on('close', () => { 359 | logger.warn(`${NAME} server closed`); 360 | }); 361 | 362 | server.on('error', (e) => { 363 | logger.error(`${NAME} server error: ${e.message}`); 364 | }); 365 | 366 | server.listen(config.localPort); 367 | 368 | logger.verbose(`${NAME} is listening on ${config.localAddr}:${config.localPort}`); 369 | 370 | return { 371 | server, 372 | udpRelay, 373 | pacServer, 374 | closeAll, 375 | }; 376 | } 377 | 378 | // eslint-disable-next-line 379 | export function startServer(config, willLogToConsole = false) { 380 | logger = logger || createLogger(config, LOG_NAMES.LOCAL, willLogToConsole); 381 | 382 | const { info, warn, error } = createAuthInfo(config); 383 | 384 | if (error) { 385 | logger.error(`${NAME} error: ${error}`); 386 | return null; 387 | } 388 | 389 | if (warn) { 390 | logger.warn(`${NAME}: ${warn}`); 391 | } 392 | 393 | return createServer(Object.assign({}, config, { 394 | authInfo: info, 395 | })); 396 | } 397 | 398 | if (module === require.main) { 399 | getConfig(process.argv.slice(2), (err, config) => { 400 | if (err) { 401 | throw err; 402 | } 403 | 404 | const { proxyOptions } = config; 405 | 406 | logger = createLogger( 407 | proxyOptions, 408 | LOG_NAMES.LOCAL, 409 | true, 410 | true, 411 | ); 412 | startServer(proxyOptions, false); 413 | }); 414 | 415 | process.on('uncaughtException', (err) => { 416 | logger.error(`${NAME} uncaughtException: ${err.stack} `, createSafeAfterHandler(logger, () => { 417 | process.exit(1); 418 | })); 419 | }); 420 | } 421 | -------------------------------------------------------------------------------- /src/ssServer.js: -------------------------------------------------------------------------------- 1 | import ip from 'ip'; 2 | import { createServer as _createServer, connect } from 'net'; 3 | import { getDstInfo, writeOrPause, createSafeAfterHandler } from './utils'; 4 | import { createLogger, LOG_NAMES } from './logger'; 5 | import { createCipher, createDecipher } from './encryptor'; 6 | import createUDPRelay from './createUDPRelay'; 7 | // import { INTERVAL_TIME } from './recordMemoryUsage'; 8 | import { getConfig } from './config'; 9 | 10 | const NAME = 'ss_server'; 11 | 12 | let logger; 13 | 14 | function createClientToDst( 15 | connection, data, 16 | password, method, 17 | onConnect, 18 | onDestroy, 19 | isLocalConnected, 20 | connectFunc, 21 | ) { 22 | const dstInfo = getDstInfo(data, true); 23 | 24 | let cipher = null; 25 | let tmp; 26 | let cipheredData; 27 | let preservedData = null; 28 | 29 | if (!dstInfo) { 30 | logger.warn(`${NAME} receive invalid msg. ` 31 | + 'local method/password doesn\'t accord with the server\'s?'); 32 | return null; 33 | } 34 | 35 | const clientOptions = { 36 | port: dstInfo.dstPort.readUInt16BE(), 37 | host: (dstInfo.atyp === 3 38 | ? dstInfo.dstAddr.toString('ascii') : ip.toString(dstInfo.dstAddr)), 39 | }; 40 | 41 | if (dstInfo.totalLength < data.length) { 42 | preservedData = data.slice(dstInfo.totalLength); 43 | } 44 | 45 | let clientToDst = null; 46 | 47 | if (connectFunc) { 48 | clientToDst = connectFunc(clientOptions, onConnect, data.slice(0, dstInfo.totalLength)); 49 | } else { 50 | clientToDst = connect(clientOptions, onConnect); 51 | } 52 | 53 | clientToDst.on('data', (clientData) => { 54 | if (!cipher) { 55 | tmp = createCipher(password, method, clientData); 56 | cipher = tmp.cipher; 57 | cipheredData = tmp.data; 58 | } else { 59 | cipheredData = cipher.update(clientData); 60 | } 61 | 62 | if (isLocalConnected()) { 63 | writeOrPause(clientToDst, connection, cipheredData); 64 | } else { 65 | clientToDst.destroy(); 66 | } 67 | }); 68 | 69 | clientToDst.on('drain', () => { 70 | connection.resume(); 71 | }); 72 | 73 | clientToDst.on('end', () => { 74 | if (isLocalConnected()) { 75 | connection.end(); 76 | } 77 | }); 78 | 79 | clientToDst.on('error', (e) => { 80 | logger.warn(`ssServer error happened when write to DST: ${e.message}`); 81 | onDestroy(); 82 | }); 83 | 84 | clientToDst.on('close', (e) => { 85 | if (isLocalConnected()) { 86 | if (e) { 87 | connection.destroy(); 88 | } else { 89 | connection.end(); 90 | } 91 | } 92 | }); 93 | 94 | return { 95 | clientToDst, preservedData, 96 | }; 97 | } 98 | 99 | function handleConnection(config, connection) { 100 | let stage = 0; 101 | let clientToDst = null; 102 | let decipher = null; 103 | let tmp; 104 | let data; 105 | let localConnected = true; 106 | let dstConnected = false; 107 | let timer = null; 108 | 109 | connection.on('data', (chunck) => { 110 | try { 111 | if (!decipher) { 112 | tmp = createDecipher(config.password, config.method, chunck); 113 | decipher = tmp.decipher; 114 | data = tmp.data; 115 | } else { 116 | data = decipher.update(chunck); 117 | } 118 | } catch (e) { 119 | logger.warn(`${NAME} receive invalid data`); 120 | return; 121 | } 122 | 123 | switch (stage) { 124 | case 0: 125 | // TODO: should pause? or preserve data? 126 | connection.pause(); 127 | 128 | tmp = createClientToDst( 129 | connection, 130 | data, 131 | config.password, 132 | config.method, 133 | () => { 134 | dstConnected = true; 135 | connection.resume(); 136 | }, 137 | () => { 138 | if (dstConnected) { 139 | dstConnected = false; 140 | clientToDst.destroy(); 141 | } 142 | 143 | if (localConnected) { 144 | localConnected = false; 145 | connection.destroy(); 146 | } 147 | }, 148 | () => localConnected, 149 | config.connect 150 | ); 151 | 152 | if (!tmp) { 153 | connection.destroy(); 154 | return; 155 | } 156 | 157 | clientToDst = tmp.clientToDst; 158 | 159 | if (tmp.preservedData) { 160 | writeOrPause(connection, clientToDst, tmp.preservedData); 161 | } 162 | 163 | stage = 1; 164 | break; 165 | case 1: 166 | writeOrPause(connection, clientToDst, data); 167 | break; 168 | default: 169 | } 170 | }); 171 | 172 | connection.on('drain', () => { 173 | clientToDst.resume(); 174 | }); 175 | 176 | connection.on('end', () => { 177 | localConnected = false; 178 | 179 | if (dstConnected) { 180 | clientToDst.end(); 181 | } 182 | }); 183 | 184 | connection.on('error', (e) => { 185 | logger.warn(`ssServer error happened in the connection with ssLocal : ${e.message}`); 186 | }); 187 | 188 | connection.on('close', (e) => { 189 | if (timer) { 190 | clearTimeout(timer); 191 | } 192 | 193 | localConnected = false; 194 | 195 | if (dstConnected) { 196 | if (e) { 197 | clientToDst.destroy(); 198 | } else { 199 | clientToDst.end(); 200 | } 201 | } 202 | }); 203 | 204 | timer = setTimeout(() => { 205 | if (localConnected) { 206 | connection.destroy(); 207 | } 208 | 209 | if (dstConnected) { 210 | clientToDst.destroy(); 211 | } 212 | }, config.timeout * 1000); 213 | } 214 | 215 | function createServer(config) { 216 | const { serverAddr, udpActive = true } = config; 217 | const server = _createServer(handleConnection.bind(null, config)) 218 | .listen(config.serverPort, serverAddr); 219 | 220 | let udpRelay = null; 221 | 222 | if (udpActive) { 223 | udpRelay = createUDPRelay(config, true, logger); 224 | } 225 | 226 | server.on('close', () => { 227 | logger.warn(`${NAME} server closed`); 228 | }); 229 | 230 | server.on('error', (e) => { 231 | logger.error(`${NAME} server error: ${e.message}`); 232 | }); 233 | 234 | logger.verbose(`${NAME} is listening on ${config.serverAddr}:${config.serverPort}`); 235 | 236 | return { 237 | server, udpRelay, 238 | }; 239 | } 240 | 241 | // eslint-disable-next-line 242 | export function startServer(config, willLogToConsole = false, injectedLogger) { 243 | logger = logger || injectedLogger 244 | || createLogger(config, LOG_NAMES.SERVER, willLogToConsole); 245 | 246 | return createServer(config); 247 | } 248 | 249 | if (module === require.main) { 250 | getConfig(process.argv.slice(2), (err, config) => { 251 | if (err) { 252 | throw err; 253 | } 254 | 255 | const { proxyOptions } = config; 256 | 257 | logger = createLogger( 258 | proxyOptions, 259 | LOG_NAMES.SERVER, 260 | true, 261 | true, 262 | ); 263 | startServer(proxyOptions, false); 264 | 265 | // TODO: 266 | // NOTE: DEV only 267 | // eslint-disable-next-line 268 | // if (config._recordMemoryUsage) { 269 | // setInterval(() => { 270 | // process.send(process.memoryUsage()); 271 | // }, INTERVAL_TIME); 272 | // } 273 | }); 274 | 275 | process.on('uncaughtException', (err) => { 276 | logger.error(`${NAME} uncaughtException: ${err.stack}`, createSafeAfterHandler(logger, () => { 277 | process.exit(1); 278 | })); 279 | }); 280 | } 281 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import ip from 'ip'; 2 | import os from 'os'; 3 | import { writeFileSync, accessSync, mkdirSync } from 'fs'; 4 | import { join } from 'path'; 5 | 6 | const DEFAULT_PATH = join(__dirname, '../logs/debug.log'); 7 | const hasOwnProperty = {}.hasOwnProperty; 8 | 9 | let platform = null; 10 | 11 | export const BufferFrom = (() => { 12 | try { 13 | Buffer.from('aa', 'hex'); 14 | } catch (err) { 15 | return ((...args) => new Buffer(...args)); 16 | } 17 | 18 | return Buffer.from; 19 | })(); 20 | 21 | export function isWindows() { 22 | if (!platform) { 23 | platform = os.type(); 24 | } 25 | 26 | return platform === 'Windows_NT'; 27 | } 28 | 29 | export function safelyKill(pid, signal) { 30 | if (pid === null || pid === undefined) { 31 | return; 32 | } 33 | 34 | if (signal && !isWindows()) { 35 | process.kill(pid, signal); 36 | } else { 37 | process.kill(pid); 38 | } 39 | } 40 | 41 | export function safelyKillChild(child, signal) { 42 | if (!child) { 43 | return; 44 | } 45 | 46 | if (signal && !isWindows()) { 47 | child.kill(signal); 48 | } else { 49 | child.kill(); 50 | } 51 | } 52 | 53 | export function fileLog(content, path = DEFAULT_PATH) { 54 | writeFileSync(path, content); 55 | } 56 | 57 | // NOTE: https://github.com/winstonjs/winston/issues/228 58 | // Winston will log things asynchronously so we have to 59 | // make sure it has log the error before exiting this 60 | // process. 61 | // And this is disappointing. 62 | export function createSafeAfterHandler(logger, next) { 63 | let numFlushes = 0; 64 | let numFlushed = 0; 65 | 66 | return () => { 67 | Object.keys(logger.transports).forEach((k) => { 68 | // eslint-disable-next-line 69 | const stream = logger.transports[k]._stream; 70 | if (stream) { 71 | numFlushes += 1; 72 | stream.once('finish', () => { 73 | numFlushed += 1; 74 | if (numFlushes === numFlushed) { 75 | next(); 76 | } 77 | }); 78 | stream.end(); 79 | } 80 | }); 81 | 82 | if (numFlushes === 0) { 83 | next(); 84 | } 85 | }; 86 | } 87 | 88 | export function closeSilently(server) { 89 | if (server) { 90 | try { 91 | server.close(); 92 | } catch (e) { 93 | // already closed 94 | } 95 | } 96 | } 97 | 98 | export function mkdirIfNotExistSync(path) { 99 | try { 100 | accessSync(path); 101 | } catch (e) { 102 | mkdirSync(path); 103 | } 104 | } 105 | 106 | export function sendDgram(socket, data, ...args) { 107 | socket.send(data, 0, data.length, ...args); 108 | } 109 | 110 | export function writeOrPause(fromCon, toCon, data) { 111 | const res = toCon.write(data); 112 | 113 | if (!res) { 114 | fromCon.pause(); 115 | } 116 | 117 | return res; 118 | } 119 | 120 | function parseDstInfo(data, offset) { 121 | const atyp = data[offset]; 122 | 123 | let dstAddr; 124 | let dstPort; 125 | let dstAddrLength; 126 | let dstPortIndex; 127 | let dstPortEnd; 128 | // length of non-data field 129 | let totalLength; 130 | 131 | switch (atyp) { 132 | case 0x01: 133 | dstAddrLength = 4; 134 | dstAddr = data.slice(offset + 1, offset + 5); 135 | dstPort = data.slice(offset + 5, offset + 7); 136 | totalLength = offset + 7; 137 | break; 138 | case 0x04: 139 | dstAddrLength = 16; 140 | dstAddr = data.slice(offset + 1, offset + 17); 141 | dstPort = data.slice(offset + 17, offset + 19); 142 | totalLength = offset + 19; 143 | break; 144 | case 0x03: 145 | dstAddrLength = data[offset + 1]; 146 | dstPortIndex = 2 + offset + dstAddrLength; 147 | dstAddr = data.slice(offset + 2, dstPortIndex); 148 | dstPortEnd = dstPortIndex + 2; 149 | dstPort = data.slice(dstPortIndex, dstPortEnd); 150 | totalLength = dstPortEnd; 151 | break; 152 | default: 153 | return null; 154 | } 155 | 156 | if (data.length < totalLength) { 157 | return null; 158 | } 159 | 160 | return { 161 | atyp, dstAddrLength, dstAddr, dstPort, totalLength, 162 | }; 163 | } 164 | 165 | export function getDstInfo(data, isServer) { 166 | // +----+-----+-------+------+----------+----------+ 167 | // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | 168 | // +----+-----+-------+------+----------+----------+ 169 | // | 1 | 1 | X'00' | 1 | Variable | 2 | 170 | // +----+-----+-------+------+----------+----------+ 171 | // Yet begin with ATYP. 172 | 173 | const offset = isServer ? 0 : 3; 174 | return parseDstInfo(data, offset); 175 | } 176 | 177 | export function getDstInfoFromUDPMsg(data, isServer) { 178 | // +----+------+------+----------+----------+----------+ 179 | // |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | 180 | // +----+------+------+----------+----------+----------+ 181 | // | 2 | 1 | 1 | Variable | 2 | Variable | 182 | // +----+------+------+----------+----------+----------+ 183 | 184 | const offset = isServer ? 0 : 3; 185 | 186 | return parseDstInfo(data, offset); 187 | } 188 | 189 | const formatKeyValues = { 190 | server: 'serverAddr', 191 | server_port: 'serverPort', 192 | local_addr: 'localAddr', 193 | local_port: 'localPort', 194 | local_addr_ipv6: 'localAddrIPv6', 195 | server_addr_ipv6: 'serverAddrIPv6', 196 | }; 197 | 198 | export function formatConfig(_config) { 199 | const formattedConfig = Object.assign({}, _config); 200 | 201 | Object.keys(formatKeyValues).forEach((key) => { 202 | if (hasOwnProperty.call(formattedConfig, key)) { 203 | formattedConfig[formatKeyValues[key]] = formattedConfig[key]; 204 | delete formattedConfig[key]; 205 | } 206 | }); 207 | 208 | return formattedConfig; 209 | } 210 | 211 | export function getDstStr(dstInfo) { 212 | if (!dstInfo) { 213 | return null; 214 | } 215 | 216 | switch (dstInfo.atyp) { 217 | case 1: 218 | case 4: 219 | return `${ip.toString(dstInfo.dstAddr)}:${dstInfo.dstPort.readUInt16BE()}`; 220 | case 3: 221 | return `${dstInfo.dstAddr.toString('utf8')}:${dstInfo.dstPort.readUInt16BE()}`; 222 | default: 223 | return 'WARN: invalid atyp'; 224 | } 225 | } 226 | 227 | export function getPrefixedArgName(name) { 228 | return name.length === 1 ? `-${name}` : `--${name}`; 229 | } 230 | 231 | export function obj2Argv(obj) { 232 | if (typeof obj !== 'object') { 233 | throw new Error('expect an object when stringify to argv'); 234 | } 235 | 236 | let argv = ''; 237 | 238 | Object.keys(obj).forEach((name) => { 239 | const argName = getPrefixedArgName(name); 240 | const value = obj[name]; 241 | let argValue = ''; 242 | 243 | if (typeof value === 'boolean') { 244 | if (!value) { 245 | return; 246 | } 247 | } else { 248 | argValue = String(value); 249 | } 250 | 251 | const parts = argValue.length > 0 ? `${argName} ${argValue}` : `${argName}`; 252 | 253 | argv = `${argv} ${parts}`; 254 | }); 255 | 256 | return argv; 257 | } 258 | -------------------------------------------------------------------------------- /test/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const auth = require('../lib/auth'); 5 | const usernamePasswordAuthetication = require('../lib/ssLocal').usernamePasswordAuthetication; 6 | const BufferFrom = require('../lib/utils').BufferFrom; 7 | 8 | const createAuthInfo = auth.createAuthInfo; 9 | const validate = auth.validate; 10 | 11 | function createMockConnection() { 12 | let writeCalledWith = null; 13 | 14 | const mockConnection = { 15 | getWriteCalled: () => writeCalledWith, 16 | write: function() { 17 | writeCalledWith = arguments; 18 | }, 19 | end: () => {}, 20 | }; 21 | 22 | return mockConnection; 23 | } 24 | 25 | describe('auth', () => { 26 | describe('createAuthInfo', () => { 27 | 28 | it('should return an object containe a "info" key and the info value contain a "forceAuth" key ' 29 | + 'and also a usernamePassword key', () => { 30 | const config = { 31 | auth: { 32 | forceAuth: true, 33 | usernamePassword: { 34 | abc: 'abc', 35 | }, 36 | }, 37 | }; 38 | 39 | const res = createAuthInfo(config); 40 | assert(res.info); 41 | assert(res.info.forceAuth); 42 | assert.strictEqual(res.info.usernamePassword, config.auth.usernamePassword); 43 | }); 44 | }); 45 | 46 | describe('validate', () => { 47 | const config = { 48 | auth: { 49 | forceAuth: true, 50 | usernamePassword: { 51 | abc: '123', 52 | }, 53 | }, 54 | }; 55 | const res = createAuthInfo(config); 56 | 57 | it('should pass the validation', () => { 58 | assert(validate(res.info, 'abc', '123')); 59 | }); 60 | 61 | it('should fail the validation', () => { 62 | assert(!validate(res.info, 'abc', '234')); 63 | assert(!validate(res.info, 'bcd', '123')); 64 | }); 65 | }); 66 | 67 | describe('usernamePasswordAuthetication', () => { 68 | it('should deny the request with invalid username/password', () => { 69 | const config = { 70 | auth: { 71 | forceAuth: true, 72 | usernamePassword: { 73 | abc: '123', 74 | }, 75 | }, 76 | }; 77 | const connection = createMockConnection(); 78 | const info = createAuthInfo(config).info; 79 | const data = BufferFrom('010361626303313232', 'hex'); 80 | 81 | usernamePasswordAuthetication(connection, data, info); 82 | 83 | assert(connection.getWriteCalled()[0].equals(BufferFrom('0101', 'hex'))); 84 | }); 85 | 86 | it('should accept the request with valid username/password', () => { 87 | const config = { 88 | auth: { 89 | forceAuth: true, 90 | usernamePassword: { 91 | abc: '123', 92 | }, 93 | }, 94 | }; 95 | const connection = createMockConnection(); 96 | const info = createAuthInfo(config).info; 97 | const data = BufferFrom('010361626303313233', 'hex'); 98 | 99 | usernamePasswordAuthetication(connection, data, info); 100 | 101 | assert(connection.getWriteCalled()[0].equals(BufferFrom('0100', 'hex'))); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyyd/encryptsocks/74532faa02f8ce95c9f802d58df1acb0c0bfd483/test/cli.js -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const c = require('../lib/config'); 3 | const DEFAULT_CONFIG = require('../lib/defaultConfig').default; 4 | const ip = require('ip'); 5 | 6 | const resolveServerAddr = c.resolveServerAddr 7 | const getConfig = c.getConfig 8 | const stringifyProxyOptions = c.stringifyProxyOptions 9 | 10 | describe('config', () => { 11 | describe('resolveServerAddr', () => { 12 | it('should keep the ivp4 `serverAddr`', done => { 13 | const config = { 14 | proxyOptions: { 15 | serverAddr: '127.0.0.1', 16 | }, 17 | }; 18 | 19 | resolveServerAddr(config, (err, newConfig) => { 20 | assert.strictEqual(newConfig.proxyOptions.serverAddr, '127.0.0.1'); 21 | done(); 22 | }); 23 | }); 24 | 25 | it('should get correct ipv4 value' , done => { 26 | const config = { 27 | proxyOptions: { 28 | serverAddr: 'example.com', 29 | }, 30 | }; 31 | 32 | resolveServerAddr(config, (err, newConfig) => { 33 | assert(ip.isV4Format(newConfig.proxyOptions.serverAddr)); 34 | done(); 35 | }); 36 | }); 37 | 38 | it('should throw when resolve invalid domain' , done => { 39 | const config = { 40 | proxyOptions: { 41 | serverAddr: 'PATH_THAT_DOES_NOT_EXIST', 42 | }, 43 | }; 44 | 45 | resolveServerAddr(config, err => { 46 | assert(err.message.indexOf('failed to resolve \'serverAddr\'') > -1); 47 | done(); 48 | }); 49 | }); 50 | }); 51 | 52 | describe('getConfig', () => { 53 | it('should force to not resolve ipv6 value when forced not to', done => { 54 | const argv = ['-s', 'PATH_THAT_DOES_NOT_EXIST'] 55 | 56 | getConfig(argv, true, (err, options) => { 57 | assert(!err); 58 | assert(options.proxyOptions.serverAddr === argv[1]); 59 | done(); 60 | }) 61 | }) 62 | 63 | it('should get `logPath` when set', (done) => { 64 | const argv = ['--log_path', '/logs'] 65 | 66 | getConfig(argv, true, (err, options) => { 67 | assert(!err); 68 | assert(options.proxyOptions.logPath === argv[1]); 69 | done(); 70 | }) 71 | }) 72 | 73 | it('should use `process.pwd()` as base when set a relative path', done => { 74 | const argv = ['--log_path', './logs'] 75 | 76 | getConfig(argv, true, (err, options) => { 77 | assert(!err); 78 | assert(options.proxyOptions.logPath 79 | === (process.cwd() + '/logs')); 80 | done(); 81 | }) 82 | }) 83 | }) 84 | 85 | describe('stringifyProxyOptions', () => { 86 | it('should stringify proxyOptions', () => { 87 | const argString = stringifyProxyOptions(DEFAULT_CONFIG); 88 | 89 | assert(argString.indexOf('-s 0.0.0.0 -p 8083 -l 127.0.0.1 -b 1080 -k YOUR_PASSWORD_HERE --pac_port 8090 -t 600 -m aes-128-cfb --level warn') === 0) 90 | }) 91 | }) 92 | }); 93 | -------------------------------------------------------------------------------- /test/createUDPRelay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const utils = require('../lib/utils'); 6 | const testServer = require('./testServer'); 7 | const ssLocal = require('../lib/ssLocal'); 8 | const ssServer = require('../lib/ssServer'); 9 | const _config = require('../lib/defaultConfig.js').default; 10 | const LOCAL_ONLY = require('./utils').LOCAL_ONLY; 11 | 12 | const config = Object.assign({}, _config, { 13 | 'level': 'error', 14 | }); 15 | 16 | const strictEqual = assert.strictEqual; 17 | const DST_RES_TEXT = testServer.DST_RES_TEXT; 18 | const DST_ADDR = testServer.DST_ADDR; 19 | const DST_PORT = testServer.DST_PORT; 20 | 21 | const createUDPAssociate = testServer.createUDPAssociate; 22 | const createUDPServer = testServer.createUDPServer; 23 | 24 | const TIMEOUT = 5000; 25 | const UDP_RES_TYPE = testServer.UDP_RES_TYPE; 26 | const UDP_RES_MSG = testServer.UDP_RES_MSG; 27 | 28 | describe('UDP Relay', () => { 29 | let dstServer; 30 | let ssLocalServer; 31 | let ssServerServer; 32 | 33 | before(function() { 34 | this.timeout(5000); 35 | ssLocalServer = ssLocal.startServer(config); 36 | ssServerServer = ssServer.startServer(config); 37 | dstServer = createUDPServer(); 38 | }); 39 | 40 | it('should work for UDP association and receive message repeately', function(cb) { 41 | this.timeout(5000); 42 | 43 | let EXPECTED_TIME = 3; 44 | let count = 0; 45 | 46 | createUDPAssociate((sendUDPFrame) => { 47 | sendUDPFrame(UDP_RES_TYPE.CONTINUOUS); 48 | }, (msg, info, client) => { 49 | assert(msg, UDP_RES_MSG); 50 | 51 | count ++; 52 | 53 | if (count === EXPECTED_TIME){ 54 | client.close(); 55 | cb(); 56 | } 57 | }); 58 | }); 59 | 60 | it('should work for UDP association', function(cb) { 61 | this.timeout(5000); 62 | 63 | let EXPECTED_TIME = 3; 64 | let count = 0; 65 | 66 | createUDPAssociate((sendToDST) => { 67 | let i; 68 | 69 | for (i = 0; i <= EXPECTED_TIME; i++) { 70 | sendToDST(UDP_RES_TYPE.REPEAT_ONCE); 71 | } 72 | }, (msg, info, client) => { 73 | assert(msg.toString(), UDP_RES_TYPE.REPEAT_ONCE); 74 | 75 | count ++; 76 | 77 | if (count === EXPECTED_TIME){ 78 | client.close(); 79 | cb(); 80 | } 81 | }); 82 | }); 83 | 84 | after(() => { 85 | dstServer.close(); 86 | ssLocalServer.closeAll(); 87 | ssServerServer.server.close(); 88 | ssServerServer.udpRelay.close(); 89 | }); 90 | }); 91 | 92 | describe(LOCAL_ONLY + ' UDP6 Relay', () => { 93 | let ssLocalServer; 94 | let ssServerServer; 95 | let dstServerUDP6; 96 | 97 | before(() => { 98 | ssLocalServer = ssLocal.startServer(config); 99 | ssServerServer = ssServer.startServer(config); 100 | dstServerUDP6 = createUDPServer('udp6'); 101 | }); 102 | 103 | it('should work for UDP association for UDP6', function(cb) { 104 | this.timeout(5000); 105 | 106 | let EXPECTED_TIME = 3; 107 | let count = 0; 108 | 109 | createUDPAssociate((sendToDST) => { 110 | let i; 111 | 112 | for (i = 0; i <= EXPECTED_TIME; i++) { 113 | sendToDST(UDP_RES_TYPE.REPEAT_ONCE); 114 | } 115 | }, (msg, info, client) => { 116 | assert(msg.toString(), UDP_RES_TYPE.REPEAT_ONCE); 117 | 118 | count ++; 119 | 120 | if (count === EXPECTED_TIME){ 121 | client.close(); 122 | cb(); 123 | } 124 | }, true); 125 | }); 126 | 127 | after(() => { 128 | dstServerUDP6.close(); 129 | ssLocalServer.closeAll(); 130 | ssServerServer.server.close(); 131 | ssServerServer.udpRelay.close(); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/encryptor.js: -------------------------------------------------------------------------------- 1 | const encryptor = require('../lib/encryptor'); 2 | const assert = require('assert'); 3 | 4 | const strictEqual = assert.strictEqual; 5 | const encrypt = encryptor.encrypt; 6 | const decrypt = encryptor.decrypt; 7 | 8 | describe('encryptor', () => { 9 | var bufBinary = '0101010101010101'; 10 | var password = 'a'; 11 | var crypted = '\u0019"\u0006hL\f|BX`Es$8Db'; 12 | var buf = new Buffer(8); 13 | var methodName = 'aes-192-cfb'; 14 | buf.write(bufBinary, 'hex'); 15 | 16 | it('should generate key from password', () => { 17 | var key = encryptor.generateKey(methodName, password) 18 | .toString('hex'); 19 | 20 | strictEqual(key, '0cc175b9c0f1b6a831c399e269772661cec520ea51ea0a47'); 21 | }); 22 | 23 | it('should encrypt data correctly', () => { 24 | var initialData = 'Hello World'; 25 | var expected = '00000000000000000000000000000000a4d373d22eaa7784c76825'; 26 | var secondData = 'I\'m oyyd'; 27 | var secondExpected = 'e033bb1c361c91bb'; 28 | var iv = new Buffer(encryptor.getParamLength(methodName)[1]); 29 | iv.fill(0); 30 | var cipheredData = null; 31 | var decipheredData = null; 32 | var tmp = encryptor.createCipher( 33 | password, methodName, 34 | new Buffer(initialData, 'utf8'), 35 | iv 36 | ); 37 | 38 | assert(!!tmp.cipher); 39 | strictEqual(tmp.data.toString('hex'), expected); 40 | 41 | var tmp2 = encryptor.createDecipher( 42 | password, methodName, 43 | tmp.data 44 | ); 45 | 46 | strictEqual(tmp2.data.toString('utf8'), initialData); 47 | 48 | cipheredData = tmp.cipher.update(secondData); 49 | 50 | strictEqual(cipheredData.toString('hex'), secondExpected); 51 | 52 | decipheredData = tmp2.decipher.update(cipheredData); 53 | 54 | strictEqual(decipheredData.toString('utf8'), secondData); 55 | }); 56 | 57 | it('should correctly encrypt and decrypt the data', () => { 58 | var initialData = 'Hello World'; 59 | var expected = '00000000000000000000000000000000a4d373d22eaa7784c76825'; 60 | var iv = new Buffer(encryptor.getParamLength(methodName)[1]); 61 | iv.fill(0); 62 | 63 | var cipheredData = encrypt(password, methodName, initialData, iv); 64 | 65 | strictEqual(cipheredData.toString('hex'), expected); 66 | 67 | var decipheredData = decrypt(password, methodName, cipheredData); 68 | 69 | strictEqual(decipheredData.toString('ascii'), initialData); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/filter.js: -------------------------------------------------------------------------------- 1 | const filterObj = require('../lib/filter'); 2 | const assert = require('assert'); 3 | 4 | const setDenyList = filterObj.setDenyList; 5 | const filter = filterObj.filter; 6 | 7 | describe('filter', () => { 8 | setDenyList([ 9 | /google/, 10 | ]); 11 | 12 | it('should let "example.com" pass', () => { 13 | assert(filter({ 14 | atyp: 0x03, 15 | dstAddr: new Buffer('example.com', 'ascii'), 16 | dstPort: new Buffer([0x00, 0x50]), 17 | })); 18 | }); 19 | 20 | it('should deny "www.google.com"', () => { 21 | assert(!filter({ 22 | atyp: 0x03, 23 | dstAddr: new Buffer('www.google.com', 'ascii'), 24 | dstPort: new Buffer([0x00, 0x50]), 25 | })); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/httpProxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const shttp = require('socks5-http-client'); 4 | const shttps = require('socks5-https-client'); 5 | const http = require('http'); 6 | const assert = require('assert'); 7 | const ip = require('ip'); 8 | 9 | const testServer = require('./testServer'); 10 | const ssLocal = require('../lib/ssLocal'); 11 | const ssServer = require('../lib/ssServer'); 12 | const utils = require('../lib/utils'); 13 | const _config = require('../lib/defaultConfig.js').default; 14 | const LOCAL_ONLY = require('./utils').LOCAL_ONLY; 15 | 16 | const config = Object.assign({}, _config, { 17 | 'level': 'error', 18 | }); 19 | const strictEqual = assert.strictEqual; 20 | const getDstInfo = utils.getDstInfo; 21 | const DST_RES_TEXT = testServer.DST_RES_TEXT; 22 | const DST_ADDR = testServer.DST_ADDR; 23 | const DST_PORT = testServer.DST_PORT; 24 | const createHTTPServer = testServer.createHTTPServer; 25 | 26 | const TIMEOUT = 30000; 27 | 28 | describe('getDstInfo', () => { 29 | 30 | it('should return `null` when the `atyp` type is not supported ', () => { 31 | const buffer = new Buffer(10); 32 | buffer.write('050100027f000001a496', 'hex'); 33 | 34 | const dstInfo = getDstInfo(buffer); 35 | 36 | strictEqual(dstInfo, null); 37 | }); 38 | 39 | it('should return correct DST info when parsing ipv4', () => { 40 | const buffer = new Buffer(10); 41 | buffer.write('050100017f000001a496', 'hex'); 42 | 43 | const dstInfo = getDstInfo(buffer); 44 | 45 | strictEqual(dstInfo.atyp, 0x01); 46 | strictEqual(dstInfo.dstAddrLength, 4); 47 | strictEqual(ip.toString(dstInfo.dstAddr), '127.0.0.1'); 48 | strictEqual(dstInfo.dstPort.readUInt16BE(), 42134); 49 | }); 50 | 51 | it('should return correct DST info when parsing domain', () => { 52 | const buffer = new Buffer(18); 53 | buffer.write('050100030b', 0, 'hex'); 54 | buffer.write('example.com', 5, 'ascii'); 55 | buffer.writeUInt16BE(80, 16); 56 | 57 | const dstInfo = getDstInfo(buffer); 58 | 59 | strictEqual(dstInfo.atyp, 0x03); 60 | strictEqual(dstInfo.dstAddrLength, 11); 61 | strictEqual(dstInfo.dstAddr.toString('ascii'), 'example.com'); 62 | strictEqual(dstInfo.dstPort.readUInt16BE(), 80); 63 | }); 64 | 65 | it('should return correct DST info when parsing ipv6', () => { 66 | const buffer = new Buffer(22); 67 | buffer.write('0501000400000000000000000000000000000001a496', 'hex'); 68 | 69 | const dstInfo = getDstInfo(buffer); 70 | 71 | strictEqual(dstInfo.atyp, 0x04); 72 | strictEqual(dstInfo.dstAddrLength, 16); 73 | strictEqual(ip.toString(dstInfo.dstAddr), '::1'); 74 | strictEqual(dstInfo.dstPort.readUInt16BE(), 42134); 75 | }); 76 | // TODO: ipv6 77 | }); 78 | 79 | describe('http proxy', () => { 80 | let dstServer; 81 | let ssLocalServer; 82 | let ssServerServer; 83 | 84 | before(function(cb) { 85 | this.timeout(TIMEOUT); 86 | ssLocalServer = ssLocal.startServer(config); 87 | ssServerServer = ssServer.startServer(config); 88 | dstServer = createHTTPServer(cb); 89 | }); 90 | 91 | describe('ipv4', () => { 92 | it('should get correct response through ipv4', function(cb) { 93 | this.timeout(TIMEOUT); 94 | 95 | const options = { 96 | port: DST_PORT, 97 | host: DST_ADDR, 98 | socksHost: config.localHost, 99 | socksPort: config.localPort, 100 | }; 101 | 102 | shttp.get(options, res => { 103 | res.on('readable', () => { 104 | strictEqual(res.read().toString('utf8'), DST_RES_TEXT, 105 | 'Responsed text is not same'); 106 | cb(); 107 | }); 108 | }); 109 | }); 110 | 111 | it('should get correct response when the `atyp` is `domain`', function(cb) { 112 | this.timeout(TIMEOUT); 113 | 114 | let success = false; 115 | 116 | shttp.get('http://example.com', res => { 117 | res.on('readable', () => { 118 | let text = res.read(); 119 | 120 | if (text) { 121 | text = text.toString('utf8'); 122 | if (~text.indexOf('Example Domain')) { 123 | success = true; 124 | cb(); 125 | } 126 | } 127 | }); 128 | 129 | res.on('close', () => { 130 | if (!success) { 131 | cb(new Error('failed')); 132 | } 133 | }); 134 | }); 135 | }); 136 | 137 | // TODO: this test seems to be invalid 138 | it('should get correct response when the requesting by ssl', function(cb) { 139 | this.timeout(TIMEOUT); 140 | 141 | shttps.get('https://example.com', res => { 142 | res.on('readable', () => { 143 | let text = res.read().toString('utf8'); 144 | assert(!!~text.indexOf('Example Domain')); 145 | cb(); 146 | }); 147 | }); 148 | }); 149 | }); 150 | 151 | describe(LOCAL_ONLY + ' ipv6', () => { 152 | it('should get correct response through ipv6', function(cb) { 153 | this.timeout(TIMEOUT); 154 | 155 | const options = { 156 | port: DST_PORT, 157 | host: '::1', 158 | socksHost: config.localHost, 159 | socksPort: config.localPort, 160 | }; 161 | 162 | shttp.get(options, res => { 163 | res.on('readable', () => { 164 | strictEqual(res.read().toString('utf8'), DST_RES_TEXT, 165 | 'Responsed text is not same'); 166 | cb() 167 | }); 168 | }); 169 | }); 170 | }); 171 | 172 | after(() => { 173 | dstServer.close(); 174 | ssLocalServer.closeAll(); 175 | ssServerServer.server.close(); 176 | ssServerServer.udpRelay.close(); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /test/pacServer.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const utils = require('../lib/gfwlistUtils'); 4 | const LOCAL_ONLY = require('./utils').LOCAL_ONLY; 5 | const assert = require('assert'); 6 | const pacServer = require('../lib/pacServer'); 7 | 8 | const strictEqual = assert.strictEqual; 9 | const requestGFWList = utils.requestGFWList; 10 | const readLine = utils.readLine; 11 | const createListArrayString = utils.createListArrayString; 12 | const updateGFWList = utils.updateGFWList; 13 | const createPACServer = pacServer.createPACServer; 14 | const request = http.request; 15 | 16 | // NOTE: this will take a lot of time 17 | xdescribe(LOCAL_ONLY + ' requestGFWList', function() { 18 | this.timeout(20000); 19 | 20 | it('should get gfwlist', function(cb) { 21 | updateGFWList(err => { 22 | if (err) { 23 | throw err; 24 | } 25 | cb(); 26 | }); 27 | }); 28 | 29 | it('should get gfwlist with specify url', function(cb) { 30 | const targetURL = 'https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt'; 31 | 32 | updateGFWList(targetURL, err => { 33 | if (err) { 34 | throw err; 35 | } 36 | cb(); 37 | }); 38 | }); 39 | }); 40 | 41 | describe('requestGFWList', function() { 42 | var text = '1\n2\r3\r\n4\r'; 43 | var text2 = '1\n'; 44 | 45 | describe('readLine', function() { 46 | it('should read line correctly', function() { 47 | readLine.clear(); 48 | strictEqual(readLine(text), '1\n'); 49 | strictEqual(readLine(text), '2\r'); 50 | strictEqual(readLine(text), '3\r\n'); 51 | strictEqual(readLine(text), '4\r'); 52 | strictEqual(readLine(text), null); 53 | }); 54 | 55 | it('should read from start when reading a new string', function() { 56 | readLine.clear(); 57 | strictEqual(readLine(text), '1\n'); 58 | strictEqual(readLine(text), '2\r'); 59 | strictEqual(readLine(text2), '1\n'); 60 | strictEqual(readLine(text), '1\n'); 61 | }); 62 | 63 | it('should strip linebreak', function() { 64 | readLine.clear(); 65 | strictEqual(readLine(text, true), '1'); 66 | strictEqual(readLine(text, true), '2'); 67 | strictEqual(readLine(text, true), '3'); 68 | strictEqual(readLine(text, true), '4'); 69 | strictEqual(readLine(text, true), null); 70 | }); 71 | }); 72 | 73 | describe('createListArrayString', function() { 74 | it('should return return array string', function() { 75 | var text = '!abc\nwww.google.com\n!www.abc.com\ngithub.com\n'; 76 | var expected = 'var rules = ["www.google.com",\n"github.com"];'; 77 | 78 | strictEqual(createListArrayString(text), expected); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('pacServer', function() { 84 | this.timeout(5000); 85 | 86 | it('should serve pac file', function(cb) { 87 | var localAddr = '127.0.0.1'; 88 | var localPort = 1082; 89 | var pacServerPort = 8084; 90 | var server = createPACServer({ localAddr, localPort, pacServerPort }); 91 | 92 | var data = ''; 93 | var expected = 'FindProxyForURL'; 94 | 95 | var HOST = localAddr + ':' + pacServerPort; 96 | 97 | request(url.parse('http://' + HOST), function(resSocket) { 98 | 99 | resSocket.on('data', function(chunk) { 100 | data = data + chunk.toString(); 101 | }); 102 | 103 | resSocket.on('end', function() { 104 | if (!!~data.indexOf(expected) && !!~data.indexOf(localAddr + ':' + localPort)) { 105 | server.close(); 106 | cb(null); 107 | return; 108 | } 109 | 110 | server.close(); 111 | cb(new Error('invalid')); 112 | }); 113 | }).end(); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /test/testServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const https = require('https'); 5 | const dgram = require('dgram'); 6 | const Socks = require('socks'); 7 | const ip = require('ip'); 8 | const config = require('../lib/defaultConfig.js').default; 9 | 10 | const DST_RES_TEXT = 'hello world!'; 11 | const DST_ADDR = '127.0.0.1'; 12 | const DST_ADDR_IPV6 = '::1'; 13 | const DST_PORT = 42134; 14 | const SOCKS_PORT = config.localPort; 15 | 16 | const UDP_RES_TYPE = { 17 | CONTINUOUS: 'CONTINUOUS', 18 | REPEAT_ONCE: 'REPEAT_ONCE', 19 | }; 20 | const UDP_RES_MSG = 'Hello there'; 21 | 22 | const UDP_ASSOCIATE_OPTIONS = { 23 | proxy: { 24 | ipaddress: DST_ADDR, 25 | port: SOCKS_PORT, 26 | type: 5, 27 | command: 'associate', 28 | }, 29 | target: { 30 | host: DST_ADDR, 31 | port: DST_PORT, 32 | }, 33 | }; 34 | 35 | function createHTTPServer(cb) { 36 | return http.createServer((req, res) => { 37 | res.end(DST_RES_TEXT); 38 | }).listen(DST_PORT, cb); 39 | } 40 | 41 | function createUDPServer(udpType) { 42 | udpType = udpType || 'udp4'; 43 | const server = dgram.createSocket(udpType); 44 | 45 | let msgTimer; 46 | 47 | server.bind(DST_PORT, udpType === 'udp4' ? DST_ADDR : DST_ADDR_IPV6); 48 | 49 | server.on('message', (msg, info) => { 50 | switch (msg.toString('utf8')) { 51 | case UDP_RES_TYPE.CONTINUOUS: 52 | for(let i = 0; i < 3; i++) { 53 | server.send(UDP_RES_MSG, 0, UDP_RES_MSG.length, info.port, info.address); 54 | } 55 | return; 56 | case UDP_RES_TYPE.REPEAT_ONCE: 57 | server.send(msg, 0, msg.length, info.port, info.address); 58 | return; 59 | default: 60 | console.log('unknown msg'); 61 | } 62 | }); 63 | 64 | return server; 65 | } 66 | 67 | function sendUDPFrame(client, port, _host, options, data) { 68 | const targetHost = (typeof options.target.host === 'string' 69 | ? options.target.host : ip.toString(options.target.host)); 70 | const targetPort = options.target.port; 71 | const host = (typeof _host === 'string' 72 | ? _host : ip.toString(_host)); 73 | 74 | const frame = Socks.createUDPFrame({ 75 | host: targetHost, 76 | port: targetPort, 77 | }, new Buffer(data)); 78 | 79 | client.send(frame, 0, frame.length, port, host); 80 | } 81 | 82 | function createUDPAssociate(onReady, onMessage, isUDP6) { 83 | // NOTE: the `socks` lib will unexpectly mutate the options. 84 | const options = Object.assign({}, { 85 | proxy: Object.assign({}, UDP_ASSOCIATE_OPTIONS.proxy), 86 | target: Object.assign({}, UDP_ASSOCIATE_OPTIONS.target, (isUDP6 ? { 87 | host: '::1', 88 | } : null)), 89 | }); 90 | 91 | Socks.createConnection(options, (err, socket, info) => { 92 | if (err) { 93 | console.log('ERR: ', err.stack); 94 | return; 95 | } 96 | 97 | const client = dgram.createSocket(isUDP6 ? 'udp6' : 'udp4'); 98 | 99 | client.on('message', (msg, info) => { 100 | onMessage(msg, info, client); 101 | }); 102 | 103 | onReady(sendUDPFrame.bind(null, client, info.port, info.host, options)); 104 | }); 105 | } 106 | 107 | function createUDPClient(udpType) { 108 | udpType = udpType || 'udp4'; 109 | const server = dgram.createSocket(udpType); 110 | const MSG = 'Hello'; 111 | 112 | let msgTimer; 113 | 114 | server.on('message', (msg, info) => { 115 | console.log(`RECEIVE msg: ${msg}`); 116 | }); 117 | 118 | msgTimer = setInterval(() => { 119 | server.send(MSG, 0, MSG.length, DST_PORT, DST_ADDR); 120 | }, 1000); 121 | 122 | return server; 123 | } 124 | 125 | 126 | module.exports = { 127 | DST_PORT, DST_ADDR, DST_RES_TEXT, 128 | UDP_RES_MSG, UDP_RES_TYPE, 129 | createHTTPServer, 130 | createUDPServer, 131 | createUDPClient, 132 | createUDPAssociate, 133 | }; 134 | 135 | if (module === require.main) { 136 | // createHTTPServer(() => { 137 | // console.log(`test server is running on ${DST_ADDR}:${DST_PORT}`); 138 | // }); 139 | createUDPServer(); 140 | createUDPClient(); 141 | } 142 | -------------------------------------------------------------------------------- /test/tools/writeArgv.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | fs.writeFileSync('./res.txt', JSON.stringify(process.argv.slice(2))); 4 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | LOCAL_ONLY: '[local only]', 3 | }; 4 | -------------------------------------------------------------------------------- /vendor/ADPMatcher.js: -------------------------------------------------------------------------------- 1 | // NOTE: exported from shadowsocksx 2 | /* 3 | * This file is part of Adblock Plus , 4 | * Copyright (C) 2006-2014 Eyeo GmbH 5 | * 6 | * Adblock Plus is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License version 3 as 8 | * published by the Free Software Foundation. 9 | * 10 | * Adblock Plus is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with Adblock Plus. If not, see . 17 | */ 18 | 19 | function createDict() 20 | { 21 | var result = {}; 22 | result.__proto__ = null; 23 | return result; 24 | } 25 | 26 | function getOwnPropertyDescriptor(obj, key) 27 | { 28 | if (obj.hasOwnProperty(key)) 29 | { 30 | return obj[key]; 31 | } 32 | return null; 33 | } 34 | 35 | function extend(subclass, superclass, definition) 36 | { 37 | if (Object.__proto__) 38 | { 39 | definition.__proto__ = superclass.prototype; 40 | subclass.prototype = definition; 41 | } 42 | else 43 | { 44 | var tmpclass = function(){}, ret; 45 | tmpclass.prototype = superclass.prototype; 46 | subclass.prototype = new tmpclass(); 47 | subclass.prototype.constructor = superclass; 48 | for (var i in definition) 49 | { 50 | if (definition.hasOwnProperty(i)) 51 | { 52 | subclass.prototype[i] = definition[i]; 53 | } 54 | } 55 | } 56 | } 57 | 58 | function Filter(text) 59 | { 60 | this.text = text; 61 | this.subscriptions = []; 62 | } 63 | Filter.prototype = { 64 | text: null, 65 | subscriptions: null, 66 | toString: function() 67 | { 68 | return this.text; 69 | } 70 | }; 71 | Filter.knownFilters = createDict(); 72 | Filter.elemhideRegExp = /^([^\/\*\|\@"!]*?)#(\@)?(?:([\w\-]+|\*)((?:\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\))*)|#([^{}]+))$/; 73 | Filter.regexpRegExp = /^(@@)?\/.*\/(?:\$~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)?$/; 74 | Filter.optionsRegExp = /\$(~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)$/; 75 | Filter.fromText = function(text) 76 | { 77 | if (text in Filter.knownFilters) 78 | { 79 | return Filter.knownFilters[text]; 80 | } 81 | var ret; 82 | if (text[0] == "!") 83 | { 84 | ret = new CommentFilter(text); 85 | } 86 | else 87 | { 88 | ret = RegExpFilter.fromText(text); 89 | } 90 | Filter.knownFilters[ret.text] = ret; 91 | return ret; 92 | }; 93 | 94 | function InvalidFilter(text, reason) 95 | { 96 | Filter.call(this, text); 97 | this.reason = reason; 98 | } 99 | extend(InvalidFilter, Filter, { 100 | reason: null 101 | }); 102 | 103 | function CommentFilter(text) 104 | { 105 | Filter.call(this, text); 106 | } 107 | extend(CommentFilter, Filter, { 108 | }); 109 | 110 | function ActiveFilter(text, domains) 111 | { 112 | Filter.call(this, text); 113 | this.domainSource = domains; 114 | } 115 | extend(ActiveFilter, Filter, { 116 | domainSource: null, 117 | domainSeparator: null, 118 | ignoreTrailingDot: true, 119 | domainSourceIsUpperCase: false, 120 | getDomains: function() 121 | { 122 | var prop = getOwnPropertyDescriptor(this, "domains"); 123 | if (prop) 124 | { 125 | return prop; 126 | } 127 | var domains = null; 128 | if (this.domainSource) 129 | { 130 | var source = this.domainSource; 131 | if (!this.domainSourceIsUpperCase) 132 | { 133 | source = source.toUpperCase(); 134 | } 135 | var list = source.split(this.domainSeparator); 136 | if (list.length == 1 && list[0][0] != "~") 137 | { 138 | domains = createDict(); 139 | domains[""] = false; 140 | if (this.ignoreTrailingDot) 141 | { 142 | list[0] = list[0].replace(/\.+$/, ""); 143 | } 144 | domains[list[0]] = true; 145 | } 146 | else 147 | { 148 | var hasIncludes = false; 149 | for (var i = 0; i < list.length; i++) 150 | { 151 | var domain = list[i]; 152 | if (this.ignoreTrailingDot) 153 | { 154 | domain = domain.replace(/\.+$/, ""); 155 | } 156 | if (domain == "") 157 | { 158 | continue; 159 | } 160 | var include; 161 | if (domain[0] == "~") 162 | { 163 | include = false; 164 | domain = domain.substr(1); 165 | } 166 | else 167 | { 168 | include = true; 169 | hasIncludes = true; 170 | } 171 | if (!domains) 172 | { 173 | domains = createDict(); 174 | } 175 | domains[domain] = include; 176 | } 177 | domains[""] = !hasIncludes; 178 | } 179 | this.domainSource = null; 180 | } 181 | return this.domains; 182 | }, 183 | sitekeys: null, 184 | isActiveOnDomain: function(docDomain, sitekey) 185 | { 186 | if (this.getSitekeys() && (!sitekey || this.getSitekeys().indexOf(sitekey.toUpperCase()) < 0)) 187 | { 188 | return false; 189 | } 190 | if (!this.getDomains()) 191 | { 192 | return true; 193 | } 194 | if (!docDomain) 195 | { 196 | return this.getDomains()[""]; 197 | } 198 | if (this.ignoreTrailingDot) 199 | { 200 | docDomain = docDomain.replace(/\.+$/, ""); 201 | } 202 | docDomain = docDomain.toUpperCase(); 203 | while (true) 204 | { 205 | if (docDomain in this.getDomains()) 206 | { 207 | return this.domains[docDomain]; 208 | } 209 | var nextDot = docDomain.indexOf("."); 210 | if (nextDot < 0) 211 | { 212 | break; 213 | } 214 | docDomain = docDomain.substr(nextDot + 1); 215 | } 216 | return this.domains[""]; 217 | }, 218 | isActiveOnlyOnDomain: function(docDomain) 219 | { 220 | if (!docDomain || !this.getDomains() || this.getDomains()[""]) 221 | { 222 | return false; 223 | } 224 | if (this.ignoreTrailingDot) 225 | { 226 | docDomain = docDomain.replace(/\.+$/, ""); 227 | } 228 | docDomain = docDomain.toUpperCase(); 229 | for (var domain in this.getDomains()) 230 | { 231 | if (this.domains[domain] && domain != docDomain && (domain.length <= docDomain.length || domain.indexOf("." + docDomain) != domain.length - docDomain.length - 1)) 232 | { 233 | return false; 234 | } 235 | } 236 | return true; 237 | } 238 | }); 239 | 240 | function RegExpFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys) 241 | { 242 | ActiveFilter.call(this, text, domains, sitekeys); 243 | if (contentType != null) 244 | { 245 | this.contentType = contentType; 246 | } 247 | if (matchCase) 248 | { 249 | this.matchCase = matchCase; 250 | } 251 | if (thirdParty != null) 252 | { 253 | this.thirdParty = thirdParty; 254 | } 255 | if (sitekeys != null) 256 | { 257 | this.sitekeySource = sitekeys; 258 | } 259 | if (regexpSource.length >= 2 && regexpSource[0] == "/" && regexpSource[regexpSource.length - 1] == "/") 260 | { 261 | var regexp = new RegExp(regexpSource.substr(1, regexpSource.length - 2), this.matchCase ? "" : "i"); 262 | this.regexp = regexp; 263 | } 264 | else 265 | { 266 | this.regexpSource = regexpSource; 267 | } 268 | } 269 | extend(RegExpFilter, ActiveFilter, { 270 | domainSourceIsUpperCase: true, 271 | length: 1, 272 | domainSeparator: "|", 273 | regexpSource: null, 274 | getRegexp: function() 275 | { 276 | var prop = getOwnPropertyDescriptor(this, "regexp"); 277 | if (prop) 278 | { 279 | return prop; 280 | } 281 | var source = this.regexpSource.replace(/\*+/g, "*").replace(/\^\|$/, "^").replace(/\W/g, "\\$&").replace(/\\\*/g, ".*").replace(/\\\^/g, "(?:[\\x00-\\x24\\x26-\\x2C\\x2F\\x3A-\\x40\\x5B-\\x5E\\x60\\x7B-\\x7F]|$)").replace(/^\\\|\\\|/, "^[\\w\\-]+:\\/+(?!\\/)(?:[^\\/]+\\.)?").replace(/^\\\|/, "^").replace(/\\\|$/, "$").replace(/^(\.\*)/, "").replace(/(\.\*)$/, ""); 282 | var regexp = new RegExp(source, this.matchCase ? "" : "i"); 283 | this.regexp = regexp; 284 | return regexp; 285 | }, 286 | contentType: 2147483647, 287 | matchCase: false, 288 | thirdParty: null, 289 | sitekeySource: null, 290 | getSitekeys: function() 291 | { 292 | var prop = getOwnPropertyDescriptor(this, "sitekeys"); 293 | if (prop) 294 | { 295 | return prop; 296 | } 297 | var sitekeys = null; 298 | if (this.sitekeySource) 299 | { 300 | sitekeys = this.sitekeySource.split("|"); 301 | this.sitekeySource = null; 302 | } 303 | this.sitekeys = sitekeys; 304 | return this.sitekeys; 305 | }, 306 | matches: function(location, contentType, docDomain, thirdParty, sitekey) 307 | { 308 | if (this.getRegexp().test(location) && this.isActiveOnDomain(docDomain, sitekey)) 309 | { 310 | return true; 311 | } 312 | return false; 313 | } 314 | }); 315 | RegExpFilter.prototype["0"] = "#this"; 316 | RegExpFilter.fromText = function(text) 317 | { 318 | var blocking = true; 319 | var origText = text; 320 | if (text.indexOf("@@") == 0) 321 | { 322 | blocking = false; 323 | text = text.substr(2); 324 | } 325 | var contentType = null; 326 | var matchCase = null; 327 | var domains = null; 328 | var sitekeys = null; 329 | var thirdParty = null; 330 | var collapse = null; 331 | var options; 332 | var match = text.indexOf("$") >= 0 ? Filter.optionsRegExp.exec(text) : null; 333 | if (match) 334 | { 335 | options = match[1].toUpperCase().split(","); 336 | text = match.input.substr(0, match.index); 337 | for (var _loopIndex6 = 0; _loopIndex6 < options.length; ++_loopIndex6) 338 | { 339 | var option = options[_loopIndex6]; 340 | var value = null; 341 | var separatorIndex = option.indexOf("="); 342 | if (separatorIndex >= 0) 343 | { 344 | value = option.substr(separatorIndex + 1); 345 | option = option.substr(0, separatorIndex); 346 | } 347 | option = option.replace(/-/, "_"); 348 | if (option in RegExpFilter.typeMap) 349 | { 350 | if (contentType == null) 351 | { 352 | contentType = 0; 353 | } 354 | contentType |= RegExpFilter.typeMap[option]; 355 | } 356 | else if (option[0] == "~" && option.substr(1) in RegExpFilter.typeMap) 357 | { 358 | if (contentType == null) 359 | { 360 | contentType = RegExpFilter.prototype.contentType; 361 | } 362 | contentType &= ~RegExpFilter.typeMap[option.substr(1)]; 363 | } 364 | else if (option == "MATCH_CASE") 365 | { 366 | matchCase = true; 367 | } 368 | else if (option == "~MATCH_CASE") 369 | { 370 | matchCase = false; 371 | } 372 | else if (option == "DOMAIN" && typeof value != "undefined") 373 | { 374 | domains = value; 375 | } 376 | else if (option == "THIRD_PARTY") 377 | { 378 | thirdParty = true; 379 | } 380 | else if (option == "~THIRD_PARTY") 381 | { 382 | thirdParty = false; 383 | } 384 | else if (option == "COLLAPSE") 385 | { 386 | collapse = true; 387 | } 388 | else if (option == "~COLLAPSE") 389 | { 390 | collapse = false; 391 | } 392 | else if (option == "SITEKEY" && typeof value != "undefined") 393 | { 394 | sitekeys = value; 395 | } 396 | else 397 | { 398 | return new InvalidFilter(origText, "Unknown option " + option.toLowerCase()); 399 | } 400 | } 401 | } 402 | if (!blocking && (contentType == null || contentType & RegExpFilter.typeMap.DOCUMENT) && (!options || options.indexOf("DOCUMENT") < 0) && !/^\|?[\w\-]+:/.test(text)) 403 | { 404 | if (contentType == null) 405 | { 406 | contentType = RegExpFilter.prototype.contentType; 407 | } 408 | contentType &= ~RegExpFilter.typeMap.DOCUMENT; 409 | } 410 | try 411 | { 412 | if (blocking) 413 | { 414 | return new BlockingFilter(origText, text, contentType, matchCase, domains, thirdParty, sitekeys, collapse); 415 | } 416 | else 417 | { 418 | return new WhitelistFilter(origText, text, contentType, matchCase, domains, thirdParty, sitekeys); 419 | } 420 | } 421 | catch (e) 422 | { 423 | return new InvalidFilter(origText, e); 424 | } 425 | }; 426 | RegExpFilter.typeMap = { 427 | OTHER: 1, 428 | SCRIPT: 2, 429 | IMAGE: 4, 430 | STYLESHEET: 8, 431 | OBJECT: 16, 432 | SUBDOCUMENT: 32, 433 | DOCUMENT: 64, 434 | XBL: 1, 435 | PING: 1, 436 | XMLHTTPREQUEST: 2048, 437 | OBJECT_SUBREQUEST: 4096, 438 | DTD: 1, 439 | MEDIA: 16384, 440 | FONT: 32768, 441 | BACKGROUND: 4, 442 | POPUP: 268435456, 443 | ELEMHIDE: 1073741824 444 | }; 445 | RegExpFilter.prototype.contentType &= ~ (RegExpFilter.typeMap.ELEMHIDE | RegExpFilter.typeMap.POPUP); 446 | 447 | function BlockingFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys, collapse) 448 | { 449 | RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys); 450 | this.collapse = collapse; 451 | } 452 | extend(BlockingFilter, RegExpFilter, { 453 | collapse: null 454 | }); 455 | 456 | function WhitelistFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys) 457 | { 458 | RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys); 459 | } 460 | extend(WhitelistFilter, RegExpFilter, { 461 | }); 462 | 463 | function Matcher() 464 | { 465 | this.clear(); 466 | } 467 | Matcher.prototype = { 468 | filterByKeyword: null, 469 | keywordByFilter: null, 470 | clear: function() 471 | { 472 | this.filterByKeyword = createDict(); 473 | this.keywordByFilter = createDict(); 474 | }, 475 | add: function(filter) 476 | { 477 | if (filter.text in this.keywordByFilter) 478 | { 479 | return; 480 | } 481 | var keyword = this.findKeyword(filter); 482 | var oldEntry = this.filterByKeyword[keyword]; 483 | if (typeof oldEntry == "undefined") 484 | { 485 | this.filterByKeyword[keyword] = filter; 486 | } 487 | else if (oldEntry.length == 1) 488 | { 489 | this.filterByKeyword[keyword] = [oldEntry, filter]; 490 | } 491 | else 492 | { 493 | oldEntry.push(filter); 494 | } 495 | this.keywordByFilter[filter.text] = keyword; 496 | }, 497 | remove: function(filter) 498 | { 499 | if (!(filter.text in this.keywordByFilter)) 500 | { 501 | return; 502 | } 503 | var keyword = this.keywordByFilter[filter.text]; 504 | var list = this.filterByKeyword[keyword]; 505 | if (list.length <= 1) 506 | { 507 | delete this.filterByKeyword[keyword]; 508 | } 509 | else 510 | { 511 | var index = list.indexOf(filter); 512 | if (index >= 0) 513 | { 514 | list.splice(index, 1); 515 | if (list.length == 1) 516 | { 517 | this.filterByKeyword[keyword] = list[0]; 518 | } 519 | } 520 | } 521 | delete this.keywordByFilter[filter.text]; 522 | }, 523 | findKeyword: function(filter) 524 | { 525 | var result = ""; 526 | var text = filter.text; 527 | if (Filter.regexpRegExp.test(text)) 528 | { 529 | return result; 530 | } 531 | var match = Filter.optionsRegExp.exec(text); 532 | if (match) 533 | { 534 | text = match.input.substr(0, match.index); 535 | } 536 | if (text.substr(0, 2) == "@@") 537 | { 538 | text = text.substr(2); 539 | } 540 | var candidates = text.toLowerCase().match(/[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g); 541 | if (!candidates) 542 | { 543 | return result; 544 | } 545 | var hash = this.filterByKeyword; 546 | var resultCount = 16777215; 547 | var resultLength = 0; 548 | for (var i = 0, l = candidates.length; i < l; i++) 549 | { 550 | var candidate = candidates[i].substr(1); 551 | var count = candidate in hash ? hash[candidate].length : 0; 552 | if (count < resultCount || count == resultCount && candidate.length > resultLength) 553 | { 554 | result = candidate; 555 | resultCount = count; 556 | resultLength = candidate.length; 557 | } 558 | } 559 | return result; 560 | }, 561 | hasFilter: function(filter) 562 | { 563 | return filter.text in this.keywordByFilter; 564 | }, 565 | getKeywordForFilter: function(filter) 566 | { 567 | if (filter.text in this.keywordByFilter) 568 | { 569 | return this.keywordByFilter[filter.text]; 570 | } 571 | else 572 | { 573 | return null; 574 | } 575 | }, 576 | _checkEntryMatch: function(keyword, location, contentType, docDomain, thirdParty, sitekey) 577 | { 578 | var list = this.filterByKeyword[keyword]; 579 | for (var i = 0; i < list.length; i++) 580 | { 581 | var filter = list[i]; 582 | if (filter == "#this") 583 | { 584 | filter = list; 585 | } 586 | if (filter.matches(location, contentType, docDomain, thirdParty, sitekey)) 587 | { 588 | return filter; 589 | } 590 | } 591 | return null; 592 | }, 593 | matchesAny: function(location, contentType, docDomain, thirdParty, sitekey) 594 | { 595 | var candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g); 596 | if (candidates === null) 597 | { 598 | candidates = []; 599 | } 600 | candidates.push(""); 601 | for (var i = 0, l = candidates.length; i < l; i++) 602 | { 603 | var substr = candidates[i]; 604 | if (substr in this.filterByKeyword) 605 | { 606 | var result = this._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, sitekey); 607 | if (result) 608 | { 609 | return result; 610 | } 611 | } 612 | } 613 | return null; 614 | } 615 | }; 616 | 617 | function CombinedMatcher() 618 | { 619 | this.blacklist = new Matcher(); 620 | this.whitelist = new Matcher(); 621 | this.resultCache = createDict(); 622 | } 623 | CombinedMatcher.maxCacheEntries = 1000; 624 | CombinedMatcher.prototype = { 625 | blacklist: null, 626 | whitelist: null, 627 | resultCache: null, 628 | cacheEntries: 0, 629 | clear: function() 630 | { 631 | this.blacklist.clear(); 632 | this.whitelist.clear(); 633 | this.resultCache = createDict(); 634 | this.cacheEntries = 0; 635 | }, 636 | add: function(filter) 637 | { 638 | if (filter instanceof WhitelistFilter) 639 | { 640 | this.whitelist.add(filter); 641 | } 642 | else 643 | { 644 | this.blacklist.add(filter); 645 | } 646 | if (this.cacheEntries > 0) 647 | { 648 | this.resultCache = createDict(); 649 | this.cacheEntries = 0; 650 | } 651 | }, 652 | remove: function(filter) 653 | { 654 | if (filter instanceof WhitelistFilter) 655 | { 656 | this.whitelist.remove(filter); 657 | } 658 | else 659 | { 660 | this.blacklist.remove(filter); 661 | } 662 | if (this.cacheEntries > 0) 663 | { 664 | this.resultCache = createDict(); 665 | this.cacheEntries = 0; 666 | } 667 | }, 668 | findKeyword: function(filter) 669 | { 670 | if (filter instanceof WhitelistFilter) 671 | { 672 | return this.whitelist.findKeyword(filter); 673 | } 674 | else 675 | { 676 | return this.blacklist.findKeyword(filter); 677 | } 678 | }, 679 | hasFilter: function(filter) 680 | { 681 | if (filter instanceof WhitelistFilter) 682 | { 683 | return this.whitelist.hasFilter(filter); 684 | } 685 | else 686 | { 687 | return this.blacklist.hasFilter(filter); 688 | } 689 | }, 690 | getKeywordForFilter: function(filter) 691 | { 692 | if (filter instanceof WhitelistFilter) 693 | { 694 | return this.whitelist.getKeywordForFilter(filter); 695 | } 696 | else 697 | { 698 | return this.blacklist.getKeywordForFilter(filter); 699 | } 700 | }, 701 | isSlowFilter: function(filter) 702 | { 703 | var matcher = filter instanceof WhitelistFilter ? this.whitelist : this.blacklist; 704 | if (matcher.hasFilter(filter)) 705 | { 706 | return !matcher.getKeywordForFilter(filter); 707 | } 708 | else 709 | { 710 | return !matcher.findKeyword(filter); 711 | } 712 | }, 713 | matchesAnyInternal: function(location, contentType, docDomain, thirdParty, sitekey) 714 | { 715 | var candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g); 716 | if (candidates === null) 717 | { 718 | candidates = []; 719 | } 720 | candidates.push(""); 721 | var blacklistHit = null; 722 | for (var i = 0, l = candidates.length; i < l; i++) 723 | { 724 | var substr = candidates[i]; 725 | if (substr in this.whitelist.filterByKeyword) 726 | { 727 | var result = this.whitelist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, sitekey); 728 | if (result) 729 | { 730 | return result; 731 | } 732 | } 733 | if (substr in this.blacklist.filterByKeyword && blacklistHit === null) 734 | { 735 | blacklistHit = this.blacklist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, sitekey); 736 | } 737 | } 738 | return blacklistHit; 739 | }, 740 | matchesAny: function(location, docDomain) 741 | { 742 | var key = location + " " + docDomain + " "; 743 | if (key in this.resultCache) 744 | { 745 | return this.resultCache[key]; 746 | } 747 | var result = this.matchesAnyInternal(location, 0, docDomain, null, null); 748 | if (this.cacheEntries >= CombinedMatcher.maxCacheEntries) 749 | { 750 | this.resultCache = createDict(); 751 | this.cacheEntries = 0; 752 | } 753 | this.resultCache[key] = result; 754 | this.cacheEntries++; 755 | return result; 756 | } 757 | }; 758 | var defaultMatcher = new CombinedMatcher(); 759 | 760 | var direct = 'DIRECT;'; 761 | 762 | for (var i = 0; i < rules.length; i++) { 763 | defaultMatcher.add(Filter.fromText(rules[i])); 764 | } 765 | 766 | function FindProxyForURL(url, host) { 767 | if (defaultMatcher.matchesAny(url, host) instanceof BlockingFilter) { 768 | return proxy; 769 | } 770 | return direct; 771 | } 772 | --------------------------------------------------------------------------------