├── .github └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .gitignore ├── .npmignore ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── assets ├── examples │ ├── auth.js │ ├── authInAuth.js │ ├── spoofRequest.js │ ├── spoofResponse.js │ ├── spoofResponseHttps.js │ ├── start.js │ ├── tcpOutgoing.js │ ├── trackOpenConnections.js │ └── upstream.js └── image_2022-04-16_10-35-45.png ├── index.mjs ├── lib ├── consts.mjs ├── log.mjs ├── socratex.mjs └── web.mjs ├── main.mjs ├── package.json └── test.mjs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package, Docker Image 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build-npm: 14 | runs-on: ubuntu-latest 15 | if: "!startsWith(github.event.head_commit.message, '[RELEASE]')" 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: 22 23 | registry-url: https://registry.npmjs.org/ 24 | - run: git config --global user.name 'Leask Wong' 25 | - run: git config --global user.email 'i@leaskh.com' 26 | - run: npm version patch -m "[RELEASE] %s" 27 | - run: git push 28 | - run: npm install 29 | - run: npm test 30 | - run: npm publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 33 | build-docker: 34 | needs: build-npm 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v3 38 | - name: Build the Docker image 39 | run: | 40 | docker build . --no-cache --file Dockerfile --tag leask/socratex 41 | docker login --username ${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }} 42 | docker push leask/socratex 43 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '17 12 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v2 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # IDE - VSCode 107 | .history/* 108 | 109 | # OS 110 | .DS_Store 111 | 112 | # Leask 113 | debug.mjs 114 | package-lock.json 115 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .history 2 | /.github 3 | /assets 4 | /build.mjs 5 | /debug.mjs 6 | /Dockerfile 7 | /keys/README.md 8 | /package-lock.json 9 | /test.mjs 10 | /webpack.config.mjs 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:current-alpine 2 | 3 | LABEL org.opencontainers.image.version=v1.x.y 4 | LABEL org.opencontainers.image.title=Socratex 5 | LABEL org.opencontainers.image.description="A Secure Web Proxy. Which is fast, secure, and easy to use." 6 | LABEL org.opencontainers.image.url="https://github.com/Leask/socratex" 7 | LABEL org.opencontainers.image.documentation="https://github.com/Leask/socratex" 8 | LABEL org.opencontainers.image.vendor="@LeaskH" 9 | LABEL org.opencontainers.image.licenses=MIT 10 | LABEL org.opencontainers.image.source="https://github.com/Leask/socratex" 11 | 12 | RUN npm install -g socratex 13 | 14 | EXPOSE 80 15 | EXPOSE 443 16 | 17 | ENTRYPOINT ["socratex"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Leask Wong 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 | # Socratex 2 | 3 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue)](./LICENSE) 4 | [![Node.js Package, Docker Image](https://github.com/Leask/socratex/actions/workflows/build.yml/badge.svg)](https://github.com/Leask/socratex/actions/workflows/build.yml) 5 | [![CodeQL](https://github.com/Leask/socratex/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/Leask/socratex/actions/workflows/codeql-analysis.yml) 6 | 7 | A Secure Web Proxy. Which is fast, secure, and easy to use. 8 | 9 | *This project is under active development. Everything may change soon.* 10 | 11 | 12 | 13 | Socratex extends the native [net.createServer](https://nodejs.org/api/net.html#net_net_createserver_options_connectionlistener), and it acts as a real transparent HTTPS-proxy built on top of TCP-level. 14 | 15 | It's a real HTTPS proxy, not HTTPS over HTTP. It allows upstream client-request dynamically to other proxies or works as a single layer encrypted proxy. 16 | 17 | Socratex will request and set up the certificate automatically, and it will automatically renew the certificate when it expires. You don't need to worry about the dirty work about HTTPS/SSL. 18 | 19 | It supports [Basic Proxy-Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authorization) and Token-Based-Authentication as default. Socratex will create a new token at the first run, you don't need to worry about it. 20 | 21 | Screen Shot 2022-04-15 at 8 47 01 PM 22 | 23 | 24 | ## Why another proxy? 25 | 26 | First of all, many people in particular countries need proxy software that is easy to deploy and could be used to secure their network traffic. Second, because of the limitation on App Store, especially in China, VPN and proxy software are not allowed to be used. So we need to find a way to avoid censorship without any client apps. Secure Web Proxy is the only choice and a promising one. 27 | 28 | 29 | ## Deploy a Secure Web Proxy within 10 seconds 30 | 31 | You need a domain name and set an A-record pointed to your cloud virtual machine. 32 | 33 | Usually, that virtual machine can not be located in China. 34 | 35 | Assumes that you have a workable Node.js (v16 or above) environment. 36 | 37 | Now let's make the magic happen! 38 | 39 | - Modern method: 40 | ```bash 41 | $ sudo su 42 | # cd ~ 43 | # npx socratex --domain=example.com --bypass=cn 44 | ``` 45 | - Classic method: 46 | ```bash 47 | $ git clone git@github.com:Leask/socratex.git 48 | $ cd socratex 49 | $ npm install 50 | $ sudo ./main.mjs --domain=example.com --bypass=cn 51 | ``` 52 | - With Docker: 53 | ```bash 54 | $ touch ~/.socratex.json 55 | $ docker pull leask/socratex 56 | $ docker run -d --restart=always -p 80:80 -p 443:443 \ 57 | -v ~/.socratex.json:/root/.socratex.json \ 58 | leask/socratex --domain=example.com --bypass=cn 59 | ``` 60 | 61 | If everything works fine, you should see a message like this: 62 | 63 | ``` 64 | [SOCRATEX Vx.y.z] https://github.com/Leask/socratex 65 | [SOCRATEX] Secure Web Proxy started at https://example.com:443 (IPv6 ::). 66 | [SOCRATEX] HTTP Server started at http://example.com:80 (IPv6 ::). 67 | [SSL] Creating new private-key and CSR... 68 | [SSL] Done. 69 | [SSL] Updating certificate... 70 | [SSL] Done. 71 | [SOCRATEX] * Token authentication: 72 | [SOCRATEX] - PAC: https://example.com/proxy.pac?token=959c298e-9f38-b201-2e7e-14af54469889 73 | [SOCRATEX] - WPAD: https://example.com/wpad.dat?token=959c298e-9f38-b201-2e7e-14af54469889 74 | [SOCRATEX] - Log: https://example.com/console?token=959c298e-9f38-b201-2e7e-14af54469889 75 | [SOCRATEX] * Basic authentication: 76 | [SOCRATEX] - PAC: https://foo:bar@example.com/proxy.pac 77 | [SOCRATEX] - WPAD: https://foo:bar@example.com/wpad.dat 78 | [SOCRATEX] - Log: https://foo:bar@example.com/console 79 | [SOCRATEX] - Proxy: https://foo:bar@example.com 80 | ``` 81 | 82 | Copy the `PAC url` or `WPAD url` and paste it into your system's `Automatic Proxy Configuration` settings. That is all you need to do. 83 | 84 | Screen Shot 2022-04-15 at 5 26 22 PM 85 | 86 | Screen Shot 2022-04-15 at 5 25 41 PM 87 | 88 | *Note*: You can also use the `log url` to monitor the system's activity. 89 | 90 | 91 | ## Command line args 92 | 93 | All args are optional. In most cases, you just need to set the domain name. Of cause, you can also set the `bypass` countries to reduce proxy traffics. 94 | 95 | | Param | Type | Description | 96 | | ------ | ------------------- | ------------ | 97 | | domain | String | Domain to deploy the proxy. | 98 | | http | With/Without | Use HTTP-only-mode for testing only. | 99 | | bypass | String | Bypass IPs in these countries, could be multiple, example: --bypass=CN --bypass=US | 100 | | user | String | Use `user` and `password` to enable Basic Authorization. | 101 | | password | String | Use `user` and `password` to enable Basic Authorization. | 102 | | token | String | Use to enable Token Authorization. | 103 | | address | String | Activate/Handle Proxy-Authentication. Returns or solves to Boolean. | 104 | | port | Number | Default 443 to handle incoming connection. | 105 | 106 | 107 | ## Limitations 108 | 109 | ### Why not use `sudo npx ...` directly? 110 | 111 | Socratex works at default HTTP (80) and HTTPS (443) ports. You need to be root to listen to these ports on some systems. Because of this issue: https://github.com/npm/cli/issues/3110, if you are in a folder NOT OWN by root, you CAN NOT use `sudo npm ...` or `sudo npx ...` directly to run socratex. 112 | 113 | ### Why doesn't work with iOS? 114 | 115 | Socratex can be used with `macOS`, `Chrome OS`, `Windows`, `Linux` and `Android`. But it's NOT compatible with iOS currently. Because iOS does not support `Secure Web Proxy` yet. I will keep an eye on this issue and try any possible walk-around solutions. 116 | 117 | 118 | ## Why name it `Socratex`? 119 | 120 | `Socratex` was named after `Socrates,` a Greek philosopher from Athens credited as the founder of Western philosophy and among the first moral philosophers of the ethical tradition of thought. 121 | 122 | [![Socrates](https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/David_-_The_Death_of_Socrates.jpg/2880px-David_-_The_Death_of_Socrates.jpg)](https://en.wikipedia.org/wiki/Socrates) 123 | 124 | *Image credit: The Death of Socrates, by Jacques-Louis David (1787)* 125 | 126 | 127 |
128 | Programmable proxy 129 | 130 | ## Programmable proxy 131 | 132 | ``` 133 | //////////////////////////////////////////////////////////////////////////////// 134 | // NO NEED TO READ ANYTHING BELOW IF YOU ARE NOT GOING TO CUSTOMIZE THE PROXY // 135 | //////////////////////////////////////////////////////////////////////////////// 136 | ``` 137 | 138 | You can also use socratex as a programmable proxy to meet your own needs. 139 | 140 | ```bash 141 | $ npm i -s socratex 142 | ``` 143 | 144 | Socratex is an ES6 module, so you can use it in your modern Node.js projects. 145 | 146 | ```javascript 147 | import { Socratex } from 'socratex'; 148 | 149 | const [port, address, options] = ['4698', '': {}]; 150 | 151 | const socratex = new Socratex(options); 152 | 153 | socratex.listen(port, address, async () => { 154 | console.log('TCP-Proxy-Server started at: ', server.address()); 155 | }); 156 | ``` 157 | 158 | 159 | ### Options object use to customize the proxy 160 | 161 | `options` should be an object. 162 | 163 | | Param | Type | Description | 164 | | ------ | ------------------- | ------------ | 165 | | basicAuth | Function/AsyncFunction | Activate/Handle Proxy-Authentication. Returns or solves to Boolean. | 166 | | tokenAuth | Function/AsyncFunction | Activate/Handle Proxy-Authentication. Returns or solves to Boolean. | 167 | | upstream | Function/AsyncFunction | The proxy to be used to upstreaming requests. Returns String. | 168 | | tcpOutgoingAddress | Function/AsyncFunction | The localAddress to use while sending requests. Returns String. | 169 | | injectData | Function/AsyncFunction | The edited data to upstream. Returns Buffer or string. | 170 | | injectResponse | Function/AsyncFunction | The edited response to return to connected client. Returns Buffer or string. | 171 | | keys | Function/AsyncFunction | The keys to use while handshake. It will work only if intercept is true. Returns Object or false. | 172 | | logLevel | Number | Default 0 to log all messages. | 173 | | intercept | Boolean | Activate interception of encrypted communications. False as default. | 174 | 175 | 176 | ### `upstream`, `tcpOutgoingAddress`, `injectData` & `injectResponse` options 177 | 178 | The options are functions having follow parameters: 179 | 180 | | Param | Type | Description | 181 | | ------ | ------------------- | ------------ | 182 | | data | Buffer | The received data. | 183 | | session | Session | Object containing info/data about Tunnel. | 184 | 185 | - upstream-Function need to return/resolve a String with format -> `IP:PORT` or `USER:PWD@IP:PORT` of used http-proxy. If *'localhost'* is returned/resolved, then the host-self will be used as proxy. 186 | - tcpOutgoingAddress-Function need to return a String with format -> `IP`. 187 | - injectData-Function need to return a String or buffer for the new spoofed data. This will be upstreamed as request. 188 | - injectResponse-Function need to return a String or buffer for the new received data. 189 | 190 | *Note*: These functions will be executed before first tcp-socket-connection is established. 191 | 192 | 193 | ### Upstream to other proxies 194 | 195 | If you don't want to use the host of active instance self, then you need to upstream connections to another http-proxy. 196 | This can be done with `upstream` attribute. 197 | 198 | ```javascript 199 | const options = { 200 | upstream: async () => { return 'x.x.x.x:3128'; }, 201 | }; 202 | ``` 203 | 204 | ### The Basic Authorization mechanism 205 | 206 | This activate basic authorization mechanism. 207 | The Auth-function will be executed while handling Proxy-Authentications. 208 | 209 | | Param | Type | Description | 210 | | ------ | ------------------- | ------------ | 211 | |username | String | The client username. | 212 | |password | String | The client password | 213 | |session | Session | Object containing info/data about Tunnel | 214 | 215 | *Note*: It needs to return True/False or a **Promise** that resolves to boolean (*isAuthenticated*). 216 | 217 | ```javascript 218 | const options = { 219 | basicAuth: async (user, password) => user === 'bar' && password === 'foo'; 220 | }; 221 | ``` 222 | 223 | ### The Token Authorization mechanism 224 | 225 | This activate token authorization mechanism. 226 | The Auth-function will be executed while handling Proxy-Authentications. 227 | 228 | | Param | Type | Description | 229 | | ------ | ------------------- | ------------ | 230 | | token | String | The client token. | 231 | | session | Session | Object containing info/data about Tunnel | 232 | 233 | *Note*: It needs to return True/False or a **Promise** that resolves to boolean (*isAuthenticated*). 234 | 235 | ```javascript 236 | const options = { 237 | tokenAuth: async (token) => token === 'a-very-long-token'; 238 | }; 239 | ``` 240 | 241 | ### Interception 242 | 243 | This feature is in very early stage, and it's for web development only. The callbacks `injectData` & `injectResponse` could be used to intercept/spoof communication. These functions are executed with the `data` and `session` arguments. 244 | 245 | ### Intercepting HTTPS 246 | 247 | The boolean attribute `intercept` allows to break SSL-Communication between Source & Destination. This will activate Security-Alarm by most used browsers. 248 | 249 | ```javascript 250 | const [uaToSwitch, switchWith] = ['curl 7.79.1', 'a-fake-user-agent']; 251 | const options = { 252 | intercept: true, 253 | injectData(data, session) { 254 | if (session.isHttps && data.toString().match(uaToSwitch)) { 255 | return Buffer.from(data.toString().replace(uaToSwitch, switchWith)); 256 | } 257 | return data; 258 | }, 259 | }; 260 | ``` 261 | 262 | ```bash 263 | curl -x localhost:8080 -k http://ifconfig.io/ua 264 | curl 7.79.1 265 | 266 | curl -x localhost:8080 -k https://ifconfig.me/ua 267 | a-fake-user-agent 268 | ``` 269 | 270 | 271 | ### The `keys` Function 272 | 273 | You can use this option to provide your own self-signed certificate. 274 | 275 | If activated needs to return an Object `{key:'String', cert:'String'}` like [native tls_connect_options.key & tls_connect_options.cert](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) or `false` statement. 276 | 277 | If no object is returned, then [default keys](https://github.com/Leask/socratex/tree/main/keys) will be used to update communication. 278 | 279 | | Param | Type | Description | 280 | | ------ | ------------------- | ------------ | 281 | | session | Session | Object containing info/data about Tunnel. | 282 | 283 | *Note*: This function will be executed before TLS-Handshake. 284 | 285 | ### Session-instance 286 | 287 | The Session-instance is a Object containing info/data about Tunnel. 288 | 289 | Use `.getConnections()` to get the current connections. 290 | 291 | ```javascript 292 | setInterval(() => { 293 | const connections = socratex.getConnections(); 294 | console.log([new Date()], 'OPEN =>', Object.keys(connections).length) 295 | }, 3000); 296 | ``` 297 | 298 | The connection items in the connections array include useful attributes/methods: 299 | 300 | - isHttps - Is session encrypted. 301 | - getTunnelStats() - Get Stats for this tunnel 302 | - getId() - Get Own ID-Session 303 | - isAuthenticated() - Is the session authenticated by user or not. 304 | - ... (More APIS tobe documented) 305 | 306 | 307 | ### Dynamically routing 308 | 309 | This example upstreams only requests for ifconfig.me to another proxy, for all other requests will be used localhost. 310 | 311 | ```javascript 312 | const options = { 313 | upstream(data, session) { 314 | return data.toString().includes('ifconfig.me') 315 | ? 'x.x.x.x:3128' : 'localhost'; 316 | }, 317 | }); 318 | ``` 319 | 320 | Testing with `curl`: 321 | 322 | ```bash 323 | curl -x 127.0.0.1:8080 https://ifconfig.me 324 | x.x.x.x 325 | 326 | curl -x 127.0.0.1:8080 https://ifconfig.co 327 | y.y.y.y 328 | ``` 329 | 330 |
331 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /assets/examples/auth.js: -------------------------------------------------------------------------------- 1 | const { Socratex } = require('../../index.mjs'); 2 | 3 | //init Socratex 4 | const server = new Socratex({ 5 | auth: function(username, password) { 6 | console.log('Proxy-Auth', { username, password }); 7 | return username === 'bar' && password === 'foo'; 8 | } 9 | }); 10 | 11 | //starting server on port 1555 12 | server.listen(8080, '0.0.0.0', function() { 13 | console.log('TCP-Proxy-Server started!', server.address()); 14 | }); 15 | -------------------------------------------------------------------------------- /assets/examples/authInAuth.js: -------------------------------------------------------------------------------- 1 | const { Socratex } = require('../../index.mjs'); 2 | const util = require('util'); 3 | const exec = util.promisify(require('child_process').exec); 4 | 5 | const toTest = [ 6 | 'http://ifconfig.me', 7 | 'https://ifconfig.me', 8 | 'http://icanhazip.com', 9 | 'http://ifconfig.co', 10 | 'https://ifconfig.co', 11 | ]; 12 | 13 | //init Socratex 14 | const firstSocratex = new Socratex({ 15 | auth: function(username, password) { 16 | return (username === 'test' && password === 'testPWD'); 17 | } 18 | }); 19 | const firstPort = 10001; 20 | //starting server on port 10001 21 | firstSocratex.listen(firstPort, '0.0.0.0', async function() { 22 | console.log('socratex was started!', firstSocratex.address()); 23 | }); 24 | 25 | 26 | //init Socratex2 27 | const secondSocratex = new Socratex({ 28 | upstream: function() { 29 | return 'test:testPWD@0.0.0.0:' + firstPort; 30 | } 31 | }); 32 | const secondPort = 10002; 33 | //starting server on port 10001 34 | secondSocratex.listen(secondPort, '0.0.0.0', async function() { 35 | console.log('2 socratex was started!', secondSocratex.address()); 36 | 37 | for (const singlePath of toTest) { 38 | const cmd = 'curl' + ' -x localhost:' + secondPort + ' ' + singlePath; 39 | console.log(cmd); 40 | const { stdout, stderr } = await exec(cmd); 41 | console.log('Response =>', stdout); 42 | } 43 | 44 | secondSocratex.close(); 45 | firstSocratex.close(); 46 | }); 47 | -------------------------------------------------------------------------------- /assets/examples/spoofRequest.js: -------------------------------------------------------------------------------- 1 | const { Socratex } = require('../../index.mjs'); 2 | const util = require('util'); 3 | const exec = util.promisify(require('child_process').exec); 4 | 5 | const toTest = ['http://ifconfig.io/ua', 'https://ifconfig.me/ua']; 6 | 7 | const uaToSwitch = 'curl/7.55.1'; 8 | const switchWith = 'My Super Fucking Spoofed UA!'; 9 | 10 | const server = new Socratex({ 11 | intercept: true, 12 | injectData: (data, session) => { 13 | if (session.isHttps) { 14 | // console.log('SESSION-DATA', data.toString()) //you can spoof here 15 | if (data.toString().match(uaToSwitch)) { 16 | const newData = Buffer.from(data.toString() 17 | .replace(uaToSwitch, switchWith)); 18 | 19 | // console.log('data', data.toString()); 20 | // console.log('newData', newData.toString()); 21 | return newData; 22 | } 23 | } 24 | return data; 25 | } 26 | }); 27 | 28 | const port = 10001; 29 | //starting server on port 10001 30 | server.listen(port, '0.0.0.0', async function() { 31 | console.log('socratex was started!', server.address()); 32 | 33 | for (const singlePath of toTest) { 34 | const cmd = 'curl' + ' -x localhost:' + port + ' -k ' + singlePath; 35 | console.log(cmd); 36 | const { stdout, stderr } = await exec(cmd) 37 | .catch((err) => ({ stdout: err.message })); 38 | console.log('Response =>', stdout); 39 | } 40 | server.close(); 41 | }); 42 | 43 | // curl -x localhost:10001 http://ifconfig.io/ua 44 | // Response => My Super Fucking Spoofed UA! 45 | // 46 | // curl -x localhost:10001 https://ifconfig.io/ua 47 | // Response => curl/7.55.1 48 | -------------------------------------------------------------------------------- /assets/examples/spoofResponse.js: -------------------------------------------------------------------------------- 1 | const { Socratex } = require('../../index.mjs'); 2 | const util = require('util'); 3 | const exec = util.promisify(require('child_process').exec); 4 | 5 | const toTest = ['http://v4.ident.me/', 'https://v4.ident.me/']; 6 | 7 | const ipToSwitch = 'x.x.x.x'; 8 | const switchWith = 'bla.bla.bla.bla'; 9 | 10 | const server = new Socratex({ 11 | injectResponse: (data, session) => { 12 | if (!session.isHttps) { 13 | //you can spoof here 14 | if (data.toString().match(ipToSwitch)) { 15 | const newData = Buffer.from(data.toString() 16 | .replace(new RegExp('Content-Length: ' + ipToSwitch.length, 'gmi'), 17 | 'Content-Length: ' + (switchWith.length)) 18 | .replace(ipToSwitch, switchWith)); 19 | 20 | return newData; 21 | } 22 | } 23 | return data; 24 | } 25 | }); 26 | 27 | const port = 10001; 28 | //starting server on port 10001 29 | server.listen(port, '0.0.0.0', async function() { 30 | console.log('socratex was started!', server.address()); 31 | 32 | for (const singlePath of toTest) { 33 | const cmd = 'curl' + ' -x localhost:' + port + ' ' + singlePath; 34 | console.log(cmd); 35 | const { stdout, stderr } = await exec(cmd); 36 | console.log('Response =>', stdout); 37 | } 38 | server.close(); 39 | }); 40 | 41 | // curl -x localhost:10001 http://v4.ident.me/ 42 | // Response => bla.bla.bla.bla 43 | // curl -x localhost:10001 https://v4.ident.me/ 44 | // Response => x.x.x.x 45 | -------------------------------------------------------------------------------- /assets/examples/spoofResponseHttps.js: -------------------------------------------------------------------------------- 1 | const { Socratex } = require('../../index.mjs'); 2 | const util = require('util'); 3 | const exec = util.promisify(require('child_process').exec); 4 | 5 | const toTest = ['http://v4.ident.me/', 'https://v4.ident.me/']; 6 | 7 | const server = new Socratex({ 8 | intercept: true, 9 | injectResponse: (data, session) => { 10 | const ipToSwitch = 'x.x.x.x'; 11 | const switchWithIp = '6.6.6.6'; 12 | // console.log('session.isHttps', session.isHttps) 13 | if (session.isHttps) { 14 | const newData = Buffer.from(data.toString() 15 | .replace(new RegExp('Content-Length: ' + ipToSwitch.length, 'gmi'), 16 | 'Content-Length: ' + (switchWithIp.length)) 17 | .replace(ipToSwitch, switchWithIp)); 18 | return newData; 19 | } 20 | return data; 21 | } 22 | }); 23 | 24 | const port = 10001; 25 | //starting server on port 10001 26 | server.listen(port, '0.0.0.0', async function() { 27 | console.log('socratex was started!', server.address()); 28 | 29 | for (const singlePath of toTest) { 30 | const cmd = 'curl' + ' -x localhost:' + port + ' -k ' + singlePath; 31 | console.log(cmd); 32 | const { stdout, stderr } = await exec(cmd); 33 | console.log('Response =>', stdout); 34 | } 35 | server.close(); 36 | }); 37 | -------------------------------------------------------------------------------- /assets/examples/start.js: -------------------------------------------------------------------------------- 1 | const { Socratex } = require('../index.mjs'); 2 | 3 | //init Socratex 4 | const server = new Socratex({}); 5 | 6 | //starting server on port 1555 7 | server.listen(8080, '0.0.0.0', function() { 8 | console.log('TCP-Proxy-Server started!', server.address()); 9 | }); 10 | -------------------------------------------------------------------------------- /assets/examples/tcpOutgoing.js: -------------------------------------------------------------------------------- 1 | const { Socratex } = require('../index.mjs'); 2 | 3 | //init Socratex 4 | const server = new Socratex({ 5 | tcpOutgoingAddress: function(data, connection) { 6 | return 'x.x.x.x'; //using other iFace as default 7 | } 8 | }); 9 | 10 | //starting server on port 1555 11 | server.listen(8080, 'y.y.y.y', function() { 12 | console.log('socratex Server was started!', server.address()); 13 | }); 14 | -------------------------------------------------------------------------------- /assets/examples/trackOpenConnections.js: -------------------------------------------------------------------------------- 1 | const { Socratex } = require('../../index.mjs'); 2 | 3 | const server = new Socratex(); 4 | 5 | //starting server on port 1555 6 | server.listen(1555, '0.0.0.0', function() { 7 | console.log('TCP-Proxy-Server started!', server.address()); 8 | }); 9 | 10 | setInterval(function showOpenSockets() { 11 | const connections = server.getConnections(); 12 | console.log([new Date()], 'OPEN =>', Object.keys(connections).length) 13 | }, 2000); 14 | -------------------------------------------------------------------------------- /assets/examples/upstream.js: -------------------------------------------------------------------------------- 1 | const { Socratex } = require('../../index.mjs'); 2 | 3 | function sleep(ms) { 4 | return new Promise(function(res, rej) { 5 | setTimeout(res, ms); 6 | }); 7 | } 8 | 9 | const server = new Socratex({ 10 | upstream: async function(data, connection) { 11 | // await sleep(1000); 12 | if (~(data.toString().indexOf('ifconfig.me'))) { 13 | return 'x.x.x:10001'; //upstream to myProxy 14 | } 15 | else if (~(data.toString().indexOf('ifconfig.co'))) { 16 | return 'x.x.x:10002'; //upstream to another proxy 17 | } 18 | else { 19 | return 'localhost'; // upstreaming to localhost 20 | } 21 | }, 22 | }); 23 | 24 | //starting server on port 1555 25 | server.listen(8080, '0.0.0.0', function() { 26 | console.log('TCP-Proxy-Server started!', server.address()); 27 | }); 28 | -------------------------------------------------------------------------------- /assets/image_2022-04-16_10-35-45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leask/socratex/a7c359290c642f40963601c7dc94d255ba34e8fc/assets/image_2022-04-16_10-35-45.png -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | import * as consts from './lib/consts.mjs'; 2 | import * as web from './lib/web.mjs'; 3 | import Log from './lib/log.mjs'; 4 | import Socratex from './lib/socratex.mjs'; 5 | 6 | export default Socratex; 7 | export { consts, Log, Socratex, web }; 8 | -------------------------------------------------------------------------------- /lib/consts.mjs: -------------------------------------------------------------------------------- 1 | const [AT, BLANK, CLRF, H11] = ['@', ' ', '\r\n', 'HTTP/1.1 ']; 2 | const [DOUBLE_CLRF, EMPTY, SEPARATOR] = [CLRF + CLRF, '', ':']; 3 | const EVENTS = { CLOSE: 'close', DATA: 'data', ERROR: 'error', EXIT: 'exit' }; 4 | const [HTTP, HTTPS, PROXY] = ['http', 'https', 'PROXY']; 5 | const HTTP_METHODS = { CONNECT: 'CONNECT', GET: 'GET' }; 6 | const [HTTP_PORT, HTTPS_PORT] = [80, 443]; 7 | const [HTTP_AUTH, HTTP_AUTH_BASIC] = ['Authorization', 'Basic']; 8 | const PROXY_AUTH = 'Proxy-Authorization'; 9 | const [SLASH, SLASH_REGEXP, SLASH_REGEXP_ONCE] = ['/', /\//gmi, /\//g]; 10 | const STATUS = { AUTH: 407, UNAUTH: 401 }; 11 | const [IDLE_CLIENT_TIMEOUT, MAX_CLIENT_COUNT] = [1000 * 60 * 60 * 24 * 7, 1000]; 12 | const MIME_TYPES = { PAC: 'application/x-ns-proxy-autoconfig' }; 13 | const HEADERS = {}; 14 | 15 | for (let i in MIME_TYPES) { HEADERS[i] = `Content-Type: ${MIME_TYPES[i]}`; } 16 | 17 | const DEFAULT_OPTIONS = { 18 | auth: false, 19 | https: false, 20 | injectData: false, 21 | injectResponse: false, 22 | intercept: false, 23 | keys: false, 24 | logLevel: 0, 25 | port: HTTP_PORT, 26 | tcpOutgoingAddress: false, 27 | upstream: false, 28 | }; 29 | 30 | const ERROR_CODES = { 31 | ENOTFOUND: 'ENOTFOUND', 32 | EPIPE: 'EPIPE', 33 | EPROTO: 'EPROTO', 34 | ETIMEDOUT: 'ETIMEDOUT', 35 | }; 36 | 37 | const HTTP_BODIES = { 38 | UNAUTHORIZED: 'Unauthorized', 39 | AUTH_REQUIRED: 'Proxy Authorization Required', 40 | NOT_FOUND: 'Not Found', 41 | TOO_MANY_REQ: 'Too Many Requests', 42 | }; 43 | 44 | const HTTP_RESPONSES = { 45 | NOT_FOUND: `${H11}404 Not Found`, 46 | NOT_OK: `${H11}400 Bad Request`, 47 | OK: `${H11}200 OK`, 48 | TIMED_OUT: `${H11}408 Request Timeout`, 49 | UNAUTHORIZED: `${H11}${STATUS.UNAUTH} Unauthorized`, 50 | TOO_MANY_REQ: `${H11}429 Too Many Requests`, 51 | AUTH_REQUIRED: `${H11}${STATUS.AUTH} Proxy Authorization Required` 52 | + `${CLRF}Proxy-Authenticate: Basic realm=""`, 53 | }; 54 | 55 | export { 56 | AT, 57 | BLANK, 58 | CLRF, 59 | DEFAULT_OPTIONS, 60 | DOUBLE_CLRF, 61 | EMPTY, 62 | ERROR_CODES, 63 | EVENTS, 64 | HEADERS, 65 | HTTP_AUTH_BASIC, 66 | HTTP_AUTH, 67 | HTTP_BODIES, 68 | HTTP_METHODS, 69 | HTTP_PORT, 70 | HTTP_RESPONSES, 71 | HTTP, 72 | HTTPS_PORT, 73 | HTTPS, 74 | IDLE_CLIENT_TIMEOUT, 75 | MAX_CLIENT_COUNT, 76 | MIME_TYPES, 77 | PROXY_AUTH, 78 | PROXY, 79 | SEPARATOR, 80 | SLASH_REGEXP_ONCE, 81 | SLASH_REGEXP, 82 | SLASH, 83 | STATUS, 84 | }; 85 | -------------------------------------------------------------------------------- /lib/log.mjs: -------------------------------------------------------------------------------- 1 | import { callosum, color, manifest, utilitas } from 'utilitas'; 2 | 3 | const [MAX_LOG_CACHE, LOG_KEY] = [50, 'LOG']; 4 | const get = async () => (await callosum.get(LOG_KEY)) || []; 5 | 6 | const push = (content, filename, options) => { 7 | options = options || []; 8 | const isErr = Error.isError(content); 9 | content = Object.isObject(content) ? JSON.stringify(content) : content; 10 | const strTime = options.time ? ` ${(Date.isDate( 11 | options.time, true 12 | ) ? options.time : new Date()).toISOString()}` : ''; 13 | const args = ['[' + color.red( 14 | utilitas.basename(filename).toUpperCase() 15 | ) + color.yellow(strTime) + ']' + (isErr ? '' : ` ${content}`)]; 16 | if (isErr) { args.push(content); } 17 | callosum.push(LOG_KEY, args.map( 18 | item => color.strip(utilitas.toString(item)) 19 | ), { bulk: true, shift: MAX_LOG_CACHE }); 20 | return console.info.apply(null, args); 21 | }; 22 | 23 | class Log { 24 | _name = ''; 25 | _logLevels = ['log', 'info', 'debug', 'warn', 'error']; 26 | _logLevel = 0; 27 | _options = { time: true }; 28 | _shouldLog(lv) { return ~~this._logLevels.indexOf(lv) >= ~~this._logLevel }; 29 | _log(args) { return push(args, this._name, this._options); }; 30 | log(args) { return this._shouldLog('log') && this._log(args); }; 31 | error(args) { return this._shouldLog('error') && this._log(args); }; 32 | constructor({ name = '', logLevel = 0 }) { 33 | Object.assign(this, { _name: name, _logLevel: ~~logLevel }); 34 | }; 35 | }; 36 | 37 | const render = async (log) => { 38 | const [pack, asArray, rM] = [await utilitas.which(), { asArray: true }, {}]; 39 | ['description', 'homepage', 'author', 'license'].map(k => rM[k] = pack[k]); 40 | const [content, rndOpt] = [ 41 | utilitas.renderObject({ ...rM, utilitas: manifest.version }, asArray), 42 | { title: `${pack.title} Console `, ...asArray }, 43 | ]; 44 | return [ 45 | '', 46 | '', 47 | '', 48 | ' ', 49 | ' Socratex Console', 50 | ' ', 51 | '', 52 | '', 53 | '
',
54 |         ...utilitas.renderBox(content, rndOpt).map(utilitas.escapeHtml),
55 |         '',
56 |         `latest ${MAX_LOG_CACHE} logs:`,
57 |         ...utilitas.renderCode([...log || await get()], asArray),
58 |         '    
', 59 | '', 60 | '', 61 | ].join('\n'); 62 | }; 63 | 64 | export default Log; 65 | export { 66 | MAX_LOG_CACHE, 67 | get, 68 | Log, 69 | push, 70 | render, 71 | }; 72 | -------------------------------------------------------------------------------- /lib/socratex.mjs: -------------------------------------------------------------------------------- 1 | import { createConnection, createServer } from 'net'; 2 | import { lookup } from 'fast-geoip'; 3 | import { TLSSocket } from 'tls'; 4 | import { ssl, utilitas } from 'utilitas'; 5 | import Log from './log.mjs'; 6 | 7 | import { 8 | AT, BLANK, CLRF, DEFAULT_OPTIONS, DOUBLE_CLRF, EMPTY, EVENTS, 9 | HTTP_AUTH_BASIC, HTTP_AUTH, HTTP_BODIES, HTTP_METHODS, HTTP_PORT, 10 | HTTP_RESPONSES, HTTP, HTTPS_PORT, HTTPS, PROXY_AUTH, SEPARATOR, 11 | SLASH_REGEXP, SLASH, STATUS, 12 | } from './consts.mjs'; 13 | 14 | import { 15 | error, getSecurityLog as _getSecurityLog, route, setBypassList, 16 | setSecurityLog as _setSecurityLog, 17 | } from './web.mjs'; 18 | 19 | const { CLOSE, DATA, ERROR, EXIT } = EVENTS; 20 | const { CONNECT } = HTTP_METHODS; 21 | const SESSION_SYNC_INTERVAL = 1000 * 10; 22 | const { AUTH_REQUIRED, OK, UNAUTHORIZED } = HTTP_RESPONSES; 23 | const socketCheck = socket => socket && !socket.destroyed ? socket : null; 24 | const socketWrite = (socket, dt) => dt && socketCheck(socket)?.write?.(dt); 25 | const socketDestroy = socket => socketCheck(socket)?.destroy?.(); 26 | const getTokenRegExp = () => new RegExp('^.*token=([\\da-z\-]*).*$', 'ig'); 27 | 28 | const parseHeaders = (data) => { 29 | // @todo: make secure 30 | const [headers, body] = data.toString().split(CLRF + CLRF + CLRF); 31 | const [headerRows, headerObject] = [headers.split(CLRF), {}]; 32 | for (let i = 0; i < headerRows.length; i++) { 33 | const headerRow = headerRows[i]; 34 | if (i === 0) { 35 | // first row contain method, path and type 36 | // const [method, path, version] = headerRow.split(BLANK); 37 | // headerObject.method = method; 38 | // headerObject.path = path; 39 | // headerObject.version = version; 40 | } else { 41 | const [attribute, value] = headerRow.split(SEPARATOR); 42 | if (attribute && value) { 43 | const lowerAttribute = attribute.trim().toLowerCase(); 44 | headerObject[lowerAttribute] = value.trim(); 45 | } 46 | } 47 | } 48 | return headerObject; 49 | }; 50 | 51 | const rebuildHeaders = (headersObject, dataBuffer) => { 52 | const [headers, body] = dataBuffer.toString().split(DOUBLE_CLRF + CLRF, 2); 53 | const firstRow = headers.split(CLRF, 1)[0]; 54 | let newData = `${firstRow}${CLRF}`; 55 | Object.keys(headersObject).map(key => 56 | newData += `${key}${SEPARATOR}${BLANK}${headersObject[key]}${CLRF}` 57 | ); 58 | newData += `${DOUBLE_CLRF}${body || ''}`; 59 | return Buffer.from(newData); 60 | }; 61 | 62 | const select = async (upstream, { data, connection }) => { 63 | if (Function.isFunction(upstream)) { 64 | let returnValue = upstream(data, connection); 65 | if (returnValue instanceof Promise) { 66 | returnValue = await returnValue; 67 | } 68 | if (returnValue !== 'localhost') { 69 | return returnValue; 70 | } 71 | } 72 | return false; 73 | }; 74 | 75 | const getAddressAndPortFromString = (ipStringWithPort) => { 76 | let [credentials, targetHost] = ipStringWithPort.split(AT); 77 | if (!targetHost) { [targetHost, credentials] = [credentials, '']; } 78 | let [protocol, host, port] = targetHost.split(SEPARATOR); 79 | if (protocol.indexOf(HTTP) === -1) { 80 | [port, host] = [host, protocol]; 81 | protocol = (port && parseInt(port) === HTTPS_PORT) ? HTTPS : HTTP; 82 | } 83 | host = (host) ? host : protocol.replace(SLASH_REGEXP, EMPTY); 84 | if (host.indexOf(SLASH + SLASH) === 0) { host = host.split(SLASH)[2]; } 85 | else { host = host.split(SLASH)[0]; } 86 | port = port || ( 87 | protocol && ~protocol.indexOf(HTTPS) ? HTTPS_PORT : HTTP_PORT 88 | ); 89 | return JSON.parse(JSON.stringify({ 90 | host, port: parseInt(port), protocol, 91 | credentials: credentials || undefined 92 | })); 93 | }; 94 | 95 | // Build options for native nodejs tcp-connection. 96 | const getConnectionOptions = (proxyToUse, upstreamHost) => { 97 | if (!utilitas.isAscii(upstreamHost)) { return false; } 98 | const upstreamed = !!proxyToUse; 99 | const upstreamToUse = upstreamed ? proxyToUse : upstreamHost; 100 | const config = getAddressAndPortFromString(upstreamToUse); 101 | const result = { ...config, ...{ upstreamed } }; 102 | if (result.upstreamed) { 103 | result.upstream = getAddressAndPortFromString(upstreamHost); 104 | } 105 | return result.port >= 0 && result.port < 65536 ? result : false; 106 | }; 107 | 108 | const onConnect = async (clientSocket, connections, options, logger) => { 109 | const { 110 | basicAuth, https, injectData, injectResponse, intercept, 111 | keys, tcpOutgoingAddress, tokenAuth, upstream, 112 | } = options; 113 | const remoteId = [ 114 | clientSocket.remoteAddress, clientSocket.remotePort 115 | ].join(SEPARATOR); 116 | const querySession = () => connections[remoteId]; 117 | const getSecurityLog = async () => 118 | await _getSecurityLog(clientSocket.remoteAddress); 119 | const getStatus = () => `(${Object.keys(connections).length}) ${remoteId} `; 120 | // options.debug && logger.log(`New connection from: ${remoteId}`); 121 | 122 | const setSecurityLog = (user, lastVisit) => 123 | querySession().setUserAuthentication(user) 124 | && (!lastVisit || (new Date() - lastVisit > SESSION_SYNC_INTERVAL)) 125 | && _setSecurityLog(clientSocket.remoteAddress); 126 | 127 | const isAuthenticated = async () => { 128 | let r; 129 | if ((r = querySession()?.isAuthenticated?.())) { return r; } 130 | else if ((r = await getSecurityLog())) { return new Date(r.lastVisit); } 131 | return false; 132 | }; 133 | 134 | const throwUnauted = () => utilitas.throwError( 135 | HTTP_BODIES.UNAUTHORIZED, STATUS.UNAUTH, { code: UNAUTHORIZED } 136 | ); 137 | 138 | const throwAuth = () => utilitas.throwError( 139 | HTTP_BODIES.AUTH_REQUIRED, STATUS.AUTH, { code: AUTH_REQUIRED } 140 | ); 141 | 142 | const destroy = () => { 143 | if (querySession()) { 144 | querySession().destroy(); 145 | delete connections[remoteId]; 146 | } 147 | return querySession(); 148 | }; 149 | 150 | const onClose = async (err) => { 151 | const tunnel = querySession(); 152 | if (err && err instanceof Error) { 153 | const { status, body } = await error(err); 154 | tunnel.clientResponseWrite(`${status}${DOUBLE_CLRF}${body}`); 155 | logger.error(`${getStatus()}E: ${err?.message || err}`); 156 | options?.debug && console.error(err); 157 | } 158 | destroy(); 159 | }; 160 | 161 | const onDataFromUpstream = async (dataFromUpStream) => { 162 | const tunnel = querySession(); 163 | const responseData = Function.isFunction(injectResponse) 164 | ? injectResponse(dataFromUpStream, tunnel) : dataFromUpStream; 165 | tunnel.clientResponseWrite(responseData); 166 | await updateSockets(); // updateSockets if needed after first response 167 | }; 168 | 169 | const onDirectConnectionOpen = (srcData) => { 170 | const tunnel = querySession(); 171 | const requestData = Function.isFunction(injectData) 172 | ? injectData(srcData, tunnel) : srcData; 173 | tunnel.clientRequestWrite(requestData); 174 | }; 175 | 176 | const updateSockets = async () => { 177 | const tunnel = querySession(); 178 | if (intercept && tunnel && tunnel.isHttps && !tunnel._updated) { 179 | const keysObject = Function.isFunction(keys) ? keys(tunnel) : false; 180 | const keyToUse = ( 181 | keysObject 182 | && typeof keysObject === 'object' 183 | && Object.keys(keysObject).length === 2 184 | ) ? keysObject : undefined; 185 | await tunnel._updateSockets({ 186 | onDataFromClient, onDataFromUpstream, onClose 187 | }, keyToUse); 188 | } 189 | }; 190 | 191 | const prepareTunnel = async (data, firstHeaderRow, isConnectMethod = 0) => { 192 | const tunnel = querySession(); 193 | const firstHeaders = firstHeaderRow.split(BLANK); 194 | const upstreamHost = firstHeaders[1]; 195 | const initOpt = getConnectionOptions(false, upstreamHost); 196 | tunnel.setTunnelOpt(initOpt); //settings opt before callback 197 | const proxy = await select(upstream, { data, connection: tunnel }); 198 | //initializing socket and forwarding received request 199 | const connectionOpt = getConnectionOptions(proxy, upstreamHost); 200 | tunnel.isHttps = !!(isConnectMethod || ( 201 | connectionOpt.upstream && connectionOpt.upstream.protocol === HTTPS 202 | )); 203 | tunnel.setTunnelOpt(connectionOpt); // updating tunnel opt 204 | // ONLY work if server-listener is not 0.0.0.0 but specific iFace/IP 205 | if (Function.isFunction(tcpOutgoingAddress)) { 206 | connectionOpt.localAddress = tcpOutgoingAddress(data, tunnel); 207 | } 208 | 209 | const onOpen = async (connectionError) => { 210 | const { ADDRESS, PORT } = tunnel.getTunnelStats(); 211 | const country = (await lookup(tunnel._dst.remoteAddress))?.country; 212 | options?.bypass?.has?.(country) && setBypassList(ADDRESS); 213 | logger.log(`${getStatus()}=> [${country}] ` 214 | + `${tunnel._dst.remoteAddress}:${PORT} ~ ${ADDRESS}`); 215 | if (connectionError) { return await onClose(connectionError); } 216 | if (isConnectMethod && !connectionOpt.upstreamed) { 217 | tunnel.clientResponseWrite(OK + CLRF + CLRF); 218 | return await updateSockets(); // response as normal http-proxy 219 | } 220 | if (!connectionOpt.credentials) { 221 | return onDirectConnectionOpen(data); 222 | } 223 | const headers = parseHeaders(data); 224 | const basedCredentials 225 | = utilitas.base64Encode(connectionOpt.credentials); 226 | headers[PROXY_AUTH.toLowerCase()] 227 | = HTTP_AUTH_BASIC + BLANK + basedCredentials; 228 | tunnel.clientRequestWrite(rebuildHeaders(headers, data)); 229 | }; 230 | 231 | if (connectionOpt?.host && connectionOpt?.port) { 232 | const responseSocket = createConnection(connectionOpt, onOpen); 233 | tunnel.setRequestSocket(responseSocket 234 | .on(DATA, onDataFromUpstream) 235 | .on(CLOSE, onClose) 236 | .on(ERROR, onClose) 237 | ); 238 | } else { 239 | const { status, body } = await route(...firstHeaders); 240 | tunnel.clientResponseWrite(`${status}${DOUBLE_CLRF}${body}`); 241 | destroy(); 242 | } 243 | return connectionOpt; 244 | }; 245 | 246 | const handleProxyTunnel = (split, data) => { 247 | const firstHeaderRow = split[0]; 248 | const tunnel = querySession(); // managing HTTP-Tunnel(upstream) & HTTPs 249 | if (~firstHeaderRow.indexOf(CONNECT)) { 250 | return prepareTunnel(data, firstHeaderRow, true); 251 | } else if (firstHeaderRow.indexOf(CONNECT) === -1 && !tunnel._dst) { 252 | return prepareTunnel(data, firstHeaderRow); // managing http 253 | } else if (tunnel && tunnel._dst) { 254 | return onDirectConnectionOpen(data); 255 | } 256 | }; 257 | 258 | const onDataFromClient = async (data) => { 259 | const dataString = data.toString(); 260 | const tunnel = querySession(); 261 | try { // @todo: make secure, split can be limited 262 | if (!dataString || !dataString.length) { return; } 263 | const headers = parseHeaders(data); 264 | const split = dataString.split(CLRF); 265 | const token = getTokenRegExp().test(split[0]) 266 | && split[0].replace(getTokenRegExp(), '$1'); 267 | const pxAth = headers[PROXY_AUTH.toLowerCase()] 268 | || headers[HTTP_AUTH.toLowerCase()]; 269 | let lastVisit; 270 | if ((lastVisit = await isAuthenticated())) { 271 | setSecurityLog('[TRANSFERRED]', lastVisit); 272 | } else if (Function.isFunction(tokenAuth) && token) { 273 | (await utilitas.resolve(tokenAuth(token, tunnel))) || throwUnauted(); 274 | setSecurityLog('[TOKEN]'); 275 | } else if (Function.isFunction(basicAuth) && pxAth) { 276 | pxAth || throwUnauted(); 277 | const credentials = pxAth.replace(HTTP_AUTH_BASIC, EMPTY); 278 | const parsedCdt = Buffer.from(credentials, 'base64').toString(); 279 | const [user, passwd] = parsedCdt.split(SEPARATOR); 280 | (await utilitas.resolve(basicAuth(user, passwd, tunnel))) || throwUnauted(); 281 | setSecurityLog(user); // @todo: split can be limited 282 | } else if (Function.isFunction(tokenAuth) || Function.isFunction(basicAuth)) { 283 | throwAuth(); 284 | }; 285 | return handleProxyTunnel(split, data); 286 | } catch (err) { return await onClose(err); } 287 | }; 288 | 289 | connections[remoteId] = new Session(remoteId); // initializing connection 290 | querySession().setResponseSocket( 291 | (https ? new TLSSocket(clientSocket, { 292 | rejectUnauthorized: false, requestCert: false, isServer: true, 293 | ...await ssl.getCert(), 294 | }) : clientSocket).on(DATA, onDataFromClient) 295 | .on(ERROR, onClose) 296 | .on(CLOSE, onClose) 297 | .on(EXIT, onClose) 298 | ); 299 | }; 300 | 301 | class Session extends Object { 302 | clientRequestWrite(data) { socketWrite(this._dst, data); return this; }; 303 | clientResponseWrite(data) { socketWrite(this._src, data); return this; }; 304 | isAuthenticated() { return this.authenticated; } 305 | setResponseSocket(socket) { this._src = socket; return this; } 306 | setRequestSocket(socket) { this._dst = socket; return this; } 307 | getId() { return this._id; } 308 | getTunnelStats() { return this._tunnel; } 309 | destroy() { 310 | socketDestroy(this._dst); socketDestroy(this._src); return this; 311 | }; 312 | constructor(id) { 313 | super(); 314 | Object.assign(this, { 315 | _dst: null, _id: id, _src: null, _tunnel: {}, 316 | authenticated: false, isHttps: false, user: null, 317 | }); 318 | }; 319 | setUserAuthentication(user) { 320 | return Object.assign(this, user ? { 321 | authenticated: new Date(), user: this.user || user 322 | } : {}); 323 | }; 324 | setTunnelOpt(options) { 325 | if (options) { 326 | const { host, port, upstream } = options; 327 | this._tunnel.ADDRESS = host; 328 | this._tunnel.PORT = port; 329 | if (!!upstream) { this._tunnel.UPSTREAM = upstream; } 330 | } 331 | return this; 332 | }; 333 | _updateSockets = async (events, keys) => { 334 | const { onDataFromClient, onDataFromUpstream, onClose } = events; 335 | if (!this._updated) { // no need to update if working as a https proxy 336 | if (!this._src.ssl) { 337 | this.setResponseSocket(new TLSSocket(this._src, { 338 | rejectUnauthorized: false, requestCert: false, 339 | isServer: true, ...keys || await ssl.getCert(), 340 | }).on(DATA, onDataFromClient) 341 | .on(CLOSE, onClose) 342 | .on(ERROR, onClose)); 343 | } 344 | this.setRequestSocket(new TLSSocket(this._dst, { 345 | rejectUnauthorized: false, requestCert: false, isServer: false, 346 | }).on(DATA, onDataFromUpstream) 347 | .on(CLOSE, onClose) 348 | .on(ERROR, onClose)); 349 | this._updated = true; 350 | } 351 | return this; 352 | }; 353 | }; 354 | 355 | class Socratex extends createServer { 356 | getConnections() { return this.connections; }; 357 | 358 | constructor(options) { 359 | options = { ...DEFAULT_OPTIONS, ...options || {} }; 360 | const logger = new Log({ 361 | logLevel: ~~options.logLevel, name: options?.name || import.meta.url 362 | }); 363 | const connections = {}; 364 | super(clntSocket => onConnect(clntSocket, connections, options, logger)); 365 | this.connections = connections; 366 | }; 367 | }; 368 | 369 | export default Socratex; 370 | -------------------------------------------------------------------------------- /lib/web.mjs: -------------------------------------------------------------------------------- 1 | import { callosum, event, utilitas } from 'utilitas'; 2 | import { render } from './log.mjs'; 3 | 4 | import { 5 | CLRF, ERROR_CODES, HEADERS, HTTP_BODIES, HTTP_METHODS, HTTP_RESPONSES, 6 | IDLE_CLIENT_TIMEOUT, MAX_CLIENT_COUNT 7 | } from './consts.mjs'; 8 | 9 | const [SECURITY_LOG, BYPASS_LIST, MAX_BYPASS_COUNT] 10 | = ['SECURITY_LOG', 'BYPASS_LIST', 1000]; 11 | const { AUTH_REQUIRED, NOT_FOUND, NOT_OK, OK, TIMED_OUT, UNAUTHORIZED, } 12 | = HTTP_RESPONSES; 13 | const { ENOTFOUND, EPIPE, EPROTO, ETIMEDOUT } = ERROR_CODES; 14 | const { GET } = HTTP_METHODS; 15 | const packResult = (status, body) => ({ status, body: body || '' }); 16 | const log = message => utilitas.log(message, import.meta.url, { time: true }); 17 | const lastVisit = date => ({ lastVisit: date ? new Date(date) : new Date() }); 18 | const getSecurityLog = async address => await getMapping(SECURITY_LOG, address); 19 | const getBypassList = async address => await getMapping(BYPASS_LIST, address); 20 | const getBypassHost = async () => Object.keys(await getBypassList()); 21 | const setSecurityLog = async (add, d) => await setMapping(SECURITY_LOG, add, d); 22 | const setBypassList = async (add, d) => await setMapping(BYPASS_LIST, add, d); 23 | const getAllMapping = () => Promise.all([getSecurityLog(), getBypassList()]); 24 | 25 | function FindProxyForURL(url, host) { 26 | 27 | // Socratex by @LeaskH 28 | // https://github.com/Leask/socratex 29 | 30 | const [local, bypass] = [[ 31 | '*.lan', '*.local', '*.internal', 32 | '10.*.*.*', '127.*.*.*', '172.16.*.*', '192.168.*.*', 33 | ], { 34 | /*BYPASS*/ 35 | }]; 36 | 37 | for (let item of local) { if (shExpMatch(host, item)) { return 'DIRECT'; } } 38 | if (isPlainHostName(host) || bypass[host]) { return 'DIRECT'; } 39 | return '/*PROXY*/'; 40 | 41 | }; 42 | 43 | const setMapping = async (key, address, date) => await callosum.assign( 44 | key, { [address]: lastVisit(date) } 45 | ); 46 | 47 | const getMapping = async (key, address) => { 48 | const resp = await callosum.get(key, ...address ? [address] : []); 49 | return resp || (address ? null : {}); 50 | }; 51 | 52 | const makePac = (bypass, proxy) => { 53 | let [rules, pac] = [{ 54 | '/*BYPASS*/': bypass.map(x => `'${x}': 1`).join(',\n '), 55 | '/*PROXY*/': proxy, // '; DIRECT' // @todo by @LeaskH 56 | }, FindProxyForURL.toString()]; 57 | for (let i in rules) { 58 | pac = pac.replace(new RegExp(RegExp.escape(i), 'ig'), rules[i]); 59 | } 60 | return pac; 61 | }; 62 | 63 | const route = async (method, path, protocol, req) => { 64 | const objUrl = new URL(path, `https://${globalThis._socratex?.domain}`); 65 | const token = objUrl.searchParams.get('token'); 66 | const authenticated = token === globalThis._socratex?.token; 67 | const reeStr = [ 68 | String(method).toUpperCase(), String(objUrl.pathname).toLowerCase(), 69 | ].join(' '); 70 | switch (reeStr) { 71 | case `${GET} /`: 72 | return packResult(OK, '42'); 73 | case `${GET} /favicon.ico`: 74 | return packResult(OK, ''); 75 | case `${GET} /console`: 76 | return authenticated 77 | ? packResult(OK, await render()) 78 | : await error({ code: UNAUTHORIZED }); 79 | case `${GET} /proxy.pac`: 80 | case `${GET} /wpad.dat`: 81 | return authenticated ? packResult(`${OK}${CLRF}${HEADERS.PAC}`, 82 | makePac(await getBypassHost(), _socratex.address) 83 | ) : await error({ code: UNAUTHORIZED }); 84 | default: 85 | return await error({ code: ENOTFOUND }); 86 | } 87 | }; 88 | 89 | const error = async (err) => { 90 | switch (err.code) { 91 | case ETIMEDOUT: 92 | return packResult(TIMED_OUT); 93 | case ENOTFOUND: 94 | return packResult(NOT_FOUND, HTTP_BODIES.NOT_FOUND); 95 | case EPROTO: 96 | return packResult(NOT_OK, HTTP_BODIES.NOT_FOUND); 97 | case UNAUTHORIZED: 98 | return packResult(UNAUTHORIZED, HTTP_BODIES.UNAUTHORIZED); 99 | case AUTH_REQUIRED: 100 | return packResult(AUTH_REQUIRED, HTTP_BODIES.AUTH_REQUIRED); 101 | case EPIPE: default: 102 | return packResult(NOT_OK); 103 | } 104 | }; 105 | 106 | const init = async (options) => { 107 | Function.isFunction(options.getStatus) && (async () => { 108 | const config = (await utilitas.resolve(options.getStatus()))?.config; 109 | const sLog = config?.securityLog || {}; 110 | const list = config?.bypassList || {}; 111 | for (let i in sLog) { await setSecurityLog(i, sLog[i].lastVisit); } 112 | for (let i in list) { await setBypassList(i, list[i].lastVisit); } 113 | const [securityLog, bypassList] = await getAllMapping(); 114 | log(`Restored ${Object.keys(securityLog).length} session(s), ` 115 | + `${Object.keys(bypassList).length} bypass-item(s).`); 116 | })(); 117 | const releaseClient = async (add) => { 118 | await callosum.del(SECURITY_LOG, add); 119 | log(`Released inactive client: ${add}.`); 120 | }; 121 | const removeBypass = async (host) => { 122 | await callosum.del(BYPASS_LIST, host); 123 | log(`Remove inactive bypass-item: ${host}.`); 124 | }; 125 | const cleanStatus = async () => { 126 | const [now, ips, hosts] = [new Date(), [], []]; 127 | let [securityLog, bypassList] = await getAllMapping(); 128 | // clean security log 129 | for (let a in securityLog) { 130 | if (securityLog[a].lastVisit + IDLE_CLIENT_TIMEOUT < now) { 131 | await releaseClient(a); 132 | } else { ips.push([securityLog[a].lastVisit, a]); } 133 | } 134 | ips.sort((a, b) => a[0] - b[0]); 135 | while (ips.length > MAX_CLIENT_COUNT) { 136 | await releaseClient(ips.shift()[1]); 137 | } 138 | // clean bypass list 139 | for (let h in bypassList) { hosts.push([bypassList[h].lastVisit, h]); } 140 | hosts.sort((a, b) => a[0] - b[0]); 141 | while (hosts.length > MAX_BYPASS_COUNT) { 142 | await removeBypass(hosts.shift()[1]); 143 | } 144 | // save 145 | [securityLog, bypassList] = await getAllMapping(); 146 | if (!Function.isFunction(options.setStatus)) { return; } 147 | await options.setStatus({ securityLog, bypassList }); 148 | log(`Saved ${Object.keys(securityLog).length} session(s), ` 149 | + `${Object.keys(bypassList).length} bypass-item(s).`); 150 | }; 151 | return await event.loop( 152 | cleanStatus, 60, 60, 0, utilitas.basename(import.meta.url), 153 | { silent: true } 154 | ); 155 | }; 156 | 157 | export default init; 158 | export { 159 | error, 160 | getBypassHost, 161 | getBypassList, 162 | getSecurityLog, 163 | init, 164 | route, 165 | setBypassList, 166 | setSecurityLog, 167 | }; 168 | -------------------------------------------------------------------------------- /main.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { callosum, manifest, ssl, storage, uoid, utilitas } from 'utilitas'; 4 | import { consts, Socratex, web } from './index.mjs'; 5 | import { parseArgs } from 'node:util'; // https://kgrz.io/node-has-native-arg-parsing.html 6 | 7 | // parse args 8 | const argsOptions = { 9 | address: { type: 'string', short: 'a', default: '' }, 10 | bypass: { type: 'string', short: 'b', default: '' }, 11 | debug: { type: 'boolean', short: 'd', default: false }, 12 | domain: { type: 'string', short: 'o', default: '' }, 13 | help: { type: 'boolean', short: 'h', default: false }, 14 | http: { type: 'boolean', short: 't', default: false }, 15 | password: { type: 'string', short: 'p', default: '' }, 16 | port: { type: 'string', short: 'o', default: '0' }, 17 | repl: { type: 'boolean', short: 'r', default: false }, 18 | token: { type: 'string', short: 'k', default: '' }, 19 | user: { type: 'string', short: 'u', default: '' }, 20 | version: { type: 'boolean', short: 'v', default: false }, 21 | }; 22 | const { values } = parseArgs({ options: argsOptions }); 23 | const argv = { 24 | ...values, port: ~~values.port, 25 | getStatus: storage.getConfig, 26 | setStatus: storage.setConfig, 27 | bypass: values.bypass && utilitas.uniqueArray(values.bypass).map(i => i.toUpperCase()), 28 | }; 29 | 30 | // constants 31 | await utilitas.locate(utilitas.__(import.meta.url, 'package.json')); // keep 1st 32 | const meta = await utilitas.which(); 33 | const setConfig = async cf => callosum.isPrimary && await storage.setConfig(cf); 34 | const renderObject = obj => utilitas.renderObject(obj, { asArray: true }); 35 | const log = message => utilitas.log(message, meta.name); 36 | const logWithTime = message => utilitas.log(message, meta.name, { time: true }); 37 | const warning = message => utilitas.log(message, 'WARNING'); 38 | 39 | const getAddress = (ptcl, server) => { 40 | const { address, family, port } = server.address(); 41 | const add = `${ptcl}://${_socratex.domain}:${port} (${family} ${address})`; 42 | return { address, family, port, add }; 43 | }; 44 | 45 | const ensureDomain = async () => { 46 | if (argv.domain) { 47 | await setConfig({ domain: argv.domain }); 48 | return argv.domain; 49 | } 50 | return (await storage.getConfig())?.config?.domain || '127.0.0.1'; 51 | }; 52 | 53 | const ensureToken = async () => { 54 | let token = (await storage.getConfig())?.config?.token; 55 | if (!token) { 56 | token = uoid.fakeUuid(); 57 | await setConfig({ token }); 58 | } 59 | return token; 60 | }; 61 | 62 | const ensureBasicAuth = async () => { 63 | const optsTrim = { trim: true }; 64 | argv.user = utilitas.ensureString(argv.user, optsTrim); 65 | argv.password = utilitas.ensureString(argv.password, optsTrim); 66 | if (argv.user && argv.password) { 67 | const basicAuth = { user: argv.user, password: argv.password }; 68 | await setConfig(basicAuth); 69 | return basicAuth; 70 | } 71 | const { user, password } = (await storage.getConfig())?.config || {}; 72 | return { user, password }; 73 | }; 74 | 75 | // commands 76 | if (argv.help) { 77 | [meta.title, '', `Usage: ${meta.name} [options]`, ''].map(x => console.log(x)); 78 | console.table(argsOptions); 79 | process.exit(); 80 | } else if (argv.version) { 81 | [meta.title, `${manifest.name} v${manifest.version}`].map(x => console.log(x)); 82 | process.exit(); 83 | } 84 | 85 | // init 86 | globalThis._socratex = { 87 | https: argv.https = !argv.http, domain: await ensureDomain(), 88 | }; 89 | const port = argv.port || (_socratex.https ? consts.HTTPS_PORT : consts.HTTP_PORT); 90 | Object.assign(_socratex, { 91 | token: await ensureToken(), ...await ensureBasicAuth(), address: ( 92 | _socratex.https ? consts.HTTPS.toUpperCase() : consts.PROXY 93 | ) + ` ${_socratex.domain}:${port}`, 94 | }); 95 | _socratex.user && _socratex.password && ( 96 | argv.basicAuth = async (username, password) => { 97 | const result = utilitas.insensitiveCompare(username, _socratex.user) 98 | && password === _socratex.password; 99 | logWithTime( 100 | `Authenticate ${result ? 'SUCCESS' : 'FAILED'} => ` 101 | + `${username}:${utilitas.mask(password)}.` 102 | ); 103 | return result; 104 | } 105 | ); 106 | _socratex.token && ( 107 | argv.tokenAuth = async (token) => { 108 | const result = token === _socratex.token; 109 | logWithTime( 110 | `Authenticate ${result ? 'SUCCESS' : 'FAILED'} => ` 111 | + `TOKEN:${utilitas.mask(token)}.` 112 | ); 113 | return result; 114 | } 115 | ); 116 | 117 | // launch 118 | await callosum.init({ 119 | initPrimary: async () => { 120 | const subAdd = `${_socratex.https ? consts.HTTPS : consts.HTTP}://`; 121 | let webAdd = `${subAdd}${_socratex.domain}`; 122 | let bscAdd = `${subAdd}${_socratex.user}:${_socratex.password}@${_socratex.domain}`; 123 | if (_socratex.https && port === consts.HTTPS_PORT) { } 124 | else if (!_socratex.https && port === consts.HTTP_PORT) { } 125 | else { 126 | const tailPort = `:${port}`; 127 | webAdd += tailPort; 128 | bscAdd += tailPort; 129 | } 130 | const content = []; 131 | [[true, 'Token authentication', { 132 | ' - PAC': `${webAdd}/proxy.pac?token=${_socratex.token}`, 133 | ' - WPAD': `${webAdd}/wpad.dat?token=${_socratex.token}`, 134 | ' - Log': `${webAdd}/console?token=${_socratex.token}`, 135 | }], [_socratex.user && _socratex.password, 'Basic authentication', { 136 | ' - PAC': `${bscAdd}/proxy.pac`, 137 | ' - WPAD': `${bscAdd}/wpad.dat`, 138 | ' - Log': `${bscAdd}/console`, 139 | ' - Proxy': `${bscAdd}`, 140 | }], [true, 'Get help', { 141 | ' - GitHub': 'https://github.com/Leask/socratex', 142 | ' - Email': 'Leask Wong ', 143 | }]].map((x, k) => x[0] && content.push( 144 | ...~~k ? [''] : [], `* ${x[1]}:`, ...renderObject(x[2])) 145 | ); 146 | console.log(utilitas.renderBox(content, { title: meta?.title, width: 120 })); 147 | }, 148 | initWorker: async () => { 149 | globalThis.socratex = new Socratex(argv); 150 | return await new Promise((resolve, _) => { 151 | socratex.listen(port, argv.address, () => { 152 | const { add } = getAddress( 153 | _socratex.https ? consts.HTTPS : consts.HTTP, socratex 154 | ); 155 | return resolve({ 156 | message: `${_socratex.https 157 | ? 'Secure ' : ''}Web Proxy started at ${add}.` 158 | }); 159 | }); 160 | }); 161 | }, 162 | onReady: async () => { 163 | if (_socratex.https) { 164 | ssl.isLocalhost(_socratex.domain) 165 | ? warning(`Using self-signed certificate for ${_socratex.domain}.`) 166 | : await ssl.init(_socratex.domain, { debug: argv.debug }); 167 | } else { warning('HTTP-only mode is not recommended.'); } 168 | web.init(argv); 169 | argv.repl && (await import('repl')).start('> '); 170 | }, 171 | }); 172 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socratex", 3 | "description": "A Secure Web Proxy. Which is fast, secure, and easy to use.", 4 | "version": "2.0.20", 5 | "private": false, 6 | "homepage": "https://github.com/Leask/socratex", 7 | "main": "index.mjs", 8 | "type": "module", 9 | "bin": { 10 | "socratex": "main.mjs" 11 | }, 12 | "engines": { 13 | "node": ">=19.x" 14 | }, 15 | "scripts": { 16 | "start": "node main.mjs", 17 | "debug": "node --inspect --trace-warnings main.mjs --http --debug", 18 | "test": "node --inspect --trace-warnings test.mjs", 19 | "updep": "npx ncu -u && npm install", 20 | "gitsync": "( git commit -am \"Released @ `date`\" || true ) && git pull && git push", 21 | "build": "npm run updep && ( git commit -am 'update dependencies' || true )", 22 | "pub": "npm run build && npm run gitsync", 23 | "beta": "npm publish --tag beta", 24 | "docker-build": "docker build --no-cache -t leask/socratex .", 25 | "docker-push": "docker push leask/socratex", 26 | "docker-publish": "npm run docker-build && npm run docker-push" 27 | }, 28 | "keywords": [ 29 | "http-proxy", 30 | "mitm", 31 | "proxy", 32 | "secure", 33 | "tcp", 34 | "transparent", 35 | "tunnel" 36 | ], 37 | "author": "Leask Wong ", 38 | "license": "MIT", 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/Leask/socratex.git" 42 | }, 43 | "dependencies": { 44 | "acme-client": "^5.0.0", 45 | "fast-geoip": "^1.1.88", 46 | "utilitas": "^1995.1.6" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test.mjs: -------------------------------------------------------------------------------- 1 | import child_process from 'child_process'; 2 | import Socratex from './index.mjs'; 3 | import util from 'util'; 4 | 5 | const test1 = async () => { 6 | console.log('\nStarting TEST1 - Normal socratex!'); 7 | const server = new Socratex({}); 8 | const toTest = ['https://ifconfig.me', 'http://icanhazip.com', 'https://ifconfig.io/ua', 'http://asdahke.e']; 9 | const PORT = 10001; 10 | return new Promise(function(res, rej) { 11 | server.listen(PORT, '0.0.0.0', async function() { 12 | console.log('socratex was started!', server.address()); 13 | for (const singlePath of toTest) { 14 | const cmd = 'curl' + ' -x 127.0.0.1:' + PORT + ' ' + singlePath; 15 | console.log(cmd); 16 | const { stdout, stderr } = await exec(cmd); 17 | console.log('Response =>', stdout); 18 | } 19 | console.log('Closing socratex Server - TEST1\n'); 20 | server.close(); 21 | res(true); 22 | }); 23 | }); 24 | }; 25 | 26 | // @todo: fix this test! 27 | // by @Leask 28 | // disabled some test due to LibreSSL compatibility issues 29 | 30 | // const test2 = async () => { 31 | // console.log('\nStarting TEST2 - Spoof Response!'); 32 | // let ownIp = ''; 33 | // const switchWith = '6.6.6.6'; 34 | // const IP_REGEXP = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/; 35 | // const toTest = ['https://ifconfig.me', 'http://ifconfig.me']; 36 | // const PORT = 10002; //starting server on port 10001 37 | // const cmdOwnIp = 'curl ' + toTest[0]; 38 | // console.log('Getting Own ip with', cmdOwnIp); 39 | // const { stdout, stderr } = await exec(cmdOwnIp); 40 | // ownIp = stdout.match(IP_REGEXP)[0].trim(); 41 | // console.log('Your IP is:', ownIp); 42 | // console.log('Starting Proxy Server with spoof-behaviors'); 43 | // const server = new Socratex({ 44 | // intercept: true, 45 | // injectResponse: (data, session) => { // SPOOFING RETURNED RESPONSE 46 | // if (data.toString().match(ownIp)) { 47 | // const newData = Buffer.from(data.toString() 48 | // .replace(new RegExp('Content-Length: ' + ownIp.length, 'gmi'), 49 | // 'Content-Length: ' + (switchWith.length)) 50 | // .replace(ownIp, switchWith)); 51 | 52 | // return newData; 53 | // } 54 | // return data; 55 | // } 56 | // }); 57 | // return new Promise(function(res, rej) { 58 | // server.listen(PORT, '0.0.0.0', async function() { 59 | // console.log('socratex was started!', server.address()); 60 | // for (const singlePath of toTest) { 61 | // const cmd = 'curl' + ' -x 127.0.0.1:' + PORT + ' -k ' + singlePath; 62 | // console.log(cmd); 63 | // const { stdout, stderr } = await exec(cmd); 64 | // console.log('Response =>', stdout); 65 | // } 66 | // console.log('Closing socratex Server - TEST2\n'); 67 | // server.close(); 68 | // res(true); 69 | // }); 70 | // }); 71 | // }; 72 | 73 | // const test3 = async () => { 74 | // console.log('\nStarting TEST3 - Spoof Request!'); 75 | // const toTest = ['http://ifconfig.io/ua', 'https://ifconfig.me/ua']; 76 | // const PORT = 10003; //starting server on port 10001 77 | // console.log('Starting Proxy Server with spoof-behaviors'); 78 | // const server = new Socratex({ 79 | // intercept: true, 80 | // injectData: (data, session) => { 81 | // return Buffer.from(data.toString().replace('curl/7.55.1', 'Spoofed UA!!')); 82 | // } 83 | // }); 84 | // return new Promise(function(res, rej) { 85 | // server.listen(PORT, '0.0.0.0', async function() { 86 | // console.log('socratex was started!', server.address()); 87 | // for (const singlePath of toTest) { 88 | // const cmd = 'curl' + ' -x 127.0.0.1:' + PORT + ' -k ' + singlePath; 89 | // console.log(cmd); 90 | // const { stdout, stderr } = await exec(cmd); 91 | // console.log('Response =>', stdout); 92 | // } 93 | // console.log('Closing socratex Server - TEST3\n'); 94 | // server.close(); 95 | // res(true); 96 | // }); 97 | // }) 98 | // }; 99 | 100 | // const test4 = async () => { 101 | // console.log('\nStarting TEST4 - Change Some Keys on runtime!'); 102 | // const toTest = ['https://ifconfig.me/', 'https://ifconfig.me/ua']; 103 | // const PORT = 10004; //starting server on port 10001 104 | // const server = new Socratex({ 105 | // intercept: true, 106 | // keys: (session) => { 107 | // const tunnel = session.getTunnelStats(); 108 | // console.log('\t\t=> Could change keys for', tunnel); 109 | // return false; 110 | // } 111 | // }); 112 | // return new Promise(function(res, rej) { 113 | // server.listen(PORT, '0.0.0.0', async function() { 114 | // console.log('socratex was started!', server.address()); 115 | // for (const singlePath of toTest) { 116 | // const cmd = 'curl' + ' -x 127.0.0.1:' + PORT + ' -k ' + singlePath; 117 | // console.log(cmd); 118 | // const { stdout, stderr } = await exec(cmd); 119 | // console.log('Response =>', stdout); 120 | // } 121 | // console.log('Closing socratex Server - TEST4\n'); 122 | // server.close(); 123 | // res(true); 124 | // }); 125 | // }); 126 | // }; 127 | 128 | // const test5 = async () => { 129 | // console.log('\nStarting TEST5 - Proxy With Authentication!'); 130 | // const singlePath = 'https://ifconfig.me/'; 131 | // const pwdToTest = ['bar:foo', 'wronguser:wrongpassword']; 132 | // const PORT = 10005; //starting server on port 10001 133 | // const server = new Socratex({ 134 | // auth: (username, password, session) => { 135 | // return username === 'bar' && password === 'foo'; 136 | // } 137 | // }); 138 | // return new Promise(function(res, rej) { 139 | // server.listen(PORT, '0.0.0.0', async function() { 140 | // console.log('socratex was started!', server.address()); 141 | // for (const pwd of pwdToTest) { 142 | // const cmd = 'curl' + ' -x ' + pwd + '@127.0.0.1:' + PORT + ' ' + singlePath; 143 | // console.log(cmd); 144 | // const { stdout, stderr } = await exec(cmd) 145 | // .catch((err) => { 146 | // if (err.message.indexOf('HTTP code 407')) return { stdout: 'HTTP CODE 407' }; 147 | // throw err; 148 | // }); 149 | // console.log('Response =>', stdout); 150 | // } 151 | 152 | // console.log('Closing socratex Server - TEST5\n'); 153 | // server.close(); 154 | // res(true); 155 | // }); 156 | // }); 157 | // }; 158 | 159 | const exec = util.promisify(child_process.exec); 160 | const main = async () => { 161 | await test1(); 162 | // await test2(); 163 | // await test3(); 164 | // await test4(); 165 | // await test5(); 166 | }; 167 | await main(); 168 | --------------------------------------------------------------------------------