Added self signed certs
This commit is contained in:
14
Dockerfile
14
Dockerfile
@@ -1,10 +1,16 @@
|
||||
FROM node:21-alpine
|
||||
RUN mkdir /app
|
||||
|
||||
RUN apk add --no-cache openssl
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN mkdir dist
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
VOLUME /app/vol
|
||||
|
||||
CMD npm run start
|
||||
CMD npm start
|
||||
@@ -39,11 +39,12 @@ const config: {
|
||||
type Domain = {
|
||||
/**
|
||||
* Name of folder with domain certificates
|
||||
* If you leave it blank and AUTO_CERTS - 1, certificate will be generated automatically
|
||||
* Will use this two files:
|
||||
* * %CERTS_PATH%/%cert-name%/privkey.pem
|
||||
* * %CERTS_PATH%/%cert-name%/fullchain.pem
|
||||
*/
|
||||
'cert-name': string
|
||||
'cert-name'?: string
|
||||
} & {
|
||||
[path: string]: StaticProxyInfo | DynamicProxyInfo
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ services:
|
||||
# VOLUME_PATH: '/app/vol' # Not required because its '/app/vol' by default
|
||||
CERTS_PATH: /etc/letsencrypt/live/ # Will be ignored because LOAD_CERTS_FROM_VOLUME is 1
|
||||
LOAD_CERTS_FROM_VOLUME: 1 # CERTS_PATH will be inherited from VOLUME_PATH
|
||||
AUTO_CERTS: 1 # Generate a cert for domain if field 'cert-name' is missed
|
||||
networks:
|
||||
- your_network # choose network with your containers
|
||||
volumes:
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "tsc && node dist/index.js"
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
|
||||
@@ -28,7 +28,6 @@ export class Config implements IConfig {
|
||||
return {
|
||||
domains: {
|
||||
"test.com": {
|
||||
[CERT_NAME]: "test.com",
|
||||
"/": {
|
||||
domain: "web-docker-container",
|
||||
port: 8080
|
||||
@@ -41,7 +40,6 @@ export class Config implements IConfig {
|
||||
},
|
||||
},
|
||||
"mirror.test.com": {
|
||||
[CERT_NAME]: "test.com",
|
||||
"/": {
|
||||
domain: "test.cc",
|
||||
https: true,
|
||||
@@ -50,7 +48,7 @@ export class Config implements IConfig {
|
||||
}
|
||||
},
|
||||
"static.test.com": {
|
||||
[CERT_NAME]: "test.com",
|
||||
[CERT_NAME]: "custom-certificate-folder",
|
||||
"/": {
|
||||
folder: "volFolder",
|
||||
cors: '*'
|
||||
@@ -106,6 +104,7 @@ export class Config implements IConfig {
|
||||
key,
|
||||
typeof val[CERT_NAME] == 'string' ? val[CERT_NAME] : null
|
||||
])
|
||||
.filter(([_, value]) => value)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
114
src/https.ts
114
src/https.ts
@@ -4,37 +4,123 @@ import tls from "tls"
|
||||
import fs from "fs"
|
||||
import { Config } from "./config"
|
||||
import { join } from "path"
|
||||
import { execSync } from "child_process"
|
||||
|
||||
export function getHttps(cfg: Config, certsPath: string, autoCerts: boolean) {
|
||||
const secureContexts = getSecureContexts(cfg, certsPath)
|
||||
|
||||
export function getHttps(cfg: Config, letsencryptPath: string) {
|
||||
const secureContexts = getSecureContexts(cfg, letsencryptPath)
|
||||
function resolveCert(cb: (...args: any[]) => void, domain: string) {
|
||||
const ctx = secureContexts[domain]
|
||||
if (cb) {
|
||||
ctx ? cb(null, ctx) : cb(Error(`No ctx for domain ${domain}`))
|
||||
return
|
||||
} else if (ctx) {
|
||||
return ctx
|
||||
}
|
||||
|
||||
throw Error(`No ctx for domain ${domain}`)
|
||||
}
|
||||
|
||||
const options: https.ServerOptions = {
|
||||
// A function that will be called if the client supports SNI TLS extension.
|
||||
SNICallback: (domain, cb) => {
|
||||
if (!(domain in secureContexts) && autoCerts) {
|
||||
const certName = `${domain}-auto-generated`
|
||||
const certDir = getCertPath(certsPath, certName)
|
||||
console.log(`${certName} certificate verification`)
|
||||
|
||||
const ctx = secureContexts[domain]
|
||||
try {
|
||||
if (fs.existsSync(certDir)) {
|
||||
if (isValidCert(certsPath, certName)) {
|
||||
console.log("Found!")
|
||||
secureContexts[domain] = loadCert(certName, certsPath)
|
||||
return resolveCert(cb, domain)
|
||||
}
|
||||
|
||||
if (cb) {
|
||||
ctx ? cb(null, ctx) : cb(Error("No ctx"))
|
||||
} else if (ctx) {
|
||||
return ctx
|
||||
fs.rmSync(certDir, { recursive: true, force: true })
|
||||
}
|
||||
else
|
||||
console.log("Not found!")
|
||||
|
||||
console.log("Generating a new certificate...")
|
||||
|
||||
secureContexts[domain] = generateCert(domain, certName, certsPath)
|
||||
|
||||
console.log("Generated!")
|
||||
} catch (error) {
|
||||
console.error("Certificate handling failed:", error)
|
||||
}
|
||||
}
|
||||
return resolveCert(cb, domain)
|
||||
},
|
||||
}
|
||||
|
||||
return (app: RequestListener) => https.createServer(options, app)
|
||||
}
|
||||
|
||||
function getSecureContexts(config: Config, letsencryptPath: string) {
|
||||
function getCertPath(certsPath: string, certName: string, key: string | null = null) {
|
||||
return key ? join(certsPath, certName, `${key}.pem`) : join(certsPath, certName)
|
||||
}
|
||||
|
||||
function isValidCert(certsPath: string, certName: string) {
|
||||
try {
|
||||
const fullchainPath = getCertPath(certsPath, certName, "fullchain");
|
||||
const privkeyPath = getCertPath(certsPath, certName, "privkey");
|
||||
|
||||
// Check if both certificate and key exist
|
||||
if (!fs.existsSync(fullchainPath)
|
||||
|| !fs.existsSync(privkeyPath))
|
||||
throw new Error("Certificate was deleted")
|
||||
|
||||
// Verify certificate validity using OpenSSL's built-in check
|
||||
const checkSeconds = 30 * 86400; // 30 days in seconds
|
||||
execSync(
|
||||
`openssl x509 -checkend ${checkSeconds} -noout -in "${fullchainPath}"`,
|
||||
{ stdio: 'ignore' }
|
||||
);
|
||||
|
||||
// Additional verification of the key pair
|
||||
execSync(
|
||||
`openssl rsa -check -noout -in "${privkeyPath}"`,
|
||||
{ stdio: 'ignore' }
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.debug("Certificate validation failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function generateCert(domain: string, certName: string, certsPath: string) {
|
||||
const certDir = getCertPath(certsPath, certName)
|
||||
fs.mkdirSync(certDir, { recursive: true })
|
||||
|
||||
const privkeyPath = getCertPath(certsPath, certName, "privkey")
|
||||
const fullchainPath = getCertPath(certsPath, certName, "fullchain")
|
||||
|
||||
// Generate private key
|
||||
execSync(`openssl genrsa -out ${privkeyPath} 2048`)
|
||||
|
||||
// Generate self-signed certificate
|
||||
execSync(`openssl req -new -x509 -key ${privkeyPath} -out ${fullchainPath} -days 395 -subj "/CN=${domain}"`)
|
||||
|
||||
return loadCert(certName, certsPath)
|
||||
}
|
||||
|
||||
function loadCert(certName: string, certsPath: string) {
|
||||
return tls.createSecureContext({
|
||||
key: fs.readFileSync(getCertPath(certsPath, certName, "privkey")),
|
||||
cert: fs.readFileSync(getCertPath(certsPath, certName, "fullchain")),
|
||||
ca: null,
|
||||
})
|
||||
}
|
||||
|
||||
/** Obtaining certs specified by the user in the 'cert-name' field */
|
||||
function getSecureContexts(config: Config, certsPath: string) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(config.getCerts())
|
||||
.filter(([_, value]) => value)
|
||||
.map(([key, value]) =>
|
||||
[key, tls.createSecureContext({
|
||||
key: fs.readFileSync(join(letsencryptPath, value, "/privkey.pem")),
|
||||
cert: fs.readFileSync(join(letsencryptPath, value, "/fullchain.pem")),
|
||||
ca: null,
|
||||
})])
|
||||
[key, loadCert(value, certsPath)])
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { env } from "./env"
|
||||
const HTTP_PORT = env.HTTP_PORT(52080),
|
||||
HTTPS_PORT = env.HTTPS_PORT(-1),
|
||||
VOLUME_PATH = env.VOLUME_PATH('./vol'),
|
||||
AUTO_CERTS = env.AUTO_CERTS(1) != 0,
|
||||
CERTS_PATH = env.LOAD_CERTS_FROM_VOLUME(0) ? VOLUME_PATH : env.CERTS_PATH('/etc/letsencrypt/live/')
|
||||
|
||||
console.log("ENV Loaded:", {
|
||||
@@ -25,7 +26,7 @@ async function main() {
|
||||
console.log("Listening http on port: " + HTTP_PORT)
|
||||
})
|
||||
if (HTTPS_PORT != -1) {
|
||||
const httpsServer = getHttps(config, CERTS_PATH)
|
||||
const httpsServer = getHttps(config, CERTS_PATH, AUTO_CERTS)
|
||||
httpsServer(app).on('upgrade', upgrade).listen(HTTPS_PORT, function () {
|
||||
console.log("Listening https on port: " + HTTPS_PORT)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user