Last active
November 20, 2024 23:36
-
-
Save andrewmackrodt/1394c0b4f1bcf1361e9127107a693854 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import http from 'node:http' | |
import https from 'node:https' | |
import net from 'node:net' | |
import type { Duplex } from 'node:stream' | |
import type { SecureContext } from 'node:tls' | |
import tls from 'node:tls' | |
import { parse as parseURL } from 'node:url' | |
import pem from 'pem' | |
import psl from 'psl' | |
type Scheme = 'http' | 'https' | |
interface Certificate { | |
cert: string | |
key: string | |
} | |
const contexts: Record<string, SecureContext> = {} | |
async function createRootCertificate(): Promise<Certificate> { | |
return new Promise((resolve, reject) => { | |
pem.createCertificate({ days: 3650, selfSigned: true }, (err, keys) => { | |
if (err) { | |
return reject(err) | |
} | |
resolve({ | |
cert: keys.certificate, | |
key: keys.clientKey, | |
}) | |
}) | |
}) | |
} | |
async function createCertificate(rootCert: Certificate, commonName: string): Promise<Certificate> { | |
return new Promise((resolve, reject) => { | |
pem.createCertificate({ | |
altNames: [commonName, `*.${commonName}`], | |
commonName: commonName, | |
days: 30, | |
organization: '', | |
organizationUnit: '', | |
serial: Date.now(), | |
serviceCertificate: rootCert.cert, | |
serviceKey: rootCert.key, | |
}, (err, keys) => { | |
if (err) { | |
return reject(err) | |
} | |
resolve({ | |
cert: keys.certificate, | |
key: keys.clientKey, | |
}) | |
}) | |
}) | |
} | |
async function getOrCreateSecureContext(rootCert: Certificate, commonName: string): Promise<SecureContext> { | |
const topLevelCommonName = getTopLevelCommonName(commonName) | |
if (topLevelCommonName in contexts) { | |
return contexts[topLevelCommonName] | |
} | |
const cert = await createCertificate(rootCert, topLevelCommonName) | |
const { context } = tls.createSecureContext({ | |
ca: rootCert.cert, | |
cert: cert.cert, | |
key: cert.key, | |
}) | |
return contexts[topLevelCommonName] = context | |
} | |
function proxy(scheme: Scheme, clientReq: http.IncomingMessage, clientRes: http.ServerResponse) { | |
const url = clientReq.url!.match(/^https?:/i) | |
? parseURL(clientReq.url!) | |
: parseURL(`${scheme}://${clientReq.headers.host}${clientReq.url}`) | |
const options: http.RequestOptions = { | |
hostname: url.hostname, | |
port: url.port, | |
path: url.path, | |
method: clientReq.method, | |
headers: clientReq.headers, | |
} | |
const cb = (remoteRes: http.IncomingMessage) => { | |
if (remoteRes.statusCode) { | |
clientRes.writeHead(remoteRes.statusCode, remoteRes.headers) | |
remoteRes.pipe(clientRes) | |
} else { | |
clientRes.destroy(new Error('the server did not supply a status code')) | |
} | |
} | |
let remoteReq: http.ClientRequest | |
if (scheme === 'https') { | |
remoteReq = https.request(options, cb) | |
} else { | |
remoteReq = http.request(options, cb) | |
} | |
remoteReq.on('error', err => { | |
console.error('failed to proxy request:', err) | |
clientRes.destroy(err) | |
}) | |
clientReq.pipe(remoteReq) | |
} | |
function getTopLevelCommonName(commonName: string) { | |
const parsed = psl.parse(commonName) | |
if ( ! ('domain' in parsed) || parsed.domain === null) { | |
return commonName | |
} else if (parsed.subdomain === null) { | |
return parsed.domain | |
} else { | |
return [...parsed.subdomain.split('.').slice(1), parsed.domain].join('.') | |
} | |
} | |
const rootCert = await createRootCertificate() | |
const tlsServer = https.createServer( | |
{ | |
ALPNProtocols: ['http/1.1'], | |
SNICallback: async (commonName: string, cb: (err: Error | null, ctx?: SecureContext) => void) => { | |
const ctx = await getOrCreateSecureContext(rootCert, commonName) | |
cb(null, ctx) | |
}, | |
cert: rootCert.cert, | |
key: rootCert.key, | |
}, | |
(req, res) => proxy('https', req, res)) | |
tlsServer.on('error', err => console.error('tlsServer', err)) | |
const tlsPort = await new Promise<number>(resolve => { | |
tlsServer.listen(0, () => { | |
const port = (tlsServer.address() as net.AddressInfo).port | |
resolve(port) | |
}) | |
}) | |
async function sendConnectionEstablished(clientSocket: Duplex) { | |
return new Promise<void>((resolve, reject) => { | |
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n', err => { | |
if (err) { | |
reject(err) | |
} else { | |
resolve() | |
} | |
}) | |
}) | |
} | |
const server = http.createServer((req, res) => proxy('http', req, res)) | |
server.on('connect', async (_req, clientSocket, head) => { | |
clientSocket.on('error', err => { | |
console.error('client socket error:', err) | |
clientSocket.destroy() | |
}) | |
try { | |
// inform client proxy connection is established | |
await sendConnectionEstablished(clientSocket) | |
// pipe client <=> server | |
const serverSocket = net.connect(tlsPort, '127.0.0.1', () => { | |
serverSocket.pipe(clientSocket) | |
clientSocket.pipe(serverSocket) | |
clientSocket.write(head) | |
}) | |
serverSocket.on('error', err => { | |
console.error(`server socket error: ${err.toString()}`, err) | |
}) | |
} catch (err) { | |
console.error('unknown error', err) | |
} | |
}) | |
server.listen(8000) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment