Last active
January 25, 2024 07:58
-
-
Save zicklag/5618a0cb0428f8ef9d8354abdc4651eb to your computer and use it in GitHub Desktop.
Deno TLS Proxy Using Traefik `acme.json` File For Certificates
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 { copy } from "https://deno.land/[email protected]/streams/conversion.ts"; | |
const DATA_FILE = Deno.args[0]; | |
const LISTEN_PORT = Deno.args[1]; | |
const TARGET_ADDR = Deno.args[2]; | |
const DOMAIN = Deno.args[3]; | |
interface AcmeJsonFile { | |
letsencrypt: { | |
Certificates: { | |
domain: { | |
main: string; | |
}; | |
certificate: string; | |
key: string; | |
}[]; | |
}; | |
} | |
interface ServerData { | |
server?: Deno.TlsListener; | |
cert?: string; | |
key?: string; | |
reload: boolean; | |
} | |
async function loadCert(): Promise<{ cert: string; key: string }> { | |
const acmeData: AcmeJsonFile = JSON.parse(await Deno.readTextFile(DATA_FILE)); | |
const domainInfo = acmeData.letsencrypt.Certificates.filter( | |
(x) => x.domain.main == DOMAIN | |
)[0]; | |
if (!domainInfo) { | |
console.error(`Could not find domain in acme.json file: ${DOMAIN}`); | |
Deno.exit(1); | |
} | |
const cert = atob(domainInfo.certificate); | |
const key = atob(domainInfo.key); | |
return { cert, key }; | |
} | |
const serverData: ServerData = { | |
reload: true, | |
}; | |
async function serve(data: ServerData) { | |
console.info("Starting server"); | |
data.server = Deno.listenTls({ | |
port: parseInt(LISTEN_PORT), | |
cert: data.cert, | |
key: data.key, | |
}); | |
while (true) { | |
if (!data.server) { | |
break; | |
} | |
try { | |
let input_conn: Deno.TlsConn; | |
try { | |
input_conn = await data.server.accept(); | |
} catch (e) { | |
console.info("Could not accept:", e); | |
break; | |
} | |
console.log(`accepted connection`); | |
const [hostname, port] = TARGET_ADDR.split(":"); | |
console.log(`Connecting to server ${TARGET_ADDR}`); | |
const output_conn = await Deno.connect({ | |
hostname, | |
port: parseInt(port), | |
}); | |
console.log(`Connected to target`); | |
// Proxy input and output | |
(async () => { | |
try { | |
await copy(input_conn, output_conn); | |
console.log("Connection closed"); | |
output_conn.close(); | |
} catch { | |
console.log("disconnect input -> output"); | |
} | |
})(); | |
(async () => { | |
try { | |
await copy(output_conn, input_conn); | |
} catch { | |
// Disconnect is normal when we close it after the client disconnects. | |
} | |
})(); | |
} catch (err) { | |
console.error("connection error", err); | |
} | |
} | |
} | |
// Watch file for changes | |
const watcher = Deno.watchFs(DATA_FILE); | |
(async (data: ServerData) => { | |
for await (const event of watcher) { | |
if (event.kind == "modify" && data.server) { | |
data.reload = true; | |
} | |
} | |
})(serverData); | |
let firstRun = new Boolean(true); | |
const reload = async () => { | |
if (serverData.reload) { | |
if (firstRun) { | |
firstRun = false; | |
} else { | |
console.log("Certs updated: restarting server"); | |
} | |
if (serverData.server) { | |
console.log("Closing server"); | |
serverData.server.close(); | |
serverData.server = undefined; | |
} | |
try { | |
const certData = await loadCert(); | |
serverData.cert = certData.cert; | |
serverData.key = certData.key; | |
serve(serverData); | |
serverData.reload = false; | |
} catch { | |
setTimeout(reload, 200); | |
} | |
} | |
setTimeout(reload, 1000); | |
}; | |
reload(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment