├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── JavaScript ├── 1-request.js ├── 2-connect.js ├── 3-multi.js ├── 4-net.js ├── 5-tunnel.js ├── 6-auth.js ├── 7-size.js ├── 8-tls.js ├── 9-tls-request.js ├── cert │ ├── README.md │ ├── generate.ext │ └── generate.sh └── utils.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": "latest" 10 | }, 11 | "globals": { 12 | "BigInt": true 13 | }, 14 | "rules": { 15 | "indent": ["error", 2], 16 | "linebreak-style": ["error", "unix"], 17 | "quotes": ["error", "single"], 18 | "semi": ["error", "always"], 19 | "no-loop-func": ["error"], 20 | "block-spacing": ["error", "always"], 21 | "camelcase": ["error"], 22 | "eqeqeq": ["error", "always"], 23 | "strict": ["error", "global"], 24 | "brace-style": [ 25 | "error", 26 | "1tbs", 27 | { 28 | "allowSingleLine": true 29 | } 30 | ], 31 | "comma-style": ["error", "last"], 32 | "comma-spacing": [ 33 | "error", 34 | { 35 | "before": false, 36 | "after": true 37 | } 38 | ], 39 | "eol-last": ["error"], 40 | "func-call-spacing": ["error", "never"], 41 | "key-spacing": [ 42 | "error", 43 | { 44 | "beforeColon": false, 45 | "afterColon": true, 46 | "mode": "minimum" 47 | } 48 | ], 49 | "keyword-spacing": [ 50 | "error", 51 | { 52 | "before": true, 53 | "after": true, 54 | "overrides": { 55 | "function": { 56 | "after": false 57 | } 58 | } 59 | } 60 | ], 61 | "max-len": [ 62 | "error", 63 | { 64 | "code": 80, 65 | "ignoreUrls": true 66 | } 67 | ], 68 | "max-nested-callbacks": [ 69 | "error", 70 | { 71 | "max": 7 72 | } 73 | ], 74 | "new-cap": [ 75 | "error", 76 | { 77 | "newIsCap": true, 78 | "capIsNew": false, 79 | "properties": true 80 | } 81 | ], 82 | "new-parens": ["error"], 83 | "no-lonely-if": ["error"], 84 | "no-trailing-spaces": ["error"], 85 | "no-unneeded-ternary": ["error"], 86 | "no-whitespace-before-property": ["error"], 87 | "object-curly-spacing": ["error", "always"], 88 | "operator-assignment": ["error", "always"], 89 | "operator-linebreak": ["error", "after"], 90 | "semi-spacing": [ 91 | "error", 92 | { 93 | "before": false, 94 | "after": true 95 | } 96 | ], 97 | "space-before-blocks": ["error", "always"], 98 | "space-before-function-paren": [ 99 | "error", 100 | { 101 | "anonymous": "never", 102 | "named": "never", 103 | "asyncArrow": "always" 104 | } 105 | ], 106 | "space-in-parens": ["error", "never"], 107 | "space-infix-ops": ["error"], 108 | "space-unary-ops": [ 109 | "error", 110 | { 111 | "words": true, 112 | "nonwords": false, 113 | "overrides": { 114 | "typeof": false 115 | } 116 | } 117 | ], 118 | "no-unreachable": ["error"], 119 | "no-global-assign": ["error"], 120 | "no-self-compare": ["error"], 121 | "no-unmodified-loop-condition": ["error"], 122 | "no-constant-condition": [ 123 | "error", 124 | { 125 | "checkLoops": false 126 | } 127 | ], 128 | "no-console": ["off"], 129 | "no-useless-concat": ["error"], 130 | "no-useless-escape": ["error"], 131 | "no-shadow-restricted-names": ["error"], 132 | "no-use-before-define": [ 133 | "error", 134 | { 135 | "functions": false 136 | } 137 | ], 138 | "arrow-parens": ["error", "always"], 139 | "arrow-body-style": ["error", "as-needed"], 140 | "arrow-spacing": ["error"], 141 | "no-confusing-arrow": [ 142 | "error", 143 | { 144 | "allowParens": true 145 | } 146 | ], 147 | "no-useless-computed-key": ["error"], 148 | "no-useless-rename": ["error"], 149 | "no-var": ["error"], 150 | "object-shorthand": ["error", "always"], 151 | "prefer-arrow-callback": ["error"], 152 | "prefer-const": ["error"], 153 | "prefer-numeric-literals": ["error"], 154 | "prefer-rest-params": ["error"], 155 | "prefer-spread": ["error"], 156 | "rest-spread-spacing": ["error", "never"], 157 | "template-curly-spacing": ["error", "never"], 158 | "consistent-return": ["error", { "treatUndefinedAsUnspecified": true }] 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /JavaScript/1-request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('node:http'); 4 | 5 | const PORT = 8000; 6 | 7 | const receiveBody = async (stream) => { 8 | const chunks = []; 9 | for await (const chunk of stream) chunks.push(chunk); 10 | return Buffer.concat(chunks); 11 | }; 12 | 13 | http.createServer(async (req, res) => { 14 | const { headers, url, method } = req; 15 | const { pathname, hostname } = new URL(url); 16 | const options = { hostname, path: pathname, method, headers }; 17 | const request = http.request(options, (response) => void response.pipe(res)); 18 | if (method === 'POST') { 19 | const body = await receiveBody(req); 20 | request.write(body); 21 | } 22 | request.end(); 23 | }).listen(PORT); 24 | -------------------------------------------------------------------------------- /JavaScript/2-connect.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('node:http'); 4 | const net = require('node:net'); 5 | 6 | const CRLF = '\r\n'; 7 | const PORT = 8000; 8 | const DEFAULT_HTTP_PORT = 80; 9 | 10 | const server = http.createServer((req, res) => { 11 | res.end('HTTP requests is not supported'); 12 | }); 13 | 14 | server.on('connect', (req, socket, head) => { 15 | socket.write('HTTP/1.1 200 Connection Established' + CRLF + CRLF); 16 | const { hostname, port } = new URL(`http://${req.url}`); 17 | const targetPort = parseInt(port, 10) || DEFAULT_HTTP_PORT; 18 | const proxy = net.connect(targetPort, hostname, () => { 19 | if (head) proxy.write(head); 20 | socket.pipe(proxy).pipe(socket); 21 | }); 22 | }); 23 | 24 | server.listen(PORT); 25 | -------------------------------------------------------------------------------- /JavaScript/3-multi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('node:http'); 4 | const net = require('node:net'); 5 | 6 | const CRLF = '\r\n'; 7 | const PORT = 8000; 8 | const DEFAULT_HTTP_PORT = 80; 9 | 10 | const receiveBody = async (stream) => { 11 | const chunks = []; 12 | for await (const chunk of stream) chunks.push(chunk); 13 | return Buffer.concat(chunks); 14 | }; 15 | 16 | const server = http.createServer(async (req, res) => { 17 | const { headers, url, method } = req; 18 | const { pathname, hostname } = new URL(url); 19 | const options = { hostname, path: pathname, method, headers }; 20 | const request = http.request(options, (result) => void result.pipe(res)); 21 | if (method === 'POST') { 22 | const body = await receiveBody(req); 23 | request.write(body); 24 | } 25 | request.end(); 26 | }); 27 | 28 | server.on('connect', (req, socket, head) => { 29 | socket.write('HTTP/1.1 200 Connection Established' + CRLF + CRLF); 30 | const { hostname, port } = new URL(`http://${req.url}`); 31 | const targetPort = parseInt(port, 10) || DEFAULT_HTTP_PORT; 32 | const proxy = net.connect(targetPort, hostname, () => { 33 | if (head) proxy.write(head); 34 | socket.pipe(proxy).pipe(socket); 35 | }); 36 | }); 37 | 38 | server.listen(PORT); 39 | -------------------------------------------------------------------------------- /JavaScript/4-net.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('node:net'); 4 | const { parseHeaders } = require('./utils.js'); 5 | 6 | const CRLF = '\r\n'; 7 | const PORT = 8000; 8 | const DEFAULT_HTTP_PORT = 80; 9 | 10 | const server = net.createServer(); 11 | 12 | server.on('connection', (socket) => { 13 | const { remoteAddress } = socket; 14 | console.log('Client connected:', remoteAddress + '\n'); 15 | 16 | socket.once('data', (data) => { 17 | console.log(`${data}`); 18 | const { method, host } = parseHeaders(data); 19 | const { hostname, port } = new URL(`http://${host}`); 20 | const targetPort = parseInt(port, 10) || DEFAULT_HTTP_PORT; 21 | const proxy = net.createConnection(targetPort, hostname, () => { 22 | const isHttps = method === 'CONNECT'; 23 | if (isHttps) socket.write('HTTP/1.1 200 OK' + CRLF + CRLF); 24 | else proxy.write(data); 25 | socket.pipe(proxy).pipe(socket); 26 | }); 27 | 28 | proxy.on('error', (err) => { 29 | console.error('Proxy connection error:', err.message); 30 | socket.end(); 31 | }); 32 | }); 33 | 34 | socket.on('end', () => { 35 | console.log('Client disconnected:', remoteAddress); 36 | }); 37 | 38 | socket.on('error', (err) => { 39 | console.error('Client connection error:', err.message); 40 | }); 41 | }); 42 | 43 | server.listen(PORT); 44 | -------------------------------------------------------------------------------- /JavaScript/5-tunnel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('node:net'); 4 | 5 | const PORT = 8080; 6 | const PROXY_PORT = 8000; 7 | const PROXY_HOST = 'localhost'; 8 | 9 | const server = net.createServer(); 10 | 11 | server.on('connection', (socket) => { 12 | const { remoteAddress } = socket; 13 | console.log('Client connected:', remoteAddress); 14 | 15 | socket.once('data', (data) => { 16 | const proxy = new net.Socket(); 17 | 18 | proxy.connect(PROXY_PORT, PROXY_HOST, () => { 19 | proxy.write(data); 20 | socket.pipe(proxy).pipe(socket); 21 | }); 22 | 23 | proxy.on('error', (err) => { 24 | console.error('Proxy connection error:', err.message); 25 | socket.end(); 26 | }); 27 | }); 28 | 29 | socket.on('end', () => { 30 | console.log('Client disconnected:', remoteAddress); 31 | }); 32 | 33 | socket.on('error', (err) => { 34 | console.error('Client connection error:', err.message); 35 | }); 36 | }); 37 | 38 | server.listen(PORT); 39 | -------------------------------------------------------------------------------- /JavaScript/6-auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('node:net'); 4 | const { parseHeaders } = require('./utils.js'); 5 | 6 | const CRLF = '\r\n'; 7 | const PORT = 8000; 8 | const DEFAULT_HTTP_PORT = 80; 9 | const HEADERS = [ 10 | 'HTTP/1.1 407 Proxy Authentication Required', 11 | 'Proxy-Authenticate: Basic realm="Proxy Authentication Required"', 12 | 'Content-Length: 0' 13 | ].join(CRLF) + CRLF + CRLF; 14 | const CREDENTIALS = 'marcus:marcus'; 15 | const AUTH_TOKEN = `Basic ${btoa(CREDENTIALS)}`; 16 | 17 | const server = net.createServer(); 18 | 19 | server.on('connection', (socket) => { 20 | const { remoteAddress } = socket; 21 | console.log('Client connected:', remoteAddress); 22 | 23 | socket.once('data', (data) => { 24 | const headers = parseHeaders(data); 25 | const { host, method, proxyAuthorization } = headers; 26 | 27 | if (proxyAuthorization !== AUTH_TOKEN) { 28 | socket.write(HEADERS); 29 | return void socket.end(); 30 | } 31 | 32 | const { hostname, port } = new URL(`http://${host}`); 33 | const targetPort = parseInt(port, 10) || DEFAULT_HTTP_PORT; 34 | const proxy = net.createConnection(targetPort, hostname, () => { 35 | const isHttps = method === 'CONNECT'; 36 | if (isHttps) socket.write('HTTP/1.1 200 OK' + CRLF + CRLF); 37 | else proxy.write(data); 38 | socket.pipe(proxy).pipe(socket); 39 | }); 40 | 41 | proxy.on('error', (err) => { 42 | console.error('Proxy connection error:', err.message); 43 | socket.end(); 44 | }); 45 | }); 46 | 47 | socket.on('end', () => { 48 | console.log('Client disconnected:', remoteAddress); 49 | }); 50 | 51 | socket.on('error', (err) => { 52 | console.error('Client connection error:', err.message); 53 | }); 54 | }); 55 | 56 | server.listen(PORT); 57 | -------------------------------------------------------------------------------- /JavaScript/7-size.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('node:net'); 4 | const stream = require('node:stream'); 5 | const { parseHeaders } = require('./utils.js'); 6 | 7 | const CRLF = '\r\n'; 8 | const PORT = 8000; 9 | const DEFAULT_HTTP_PORT = 80; 10 | 11 | const server = net.createServer(); 12 | 13 | server.on('connection', (socket) => { 14 | const { remoteAddress } = socket; 15 | console.log('Client connected:', remoteAddress + '\n'); 16 | 17 | let size = 0; 18 | socket.once('data', (data) => { 19 | const { method, host } = parseHeaders(data.toString()); 20 | const { hostname, port } = new URL(`http://${host}`); 21 | const targetPort = parseInt(port, 10) || DEFAULT_HTTP_PORT; 22 | const proxy = net.createConnection(targetPort, hostname, () => { 23 | const isHttps = method === 'CONNECT'; 24 | if (isHttps) socket.write('HTTP/1.1 200 OK' + CRLF + CRLF); 25 | else proxy.write(data); 26 | const options = { 27 | transform(chunk, encoding, next) { 28 | size += chunk.length; 29 | this.push(chunk); 30 | next(); 31 | } 32 | }; 33 | const sizeStream = new stream.Transform(options); 34 | socket.pipe(proxy).pipe(sizeStream).pipe(socket); 35 | }); 36 | 37 | proxy.on('error', (err) => { 38 | console.error('Proxy connection error:', err.message); 39 | socket.end(); 40 | }); 41 | }); 42 | 43 | socket.on('end', () => { 44 | console.log('Client disconnected:', remoteAddress, `Used ${size} bytes`); 45 | }); 46 | 47 | socket.on('error', (err) => { 48 | console.error('Client connection error:', err.message); 49 | }); 50 | }); 51 | 52 | server.listen(PORT); 53 | -------------------------------------------------------------------------------- /JavaScript/8-tls.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tls = require('node:tls'); 4 | const fs = require('node:fs'); 5 | const { parseHeaders } = require('./utils.js'); 6 | 7 | const PORT = 8000; 8 | const DEFAULT_TLS_PORT = 443; 9 | 10 | const options = { 11 | key: fs.readFileSync('./cert/key.pem'), 12 | cert: fs.readFileSync('./cert/cert.pem'), 13 | }; 14 | 15 | const server = tls.createServer(options); 16 | 17 | server.on('secureConnection', (socket) => { 18 | const { remoteAddress } = socket; 19 | console.log('Client connected:', remoteAddress + '\n'); 20 | 21 | socket.on('data', (data) => { 22 | console.log(`${data}`); 23 | const { host, port } = parseHeaders(data); 24 | const targetPort = parseInt(port, 10) || DEFAULT_TLS_PORT; 25 | const proxy = new tls.TLSSocket(); 26 | proxy.connect(targetPort, host, () => { 27 | proxy.write(data); 28 | socket.pipe(proxy).pipe(socket); 29 | }); 30 | }); 31 | }); 32 | 33 | server.listen(PORT); 34 | -------------------------------------------------------------------------------- /JavaScript/9-tls-request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const https = require('node:https'); 4 | 5 | const port = 8000; 6 | const host = 'localhost'; 7 | 8 | const options = { 9 | host: 'www.google.com', 10 | rejectUnauthorized: false, //Remove if proxy not use self-signed cert 11 | agent: new https.Agent({ host, port }), 12 | }; 13 | 14 | https.request(options, (res) => { 15 | const chunks = []; 16 | res.on('data', (data) => void chunks.push(data)); 17 | res.on('end', () => void console.log(chunks.join())); 18 | }).end(); 19 | -------------------------------------------------------------------------------- /JavaScript/cert/README.md: -------------------------------------------------------------------------------- 1 | # Start HTTP server 2 | 3 | ## With self-signed certificate (for testing) 4 | 5 | - Generate certificates, run: `./cert/generate.sh` 6 | - Start server: `node 8-tls.js` 7 | - Send request with proxy: `node 9-tls-request.js` 8 | 9 | ## With certbot (for production) 10 | 11 | - Let's Encrypt is a free certificate authority: https://letsencrypt.org/ 12 | - Use Certbot (free tool for automatically generatinging Let’s Encrypt 13 | certificates to enable HTTPS): https://certbot.eff.org/ 14 | - Install: `dnf -y install certbot` 15 | - Generate certificate: 16 | `certbot certonly --standalone -d www.domain.com -d domain.com -m your.name@domain.com --agree-tos --no-eff-email` 17 | - Copy certificate: 18 | `cp /etc/letsencrypt/live/domain.com/fullchain.pem ./cert/cert.pem` 19 | - Copy private key: 20 | `cp /etc/letsencrypt/live/domain.com/privkey.pem ./cert/key.pem` 21 | - Or use your web server for challenge exchange: 22 | `certbot certonly --webroot -w ./ -d domain.com -m your.name@domain.com --agree-tos --no-eff-email` 23 | -------------------------------------------------------------------------------- /JavaScript/cert/generate.ext: -------------------------------------------------------------------------------- 1 | authorityKeyIdentifier=keyid,issuer 2 | basicConstraints=CA:FALSE 3 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 4 | subjectAltName = @alt_names 5 | 6 | [alt_names] 7 | DNS.1 = example.com 8 | IP.1 = 127.0.0.1 9 | IP.2 = ::1 10 | -------------------------------------------------------------------------------- /JavaScript/cert/generate.sh: -------------------------------------------------------------------------------- 1 | cd "$(dirname "$0")" 2 | openssl genrsa -out key.pem 3072 3 | openssl req -new -out self.pem -key key.pem -subj '/CN=localhost' 4 | openssl req -text -noout -in self.pem 5 | openssl x509 -req -days 1024 -in self.pem -signkey key.pem -out cert.pem -extfile generate.ext 6 | -------------------------------------------------------------------------------- /JavaScript/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CRLF = '\r\n'; 4 | 5 | const toUpperCamel = (s) => s.charAt(0).toUpperCase() + s.slice(1); 6 | 7 | const toLower = (s) => s.toLowerCase(); 8 | 9 | const spinalToCamel = (s) => { 10 | const words = s.split('-'); 11 | const first = words.length > 0 ? words.shift().toLowerCase() : ''; 12 | return first + words.map(toLower).map(toUpperCamel).join(''); 13 | }; 14 | 15 | const parseHeaders = (buffer) => { 16 | const [headers] = buffer.toString().split(CRLF + CRLF); 17 | const lines = headers.split('\n'); 18 | const [method] = lines.shift().split(' '); 19 | const result = lines.reduce((headers, line) => { 20 | const [key, value = ''] = line.split(': '); 21 | const header = (key.includes('-') ? spinalToCamel : toLower)(key); 22 | headers[header] = value.trim(); 23 | return headers; 24 | }, { method }); 25 | return result; 26 | }; 27 | 28 | module.exports = { parseHeaders }; 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![HTTP Proxy)](https://img.youtube.com/vi/4yF6hlxiIic/0.jpg)](https://www.youtube.com/watch?v=4yF6hlxiIic) 2 | --------------------------------------------------------------------------------