Added self signed certs
This commit is contained in:
14
Dockerfile
14
Dockerfile
@@ -1,10 +1,16 @@
|
|||||||
FROM node:21-alpine
|
FROM node:21-alpine
|
||||||
RUN mkdir /app
|
|
||||||
|
RUN apk add --no-cache openssl
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
|
||||||
RUN mkdir dist
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
VOLUME /app/vol
|
VOLUME /app/vol
|
||||||
|
|
||||||
CMD npm run start
|
CMD npm start
|
||||||
@@ -39,11 +39,12 @@ const config: {
|
|||||||
type Domain = {
|
type Domain = {
|
||||||
/**
|
/**
|
||||||
* Name of folder with domain certificates
|
* 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:
|
* Will use this two files:
|
||||||
* * %CERTS_PATH%/%cert-name%/privkey.pem
|
* * %CERTS_PATH%/%cert-name%/privkey.pem
|
||||||
* * %CERTS_PATH%/%cert-name%/fullchain.pem
|
* * %CERTS_PATH%/%cert-name%/fullchain.pem
|
||||||
*/
|
*/
|
||||||
'cert-name': string
|
'cert-name'?: string
|
||||||
} & {
|
} & {
|
||||||
[path: string]: StaticProxyInfo | DynamicProxyInfo
|
[path: string]: StaticProxyInfo | DynamicProxyInfo
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ services:
|
|||||||
# VOLUME_PATH: '/app/vol' # Not required because its '/app/vol' by default
|
# 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
|
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
|
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:
|
networks:
|
||||||
- your_network # choose network with your containers
|
- your_network # choose network with your containers
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsc && node dist/index.js"
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export class Config implements IConfig {
|
|||||||
return {
|
return {
|
||||||
domains: {
|
domains: {
|
||||||
"test.com": {
|
"test.com": {
|
||||||
[CERT_NAME]: "test.com",
|
|
||||||
"/": {
|
"/": {
|
||||||
domain: "web-docker-container",
|
domain: "web-docker-container",
|
||||||
port: 8080
|
port: 8080
|
||||||
@@ -41,7 +40,6 @@ export class Config implements IConfig {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"mirror.test.com": {
|
"mirror.test.com": {
|
||||||
[CERT_NAME]: "test.com",
|
|
||||||
"/": {
|
"/": {
|
||||||
domain: "test.cc",
|
domain: "test.cc",
|
||||||
https: true,
|
https: true,
|
||||||
@@ -50,7 +48,7 @@ export class Config implements IConfig {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"static.test.com": {
|
"static.test.com": {
|
||||||
[CERT_NAME]: "test.com",
|
[CERT_NAME]: "custom-certificate-folder",
|
||||||
"/": {
|
"/": {
|
||||||
folder: "volFolder",
|
folder: "volFolder",
|
||||||
cors: '*'
|
cors: '*'
|
||||||
@@ -106,6 +104,7 @@ export class Config implements IConfig {
|
|||||||
key,
|
key,
|
||||||
typeof val[CERT_NAME] == 'string' ? val[CERT_NAME] : null
|
typeof val[CERT_NAME] == 'string' ? val[CERT_NAME] : null
|
||||||
])
|
])
|
||||||
|
.filter(([_, value]) => value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
116
src/https.ts
116
src/https.ts
@@ -4,37 +4,123 @@ import tls from "tls"
|
|||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { Config } from "./config"
|
import { Config } from "./config"
|
||||||
import { join } from "path"
|
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) {
|
function resolveCert(cb: (...args: any[]) => void, domain: string) {
|
||||||
const secureContexts = getSecureContexts(cfg, letsencryptPath)
|
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 = {
|
const options: https.ServerOptions = {
|
||||||
// A function that will be called if the client supports SNI TLS extension.
|
// A function that will be called if the client supports SNI TLS extension.
|
||||||
SNICallback: (domain, cb) => {
|
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 (cb) {
|
if (isValidCert(certsPath, certName)) {
|
||||||
ctx ? cb(null, ctx) : cb(Error("No ctx"))
|
console.log("Found!")
|
||||||
} else if (ctx) {
|
secureContexts[domain] = loadCert(certName, certsPath)
|
||||||
return ctx
|
return resolveCert(cb, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
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(
|
return Object.fromEntries(
|
||||||
Object.entries(config.getCerts())
|
Object.entries(config.getCerts())
|
||||||
.filter(([_, value]) => value)
|
|
||||||
.map(([key, value]) =>
|
.map(([key, value]) =>
|
||||||
[key, tls.createSecureContext({
|
[key, loadCert(value, certsPath)])
|
||||||
key: fs.readFileSync(join(letsencryptPath, value, "/privkey.pem")),
|
|
||||||
cert: fs.readFileSync(join(letsencryptPath, value, "/fullchain.pem")),
|
|
||||||
ca: null,
|
|
||||||
})])
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { env } from "./env"
|
|||||||
const HTTP_PORT = env.HTTP_PORT(52080),
|
const HTTP_PORT = env.HTTP_PORT(52080),
|
||||||
HTTPS_PORT = env.HTTPS_PORT(-1),
|
HTTPS_PORT = env.HTTPS_PORT(-1),
|
||||||
VOLUME_PATH = env.VOLUME_PATH('./vol'),
|
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/')
|
CERTS_PATH = env.LOAD_CERTS_FROM_VOLUME(0) ? VOLUME_PATH : env.CERTS_PATH('/etc/letsencrypt/live/')
|
||||||
|
|
||||||
console.log("ENV Loaded:", {
|
console.log("ENV Loaded:", {
|
||||||
@@ -25,7 +26,7 @@ async function main() {
|
|||||||
console.log("Listening http on port: " + HTTP_PORT)
|
console.log("Listening http on port: " + HTTP_PORT)
|
||||||
})
|
})
|
||||||
if (HTTPS_PORT != -1) {
|
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 () {
|
httpsServer(app).on('upgrade', upgrade).listen(HTTPS_PORT, function () {
|
||||||
console.log("Listening https on port: " + HTTPS_PORT)
|
console.log("Listening https on port: " + HTTPS_PORT)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user