@vladmandic/piacme
Advanced tools
Comparing version 1.0.2 to 1.0.3
{ | ||
"name": "@vladmandic/piacme", | ||
"version": "1.0.2", | ||
"version": "1.0.3", | ||
"description": "Simple ACME/LetsEncrypt HTTP/SSL Certificate Management", | ||
@@ -47,4 +47,4 @@ "main": "dist/piacme.js", | ||
"devDependencies": { | ||
"esbuild": "^0.15.2", | ||
"eslint": "^8.22.0", | ||
"esbuild": "^0.15.10", | ||
"eslint": "^8.25.0", | ||
"eslint-config-airbnb-base": "^15.0.0", | ||
@@ -51,0 +51,0 @@ "eslint-plugin-import": "^2.26.0" |
@@ -38,3 +38,3 @@ const fs = require('fs'); | ||
function notify(evt, msg) { | ||
if (config.debug) log.data('acme notification', { evt, msg }); | ||
if (config.debug) log.data('ACME Notification:', evt, msg); | ||
let key; | ||
@@ -60,6 +60,6 @@ if (msg.challenge && msg.challenge.keyAuthorization) key = msg.challenge.keyAuthorization; | ||
if (!config.domains || (config.domains.length <= 0)) { | ||
log.info('acme skip create certificate', { domains: config.domains }); | ||
log.info('ACME Skip create certificate', { domains: config.domains }); | ||
return false; | ||
} | ||
log.info('acme create certificate', { domains: config.domains, encoding: 'der' }); | ||
log.info('ACME Create certificate', { domains: config.domains, encoding: 'der' }); | ||
// what are we requesting | ||
@@ -83,5 +83,5 @@ const csrDer = await CSR.csr({ jwk: config.key, domains: config.domains, encoding: 'der' }); | ||
res.end(); | ||
log.info('acme challenge', { key: key.host, url: req.url, sent: key.key }); | ||
log.info('ACME Challenge', { key: key.host, url: req.url, sent: key.key }); | ||
} else { | ||
log.info('acme challenge', { key: key.host, url: req.url }); | ||
log.info('ACME Challenge', { key: key.host, url: req.url }); | ||
} | ||
@@ -92,8 +92,10 @@ }); | ||
// sudo setcap 'cap_net_bind_service=+ep' `which node` | ||
server.listen(80, () => log.state('acme validation', { server: 'ready', webroot: './.well-known/acme-challenge' })); | ||
server.on('error', (err) => log.error('acme validation', { err: err.message || err })); | ||
server.on('request', (req, res) => { // stop http server once request has finished | ||
req.socket['_isIdle'] = false; | ||
server.listen(80, () => log.state('ACME Validation server ready')); | ||
// stop http server | ||
server.on('request', (req, res) => { | ||
// eslint-disable-next-line no-underscore-dangle | ||
req.socket._isIdle = false; | ||
res.on('finish', () => { | ||
log.state('acme validation', { server: 'finish' }); | ||
log.state('ACME Validation', { server: 'finish' }); | ||
req.socket['_isIdle'] = true; | ||
@@ -103,6 +105,7 @@ req.socket.destroy(); | ||
}); | ||
// server.on('close', () => log.info('acme validation server closed')); | ||
// server.on('close', () => log.info('ACME Validation server closed')); | ||
// start actual verification | ||
log.info('acme validating', { domains: config.domains }); | ||
log.info('ACME Validating domains:', { domains: config.domains }); | ||
log.info(`ACME Account contract: ${config.account.contact} crv: ${config.accountKey.crv}`); | ||
@@ -113,17 +116,17 @@ let pems; | ||
} catch (err) { | ||
log.warn('acme validation exception', err.code ? { code: err.code, syscall: err.syscall, address: err.address, port: err.port } : err); | ||
log.warn('ACME Validation exception', err.code ? { code: err.code, syscall: err.syscall, address: err.address, port: err.port } : err); | ||
} | ||
server.close(() => log.info('acme validation', { server: 'close' })); | ||
server.close(() => log.info('ACME Validation', { server: 'close' })); | ||
// generate actual fullchain from received pems | ||
if (!pems || !pems.cert || !pems.chain) { | ||
log.warn('acme validation failed'); | ||
log.warn('ACME Validation failed'); | ||
return false; | ||
} | ||
log.info('acme certificate:', { create: config.fullChain }); | ||
log.info('ACME Certificate:', { create: config.fullChain }); | ||
cert = `${pems.cert}\n${pems.chain}\n`; | ||
await fs.promises.writeFile(config.fullChain, cert, 'ascii'); | ||
} else if (initial) { | ||
log.info('acme certificate', { load: config.fullChain }); | ||
log.info('ACME Certificate', { load: config.fullChain }); | ||
cert = await fs.promises.readFile(config.fullChain, 'ascii'); | ||
@@ -137,3 +140,3 @@ } | ||
if (!config.domains || (config.domains.length <= 0)) { | ||
log.info('acme skip create keys', { domains: config.domains }); | ||
log.info('ACME Skip create keys', { domains: config.domains }); | ||
return; | ||
@@ -146,7 +149,7 @@ } | ||
await acme.init(directoryUrl); | ||
log.info('acme request', { domains: config.domains }); | ||
log.info('ACME Request', { domains: config.domains }); | ||
// Generate or load account key | ||
if (!fs.existsSync(config.accountKeyFile)) { | ||
log.info('acme account key', { generate: config.accountKeyFile }); | ||
log.info('ACME Account key', { generate: config.accountKeyFile }); | ||
const accountKeypair = await Keypairs.generate({ kty: 'EC', format: 'jwk' }); | ||
@@ -157,3 +160,3 @@ config.accountKey = accountKeypair.private; | ||
} else { | ||
log.info('acme account key', { load: config.accountKeyFile }); | ||
log.info('ACME Account key', { load: config.accountKeyFile }); | ||
const pem = await fs.promises.readFile(config.accountKeyFile, 'ascii'); | ||
@@ -165,7 +168,7 @@ config.accountKey = await Keypairs.import({ pem }); | ||
if (!fs.existsSync(config.accountFile)) { | ||
log.info('acme account', { create: config.accountFile }); | ||
log.info('ACME Account', { create: config.accountFile }); | ||
config.account = await acme.accounts.create({ subscriberEmail: config.subscriber, agreeToTerms: true, accountKey: config.accountKey }); | ||
await fs.promises.writeFile(config.accountFile, JSON.stringify(config.account), 'ascii'); | ||
} else { | ||
log.info('acme account', { load: config.accountFile }); | ||
log.info('ACME Account', { load: config.accountFile }); | ||
const json = await fs.promises.readFile(config.accountFile, 'ascii'); | ||
@@ -177,3 +180,3 @@ config.account = JSON.parse(json); | ||
if (!fs.existsSync(config.ServerKeyFile)) { | ||
log.info('acme server key', { generate: config.ServerKeyFile }); | ||
log.info('ACME Server key: generate', config.ServerKeyFile); | ||
const serverKeypair = await Keypairs.generate({ kty: 'RSA', format: 'jwk' }); | ||
@@ -184,3 +187,3 @@ config.key = serverKeypair.private; | ||
} else { | ||
log.info('acme server key', { load: config.ServerKeyFile }); | ||
log.info('ACME Server key: load', config.ServerKeyFile); | ||
const pem = await fs.promises.readFile(config.ServerKeyFile, 'ascii'); | ||
@@ -234,15 +237,15 @@ config.key = await Keypairs.import({ pem }); | ||
if (fs.existsSync(config.fullChain)) { | ||
if (initial) log.info('ssl certificate', { check: config.fullChain }); | ||
if (initial) log.info('ACME Certificate check:', config.fullChain); | ||
const ssl = await parseCert(); | ||
const now = new Date(); | ||
if (!ssl.account || ssl.account.error) { | ||
log.warn('ssl certificate error', { account: ssl.account.error }); | ||
log.warn(`ACME Certificate account error: ${ssl.account.error || 'unknown'}`); | ||
return false; | ||
} | ||
if (!ssl.serverKey || ssl.serverKey.error || !ssl.accountKey || ssl.accountKey.error) { | ||
log.warn('ssl certificate error', { key: ssl.serverKey.error, account: ssl.account.error }); | ||
log.warn(`SSL Keys error server:${ssl.serverKey.error} account:${ssl.account.error}`); | ||
return false; | ||
} | ||
if (!ssl.fullChain || ssl.fullChain.error) { | ||
log.warn('ssl certificate error', { fullchain: ssl.fullChain.error }); | ||
log.warn(`SSL Certificate error: ${ssl.fullChain.error}`); | ||
return false; | ||
@@ -252,3 +255,3 @@ } | ||
if (!ssl.fullChain.notBefore || (now - ssl.fullChain.notBefore < 0)) { | ||
log.warn('ssl certificate invalid', { notbefore: ssl.fullChain.notBefore }); | ||
log.warn(`ACME Certificate invalid notBefore: ${ssl.fullChain.notBefore}`); | ||
return false; | ||
@@ -258,3 +261,3 @@ } | ||
if (!ssl.fullChain.notAfter || (now - ssl.fullChain.notAfter > 0)) { | ||
log.warn('ssl certificate invalid', { notafter: ssl.fullChain.notAfter }); | ||
log.warn(`ACME Certificate invalid notAfter: ${ssl.fullChain.notAfter}`); | ||
return false; | ||
@@ -264,7 +267,7 @@ } | ||
config.remainingDays = (ssl.fullChain.notAfter - now) / 1000 / 60 / 60 / 24; | ||
log.state('ssl certificate validity', { days: Math.round(config.remainingDays), renew: config.remainingDays < config.renewDays ? 'now' : 'skip' }); | ||
log.state('SSL Certificate expires in', config.remainingDays.toFixed(1), `days: ${config.remainingDays < config.renewDays ? 'renewing now' : 'skipping renewal'}`); | ||
if (config.remainingDays < config.renewDays) return false; | ||
return true; | ||
} | ||
log.warn('ssl certificate missing', { cert: config.fullChain }); | ||
log.warn(`SSL Certificate does not exist: ${config.fullChain}`); | ||
return false; | ||
@@ -282,3 +285,3 @@ } | ||
if (!certOk) { | ||
log.error('ssl certificate validation failed'); | ||
log.error('SSL Certificate did not pass validation'); | ||
} | ||
@@ -290,19 +293,2 @@ if (callback) { | ||
} | ||
/* | ||
if (initial) { | ||
const ssl = await parseCert(); | ||
if (ssl.account && !ssl.account.error) { | ||
log.info(`SSL account: ${ssl.account.contact} Created: ${moment(ssl.account.createdAt).format('YYYY-MM-DD HH:mm:ss')} `); | ||
} else log.warn(`SSL account error: ${ssl.account.error}`); | ||
if (ssl.serverKey && !ssl.serverKey.error && ssl.accountKey && !ssl.accountKey.error) { | ||
log.info(`SSL keys server: ${ssl.serverKey.type} Account: ${ssl.accountKey.type} `); | ||
} else log.warn(`SSL keys error server: ${ssl.serverKey.error} Account: ${ssl.account.error}`); | ||
if (ssl.fullChain && !ssl.fullChain.error) { | ||
log.info(`SSL certificate subject: ${ssl.fullChain.subject} Issuer: ${ssl.fullChain.issuer}`); | ||
} else log.warn(`SSL certificate error: ${ssl.fullChain.error}`); | ||
config.SSL = { Key: `../${config.ServerKeyFile}`, Crt: `../${config.fullChain}` }; | ||
initial = false; | ||
} | ||
return config.SSL; | ||
*/ | ||
} | ||
@@ -314,11 +300,69 @@ | ||
await getCert(); | ||
log.state('ssl monitor', { cert: config.fullChain, check: 'complete' }); | ||
log.state('SSL Monitor certificate check complete'); | ||
}, 1000 * 60 * config.monitorInterval); | ||
} | ||
function initConfig(userConfig) { | ||
function setConfig(userConfig) { | ||
config = { ...config, ...userConfig }; | ||
} | ||
async function test() { | ||
async function scanHost(host, startPort, endPort) { | ||
try { | ||
log.info('Connection test', { fetch: 'create' }); | ||
const res = await fetch('https://www.ipfingerprints.com/scripts/getPortsInfo.php', { | ||
headers: { | ||
accept: 'application/json, text/javascript, */*; q=0.01', | ||
'cache-control': 'no-cache', | ||
'content-type': 'application/x-www-form-urlencoded', | ||
pragma: 'no-cache', | ||
Referer: 'https://www.ipfingerprints.com/portscan.php', | ||
}, | ||
body: `remoteHost=${host}&start_port=${startPort}&end_port=${endPort}&normalScan=Yes&scan_type=connect&ping_type=none`, | ||
method: 'POST', | ||
}); | ||
if (!res?.ok) return []; | ||
const html = await res.text(); | ||
const text = html.replace(/<[^>]*>/g, ''); | ||
const lines = text.split('\\n').map((line) => line.replace('\'', '').trim()).filter((line) => line.includes('tcp ')); | ||
const parsed = lines.map((line) => line.replace('\\/tcp', '').split(' ').filter((str) => str.length > 0)); | ||
const services = parsed.map((line) => ({ host, port: Number(line[0]), state: line[1], service: line[2] })); | ||
return services; | ||
} catch { | ||
return []; | ||
} | ||
} | ||
async function testConnection(host, timeout = 5000) { | ||
return new Promise((resolve) => { | ||
let timeoutCallback; | ||
const server = http.createServer(); | ||
server.on('listening', () => { | ||
// log.state('Connection test', { server: 'listen' }); | ||
scanHost(host, 80, 80).then((services) => { | ||
log.state('Connection test', { services }); | ||
if (timeoutCallback) clearTimeout(timeoutCallback); | ||
resolve(services.length > 0); | ||
server.close(); | ||
}); | ||
}); | ||
server.on('error', (err) => log.warn('Connection test', { code: err.code, call: err.syscall, port: err.port })); | ||
// server.on('close', () => log.info('Connection test', { server: 'close' })); | ||
server.on('request', (req, res) => { | ||
// log.state('Connection test', { url: req.url }); | ||
res.writeHead(200); | ||
res.write('{ "connection": true }'); | ||
res.end(); | ||
}); | ||
timeoutCallback = setTimeout(() => { | ||
if (server) server.close(); | ||
log.warn('Connection test', { host, timeout }); | ||
resolve(false); | ||
}, timeout); | ||
server.listen(80); | ||
}); | ||
} | ||
async function testModule() { | ||
const status = await testConnection(config.domains[0]); | ||
log.data('Test connection', status); | ||
await getCert(); | ||
@@ -330,8 +374,8 @@ const details = await parseCert(); // parse any cert | ||
try { | ||
if (require.main === module) test(); | ||
} catch { | ||
// | ||
if (require.main === module) testModule(); | ||
} catch (err) { | ||
log.error('test:', err); | ||
} | ||
exports.init = initConfig; | ||
exports.setConfig = setConfig; | ||
exports.getCert = getCert; | ||
@@ -343,1 +387,2 @@ exports.parseCert = parseCert; | ||
exports.monitorCert = monitorCert; | ||
exports.testConnection = testConnection; |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
831671
24936
7