Skip to content

Instantly share code, notes, and snippets.

@andrewmackrodt
Last active November 20, 2024 23:36
Show Gist options
  • Save andrewmackrodt/1394c0b4f1bcf1361e9127107a693854 to your computer and use it in GitHub Desktop.
Save andrewmackrodt/1394c0b4f1bcf1361e9127107a693854 to your computer and use it in GitHub Desktop.
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