Added self signed certs

This commit is contained in:
Fasterino (Server)
2025-02-27 23:29:25 +03:00
parent 972b671b92
commit cd2db5b04f
7 changed files with 119 additions and 24 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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:

View File

@@ -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",

View File

@@ -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)
)
}

View File

@@ -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)])
)
}

View File

@@ -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)
})