├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── appveyor.yml ├── free_ss.js └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_style = tab 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.{cmd,bat}] 14 | end_of_line = crlf 15 | 16 | [README.md] 17 | tab_width = 2 18 | 19 | [{package.json,.travis.yml,appveyor.yml}] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "script", 4 | "impliedStrict": false 5 | }, 6 | "env": { 7 | "es6": true, 8 | "node": true 9 | }, 10 | "extends": [ 11 | "standard" 12 | ], 13 | "root": true, 14 | "rules": { 15 | "comma-dangle": [ 16 | "error", 17 | { 18 | "arrays": "always-multiline", 19 | "objects": "always-multiline", 20 | "imports": "always-multiline", 21 | "exports": "always-multiline", 22 | "functions": "never" 23 | } 24 | ], 25 | "indent": [ 26 | "error", 27 | "tab", 28 | { 29 | "SwitchCase": 1 30 | } 31 | ], 32 | "no-tabs": [ 33 | "off" 34 | ], 35 | "no-var": [ 36 | "error" 37 | ], 38 | "prefer-arrow-callback": [ 39 | "error" 40 | ], 41 | "prefer-const": [ 42 | "error" 43 | ], 44 | "quotes": [ 45 | "error", 46 | "double" 47 | ], 48 | "semi": [ 49 | "error", 50 | "always", 51 | { 52 | "omitLastInOneLineBlock": false 53 | } 54 | ], 55 | "strict": [ 56 | "error", 57 | "safe" 58 | ] 59 | }, 60 | "overrides": [ 61 | { 62 | "files": [ 63 | "test/**/*" 64 | ], 65 | "env": { 66 | "mocha": true 67 | }, 68 | "rules": { 69 | "no-template-curly-in-string": "off" 70 | } 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | 64 | # End of https://www.gitignore.io/api/node 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 刘祺 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | free-ss 2 | =============== 3 | [![AppVeyor](https://img.shields.io/appveyor/ci/gucong3000/free-ss.svg?&label=Windows)](https://ci.appveyor.com/project/gucong3000/free-ss) 4 | 5 | > 免费[Shadowsocks](https://github.com/shadowsocks/shadowsocks-windows/)账号自动配置工具。 6 | 7 | ## 安装 8 | 9 | ```bash 10 | npm i -g gucong3000/free_ss 11 | ``` 12 | 13 | ## 使用 14 | 15 | - [Shadowsocks](https://github.com/shadowsocks/shadowsocks-windows/)用户 16 | ```bash 17 | free-ss --ss 18 | ``` 19 | - [COW (Climb Over the Wall) proxy](https://github.com/cyfdecyf/cow)用户 20 | ```bash 21 | free-ss --cow 22 | ``` 23 | 24 | ## 功能 25 | - 自动同步[FreeSSR](https://freessr.win)、[iShadow](https://ss.ishadowx.net)其他网站上的免费账号至本地配置文件([gui-config.json](https://ci.appveyor.com/api/projects/gucong3000/free-ss/artifacts/gui-config.json)或[rc.txt](https://ci.appveyor.com/api/projects/gucong3000/free-ss/artifacts/rc.txt)). 26 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | skip_branch_with_pr: true 3 | skip_tags: true 4 | build: off 5 | 6 | environment: 7 | matrix: 8 | - nodejs_version: stable 9 | 10 | # Install scripts. (runs after repo cloning) 11 | install: 12 | # install Node.js 13 | - ps: Install-Product node $env:nodejs_version 14 | # install modules 15 | - npm install 16 | 17 | # to run your custom scripts instead of automatic tests 18 | test_script: 19 | - npm test 20 | 21 | # to run your custom scripts instead of provider deployments 22 | after_test: 23 | - appveyor PushArtifact rc.txt 24 | - appveyor PushArtifact gui-config.json 25 | -------------------------------------------------------------------------------- /free_ss.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | const path = require("path"); 5 | const got = require("got"); 6 | const os = require("os"); 7 | const fs = require("fs-extra"); 8 | const cowRcPath = os.platform() === "win32" ? "rc.txt" : path.join(os.homedir(), ".cow/rc"); 9 | const ssRcPath = "gui-config.json"; 10 | const { JSDOM } = require("jsdom"); 11 | 12 | // 可以抓取SS账号的网页,及其CSS选择符 13 | const srvs = { 14 | "http://55service.yaozeyuan.online:8733/whitelist/": "table", 15 | "https://ss.ishadowx.net": "#portfolio .hover-text", 16 | "https://freessr.win": ".text-center", 17 | }; 18 | 19 | // 中文所对应的配置项key名 20 | const keyMap = { 21 | "加密方式": "method", 22 | "服务器地址": "server", 23 | "服务地址": "server", 24 | "服务密码": "password", 25 | "服务器端口": "server_port", 26 | "服务端口": "server_port", 27 | "端口号": "server_port", 28 | "状态": "remarks", 29 | "ip address": "server", 30 | "port": "server_port", 31 | }; 32 | 33 | function tabel2config (table) { 34 | const server = {}; 35 | Array.from(table.querySelectorAll("tr")).forEach(tr => { 36 | if (tr.children.length === 2) { 37 | const key = getConfigKey(tr.children[0].innerHTML); 38 | const value = tr.children[1].innerHTML.trim(); 39 | if (key && value) { 40 | server[key] = value; 41 | } 42 | } 43 | }); 44 | return server; 45 | } 46 | 47 | function nodeText2config (node) { 48 | // 提取dom元素中的信息 49 | const text = (node.innerText || node.textContent).trim(); 50 | if (/\n/.test(text)) { 51 | // 一般的正常情况,按换行符分隔字符串即可 52 | node = text.split(/\s*\n\s*/g); 53 | } else { 54 | // 貌似jsDOM不支持innerText属性,所以采用分析子节点的办法 55 | node = Array.from(node.childNodes).filter(node => { 56 | return node.nodeType === 3; 57 | }).map(node => { 58 | return (node.innerText || node.textContent).trim(); 59 | }); 60 | } 61 | 62 | // 将提取到的信息,转为配置文件所需格式 63 | const server = {}; 64 | 65 | // 遍历每行信息 66 | node.forEach(inf => { 67 | // 按冒号分隔字符串 68 | inf = inf.split(/\s*[::]\s*/g); 69 | const key = getConfigKey(inf[0]); 70 | const val = inf[1]; 71 | if (key && val) { 72 | server[key] = val; 73 | } 74 | }); 75 | 76 | return server; 77 | } 78 | 79 | function node2config (node) { 80 | return node.tagName === "TABLE" ? tabel2config(node) : nodeText2config(node); 81 | } 82 | 83 | function getConfigKey (key) { 84 | if (!key) { 85 | return; 86 | } 87 | 88 | key = key.trim().toLowerCase(); 89 | 90 | if (!keyMap[key]) { 91 | if (/^\w+$/.test(key)) { 92 | return key; 93 | } 94 | 95 | key = Object.keys(keyMap).find(keyName => ( 96 | keyName.includes(key) 97 | )); 98 | 99 | return key && keyMap[key]; 100 | } 101 | return keyMap[key]; 102 | } 103 | 104 | function getServers () { 105 | return Promise.all(Object.keys(srvs).map(url => ( 106 | JSDOM.fromURL(url, { 107 | referrer: url, 108 | }).then(dom => ( 109 | Array.from( 110 | dom.window.document.querySelectorAll(srvs[url]) 111 | ).map(node2config) 112 | ), console.error) 113 | ))).then(servers => ( 114 | [].concat.apply([], servers).filter(server => { 115 | if (server && server.server && server.password) { 116 | server.server_port = server.server_port ? +server.server_port : 443; 117 | server.server = server.server.toLowerCase(); 118 | server.method = server.method ? server.method.toLowerCase() : "aes-256-cfb"; 119 | server.group = "free-ss"; 120 | return true; 121 | } 122 | }) 123 | )); 124 | } 125 | 126 | async function format (servers) { 127 | if (!servers.length) { 128 | throw new Error("未找到任何服务器。"); 129 | } 130 | 131 | const [ 132 | isCow, 133 | isSs, 134 | ] = await Promise.all([ 135 | process.argv.includes("--cow") || fs.exists(cowRcPath), 136 | process.argv.includes("--ss") || fs.exists(ssRcPath), 137 | ]); 138 | 139 | await Promise.all([ 140 | isCow && cow(servers), 141 | (isSs || !(isCow || isSs)) && ss(servers), 142 | ]); 143 | } 144 | 145 | async function ss (servers) { 146 | const config = await fs.readJSON(ssRcPath).then(config => { 147 | if (Array.isArray(config.configs)) { 148 | config.configs = config.configs.filter( 149 | server => server.group !== "free-ss" 150 | ).concat(servers); 151 | } else { 152 | config.configs = servers; 153 | } 154 | return config; 155 | }, () => ({ 156 | index: -1, 157 | shareOverLan: true, 158 | strategy: "com.shadowsocks.strategy.ha", 159 | configs: servers, 160 | })); 161 | 162 | await fs.writeJSON(ssRcPath, config, { 163 | EOL: os.EOL, 164 | spaces: "\t", 165 | }); 166 | return config; 167 | } 168 | 169 | async function cow (servers) { 170 | let config = await fs.readFile(cowRcPath, "utf-8").catch(() => ( 171 | "" 172 | )); 173 | 174 | config = config.replace(/^#\s*free-ss\s*start\s*$[\s\S]*?^#\s*free-ss\s*end\s*$/igm, "").trim(); 175 | if (!config) { 176 | config = [ 177 | "listen = http://0.0.0.0:1080", 178 | "loadBalance = latency", 179 | ].join(os.EOL); 180 | } 181 | config = [ 182 | config, 183 | "", 184 | "# free-ss start", 185 | ].concat(servers.map(server => ( 186 | `proxy = ss://${server.method}:${server.password}@${server.server}:${server.server_port}` 187 | ))).concat( 188 | "# free-ss end", 189 | "" 190 | ).join(os.EOL); 191 | 192 | await fs.writeFile(cowRcPath, config); 193 | return config; 194 | } 195 | 196 | function getServerFromCI () { 197 | if (process.env.CI) { 198 | return; 199 | } 200 | return got("https://ci.appveyor.com/api/projects/gucong3000/free-ss/artifacts/gui-config.json", { 201 | json: true, 202 | }).then(config => config.body.configs).catch(() => ({})); 203 | } 204 | 205 | async function getConfig () { 206 | let [ 207 | newServers, 208 | oldServers, 209 | ] = await Promise.all([ 210 | getServers(), 211 | getServerFromCI(), 212 | ]); 213 | 214 | if (oldServers) { 215 | oldServers = oldServers.filter(oldServer => ( 216 | !newServers.some(newServer => ( 217 | newServer.server === oldServer.server && newServer.server_port === oldServer.server_port 218 | )) 219 | )); 220 | if (oldServers.length) { 221 | newServers = newServers.concat(oldServers); 222 | } 223 | } 224 | return format(newServers); 225 | } 226 | 227 | if (process.mainModule === module) { 228 | getConfig().catch(console.error); 229 | } 230 | 231 | module.exports = getConfig; 232 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "free-ss", 3 | "description": "free shadowsocks config sync", 4 | "bin": { 5 | "free-ss": "./free_ss.js" 6 | }, 7 | "version": "0.1.0", 8 | "homepage": "https://github.com/gucong3000/free_ss", 9 | "author": "gucong (https://github.com/gucong3000)", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/gucong3000/free_ss.git" 13 | }, 14 | "keywords": [ 15 | "Sublime Text", 16 | "Chinese", 17 | "languages", 18 | "I18N" 19 | ], 20 | "licenses": [ 21 | { 22 | "type": "MIT", 23 | "url": "https://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt" 24 | } 25 | ], 26 | "dependencies": { 27 | "fs-extra": "^7.0.1", 28 | "got": "^9.3.2", 29 | "jsdom": "^13.0.0" 30 | }, 31 | "devDependencies": { 32 | "eslint": "^5.10.0", 33 | "eslint-config-standard": "^12.0.0", 34 | "eslint-plugin-import": "^2.14.0", 35 | "eslint-plugin-node": "^8.0.0", 36 | "eslint-plugin-promise": "^4.0.1", 37 | "eslint-plugin-standard": "^4.0.0" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/gucong3000/free_ss/issues" 41 | }, 42 | "main": "free_ss.js", 43 | "scripts": { 44 | "pretest": "eslint .", 45 | "test": "node . --cow --ss" 46 | }, 47 | "license": "MIT" 48 | } 49 | --------------------------------------------------------------------------------