@cocreate/acme
Advanced tools
Comparing version 1.0.0 to 1.1.0
@@ -0,1 +1,15 @@ | ||
# [1.1.0](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.0.0...v1.1.0) (2023-12-31) | ||
### Bug Fixes | ||
* filenames ([23735c4](https://github.com/CoCreate-app/CoCreate-acme/commit/23735c44f30cdd848959a3046ac40665302f38b6)) | ||
* Wraped in class to use with modules ([4ff5bea](https://github.com/CoCreate-app/CoCreate-acme/commit/4ff5beabf9aee2b53d41839d3c6f276344726cb3)) | ||
### Features | ||
* emit certificateCreated ([eb5cdc6](https://github.com/CoCreate-app/CoCreate-acme/commit/eb5cdc6142c7bfd15f548bbdaf908c33e3f6c19f)) | ||
* store certificates using crud so other nodes can reused ([aea5f15](https://github.com/CoCreate-app/CoCreate-acme/commit/aea5f1557f358934f6b08c9f94a734af80a56f6e)) | ||
# 1.0.0 (2023-12-30) | ||
@@ -2,0 +16,0 @@ |
{ | ||
"name": "@cocreate/acme", | ||
"version": "1.0.0", | ||
"version": "1.1.0", | ||
"description": "An intergration with ACME and CoCreateJS.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
341
src/index.js
@@ -1,131 +0,274 @@ | ||
const { Client } = require('acme-client'); | ||
const { Client, forge } = require('acme-client'); | ||
const fs = require('fs'); | ||
const email = 'mailto:ssl@cocreate.app'; | ||
const certificates = new Map() | ||
const email = 'ssl@cocreate.app'; | ||
const keyPath = 'certificates/'; | ||
let client | ||
const hosts = {} | ||
async function init() { | ||
if (!fs.existsSync(keyPath)) { | ||
fs.mkdirSync(keyPath, { recursive: true }); // Create the directory if it doesn't exist | ||
// Run this once per server to generate random constants | ||
const DAYS = Math.floor(Math.random() * 7); // Random days between 0-6 | ||
const HOURS = Math.floor(Math.random() * 24); // Random hours between 0-23 | ||
const MINUTES = Math.floor(Math.random() * 60); // Random minutes between 0-59 | ||
// Store these constants in a configuration file, environment variable, or database | ||
class CoCreateAcme { | ||
constructor(crud) { | ||
this.crud = crud | ||
this.init().catch(err => { | ||
console.error('Error initializing ACME client:', err); | ||
// TODO: Handle initialization error (possibly retry or exit) | ||
}); | ||
} | ||
const accountKeyPath = keyPath + 'account.pem'; | ||
async init() { | ||
if (!fs.existsSync(keyPath)) { | ||
fs.mkdirSync(keyPath, { recursive: true }); // Create the directory if it doesn't exist | ||
} | ||
let accountKey; | ||
let isNewAccount = false; // Flag to check if the account is new | ||
const accountKeyPath = keyPath + 'account.pem'; | ||
// Check if the account key exists and load it; otherwise, create a new one | ||
if (!fs.existsSync(accountKeyPath)) { | ||
accountKey = await Client.forge.createPrivateKey(); | ||
fs.writeFileSync(accountKeyPath, accountKey); // Store the account key | ||
fs.chmodSync(accountKeyPath, '400') | ||
let accountKey = ''; | ||
let isNewAccount = false; // Flag to check if the account is new | ||
isNewAccount = true; // New account, so will need to register it | ||
} else { | ||
// Load the existing account key | ||
accountKey = fs.readFileSync(accountKeyPath, 'utf8'); | ||
} | ||
// Check if the account key exists and load it; otherwise, create a new one | ||
if (!fs.existsSync(accountKeyPath)) { | ||
fs.writeFileSync(accountKeyPath, accountKey); // Store the account key | ||
accountKey = await forge.createPrivateKey(); | ||
isNewAccount = true; // New account, so will need to register it | ||
fs.writeFileSync(accountKeyPath, accountKey); // Store the account key | ||
// fs.chmodSync(accountKeyPath, '400') | ||
} else { | ||
accountKey = fs.readFileSync(accountKeyPath, 'utf8'); | ||
} | ||
// Initialize the ACME client with the account key | ||
client = new Client({ | ||
directoryUrl: Client.directory.letsencrypt.staging, | ||
accountKey: accountKey | ||
}); | ||
// Initialize the ACME client with the account key | ||
client = new Client({ | ||
directoryUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory', | ||
accountKey: accountKey | ||
}); | ||
// Register the new account if it was just created | ||
if (isNewAccount) { | ||
await client.createAccount({ | ||
termsOfServiceAgreed: true, | ||
contact: [email] | ||
}); | ||
// Register the new account if it was just created | ||
if (isNewAccount) { | ||
try { | ||
// Attempt to create an account | ||
await client.createAccount({ | ||
termsOfServiceAgreed: true, | ||
contact: ['mailto:' + email] | ||
}); | ||
console.log("ACME account created successfully!"); | ||
} catch (error) { | ||
fs.unlinkSync(accountKeyPath) | ||
// Handle errors that occur during account creation | ||
console.error("Error creating ACME account:", error.message); | ||
// Depending on the type of error, you might want to retry, log the error, alert someone, etc. | ||
} | ||
} | ||
} | ||
} | ||
async function requestCertificate(host, wildcard = false) { | ||
/* Place your domain(s) here */ | ||
const domains = wildcard ? [host, `*.${host}`] : [host, `www.${host}`]; | ||
async requestCertificate(host, organization_id, wildcard = false) { | ||
try { | ||
/* Create certificate request */ | ||
const [key, csr] = await Client.forge.createCsr({ | ||
commonName: domains[0], | ||
altNames: domains | ||
}); | ||
const self = this | ||
/* Request certificate */ | ||
const cert = await client.auto({ | ||
csr, | ||
email, // Replace with your email | ||
termsOfServiceAgreed: true, | ||
challengeCreateFn: async (authz, challenge, keyAuthorization) => { | ||
/* Log the URL and content for the HTTP-01 challenge */ | ||
if (challenge.type === 'http-01') { | ||
const challengeUrl = `http://${authz.identifier.value}/.well-known/acme-challenge/${challenge.token}`; | ||
const keyAuth = keyAuthorization; | ||
const hostKeyPath = keyPath + host + '/'; | ||
if (!fs.existsSync(hostKeyPath)) { | ||
fs.mkdirSync(hostKeyPath, { recursive: true }); | ||
} | ||
const domains = wildcard ? [host, `*.${host}`] : [host]; | ||
console.log('Please create a file accessible on:'); | ||
console.log(challengeUrl); | ||
console.log('With the content:'); | ||
console.log(keyAuth); | ||
/* Create certificate request */ | ||
const [key, csr] = await forge.createCsr({ | ||
commonName: domains[0], | ||
altNames: domains | ||
}); | ||
let file = { | ||
"content-type": "text/plain", | ||
"directory": "acme-challenge", | ||
"host": [ | ||
authz.identifier.value | ||
], | ||
"name": challenge.token, | ||
"organization_id": "652c8d62679eca03e0b116a7", | ||
"path": "/.well-known/acme-challenge/", | ||
"pathname": `/.well-known/acme-challenge/${challenge.token}`, | ||
"public": "true", | ||
"src": keyAuth | ||
let challenge_id = '' | ||
/* Request certificate */ | ||
const cert = await client.auto({ | ||
csr, | ||
email: [email], // Replace with your email | ||
termsOfServiceAgreed: true, | ||
challengeCreateFn: async (authz, challenge, keyAuthorization) => { | ||
if (challenge.type === 'http-01') { | ||
const httpChallenge = await self.crud.send({ | ||
method: 'object.create', | ||
array: 'files', | ||
object: { | ||
"content-type": "text/plain", | ||
"directory": "acme-challenge", | ||
"host": [ | ||
authz.identifier.value | ||
], | ||
"name": challenge.token, | ||
"organization_id": "652c8d62679eca03e0b116a7", | ||
"path": "/.well-known/acme-challenge/", | ||
"pathname": `/.well-known/acme-challenge/${challenge.token}`, | ||
"public": "true", | ||
"src": keyAuthorization | ||
}, | ||
organization_id, | ||
}); | ||
if (httpChallenge && httpChallenge.object && httpChallenge.object[0]) | ||
challenge_id = httpChallenge.object[0]._id | ||
else | ||
console.error('error creating challenge url') | ||
} else if (challenge.type === 'dns-01') { | ||
// Calculate the DNS TXT record value | ||
const dnsRecordName = `_acme-challenge.${authz.identifier.value}`; | ||
const dnsRecordValue = await client.getChallengeKeyAuthorization(challenge); | ||
console.log(`Add this TXT record to your DNS:`); | ||
console.log(`Name: ${dnsRecordName}`); | ||
console.log(`Value: ${dnsRecordValue}`); | ||
// Here, implement the logic to add the TXT record to your DNS | ||
// await updateDnsTxtRecord(dnsRecordName, dnsRecordValue); // Hypothetical function to update DNS | ||
} | ||
}, | ||
challengeRemoveFn: async (authz, challenge, keyAuthorization) => { | ||
/* Clean up challenge response here if necessary */ | ||
console.log(`Challenge removed for token: ${challenge.token}`); | ||
if (challenge.type === 'http-01') { | ||
self.crud.send({ | ||
method: 'object.delete', | ||
array: 'files', | ||
object: { | ||
_id: challenge_id | ||
}, | ||
organization_id, | ||
}); | ||
} else if (challenge.type === 'dns-01') { | ||
// await removeDnsTxtRecord(challenge); // A hypothetical function to clean up DNS | ||
} | ||
} | ||
} else if (challenge.type === 'dns-01') { | ||
// Calculate the DNS TXT record value | ||
const dnsRecordName = `_acme-challenge.${authz.identifier.value}`; | ||
const dnsRecordValue = await client.getChallengeKeyAuthorization(challenge); | ||
}); | ||
console.log(`Add this TXT record to your DNS:`); | ||
console.log(`Name: ${dnsRecordName}`); | ||
console.log(`Value: ${dnsRecordValue}`); | ||
let expires = await forge.readCertificateInfo(cert); | ||
expires = expires.notAfter; | ||
certificates.set(host, expires) | ||
// Here, implement the logic to add the TXT record to your DNS | ||
// await updateDnsTxtRecord(dnsRecordName, dnsRecordValue); // Hypothetical function to update DNS | ||
} | ||
/* Save the certificate and key */ | ||
fs.writeFileSync(hostKeyPath + 'fullchain.pem', cert); | ||
// fs.chmodSync(keyPath + 'fullchain.pem', '444') | ||
}, | ||
challengeRemoveFn: async (authz, challenge, keyAuthorization) => { | ||
/* Clean up challenge response here if necessary */ | ||
console.log(`Challenge removed for token: ${challenge.token}`); | ||
if (challenge.type === 'http-01') { | ||
/* Clean up challenge response here if necessary */ | ||
} else if (challenge.type === 'dns-01') { | ||
// await removeDnsTxtRecord(challenge); // A hypothetical function to clean up DNS | ||
fs.writeFileSync(hostKeyPath + 'private-key.pem', key); | ||
// fs.chmodSync(keyPath + 'private-key.pem', '400') | ||
process.emit('certificateCreated', host) | ||
let safeKey = host.replace(/\./g, '_'); | ||
let organization = await this.crud.send({ | ||
method: 'object.update', | ||
array: 'organizations', | ||
object: { | ||
_id: organization_id, | ||
['ssl.' + safeKey]: { cert, key: key.toString('utf-8') } | ||
}, | ||
organization_id, | ||
}); | ||
console.log('Successfully created certificate!'); | ||
return true | ||
} catch (error) { | ||
delete hosts[host] | ||
return false | ||
} | ||
} | ||
async getCertificate(host, organization_id) { | ||
const hostKeyPath = keyPath + host + '/'; | ||
let organization = await this.crud.send({ | ||
method: 'object.read', | ||
array: 'organizations', | ||
object: { | ||
_id: organization_id | ||
}, | ||
organization_id, | ||
}); | ||
if (organization && organization.object && organization.object[0]) { | ||
if (!organization.object[0].host || !organization.object[0].host.includes(host)) | ||
return false | ||
let safeKey = host.replace(/\./g, '_'); | ||
if (organization.object[0].ssl && organization.object[0].ssl[safeKey]) { | ||
let cert = organization.object[0].ssl[safeKey].cert | ||
let key = organization.object[0].ssl[safeKey].key | ||
if (cert && key) { | ||
let expires = await forge.readCertificateInfo(cert); | ||
expires = expires.notAfter; | ||
if (this.isValid(expires)) { | ||
certificates.set(host, expires) | ||
if (!fs.existsSync(hostKeyPath)) { | ||
fs.mkdirSync(hostKeyPath, { recursive: true }); | ||
} | ||
fs.writeFileSync(hostKeyPath + 'fullchain.pem', cert); | ||
// fs.chmodSync(keyPath + 'fullchain.pem', '444') | ||
fs.writeFileSync(hostKeyPath + 'private-key.pem', key); | ||
// fs.chmodSync(keyPath + 'private-key.pem', '400') | ||
// TODO: emit change so that nginx can reload | ||
return true | ||
} | ||
} | ||
} | ||
} | ||
return await this.requestCertificate(host, organization_id, false) | ||
} | ||
async checkCertificate(host, organization_id) { | ||
let hostname = host.split(':')[0] | ||
if (hostname === 'localhost' || hostname === '127.0.0.1') | ||
return true | ||
let expires = certificates.get(host) | ||
if (expires && this.isValid(expires)) { | ||
return true | ||
} | ||
}); | ||
/* Save the certificate and key */ | ||
fs.writeFileSync(keyPath + 'certificate.pem', cert); | ||
fs.chmodSync(keyPath + 'certificate.pem', '444') | ||
const hostKeyPath = keyPath + host + '/'; | ||
if (fs.existsSync(hostKeyPath + 'fullchain.pem')) { | ||
expires = fs.readFileSync(hostKeyPath + 'fullchain.pem', 'utf8'); | ||
expires = await forge.readCertificateInfo(expires); | ||
expires = expires.notAfter; | ||
if (this.isValid(expires)) { | ||
certificates.set(host, expires) | ||
return true | ||
} | ||
} | ||
fs.writeFileSync(keyPath + 'private-key.pem', key); | ||
fs.chmodSync(keyPath + 'certificate.pem', '400') | ||
if (!hosts[host]) | ||
hosts[host] = this.getCertificate(host, organization_id) | ||
return hosts[host] | ||
} | ||
console.log('Successfully created certificate!'); | ||
} | ||
isValid(expires) { | ||
let currentDate = new Date(); | ||
currentDate.setDate(currentDate.getDate() + DAYS); | ||
currentDate.setHours(currentDate.getHours() + HOURS); | ||
currentDate.setMinutes(currentDate.getMinutes() + MINUTES); | ||
if (expires && currentDate < expires) { | ||
return true; // SSL is still valid, no need to renew | ||
} | ||
} | ||
init().catch(err => { | ||
console.error('Error initializing ACME client:', err); | ||
// TODO: Handle initialization error (possibly retry or exit) | ||
}); | ||
} | ||
module.exports = { requestCertificate } | ||
// requestCertificate(host).catch(err => { | ||
// console.error('Error creating certificate:', err); | ||
// }); | ||
module.exports = CoCreateAcme; |
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
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
44839
287