├── .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 | [](./LICENSE)
4 | [](https://github.com/Leask/socratex/actions/workflows/build.yml)
5 | [](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 |
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 |
85 |
86 |
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 | [](https://en.wikipedia.org/wiki/Socrates)
123 |
124 | *Image credit: The Death of Socrates, by Jacques-Louis David (1787)*
125 |
126 |
127 | 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 |
', 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 | --------------------------------------------------------------------------------