lets-encrypt-acme-client
Version:
Automatically Create and Renew LetsEncrypt! SSL Certificates, including Wildcard Certificates for supported DNS Providers
227 lines (186 loc) • 7.08 kB
JavaScript
/**
* Copyright © 2024 FirstTimeEZ
* https://github.com/FirstTimeEZ
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as acme from 'base-acme-client';
const STATUS_PENDING = "pending";
/**
* Cloudflare Provider
*/
export const CF_Provider = {
dnsChallenges: [],
internalCloudFlareProvider: async (dnsProvider, account, acmeDirectory, nextNonce, acmeKeyChain) => {
if (dnsProvider.token == undefined) {
console.log("Expected API Token with DNS Edit permissions for Cloudflare DNS Provider");
return true;
}
for (let index = 0; index < CF_Provider.dnsChallenges.length; index++) {
if (CF_Provider.dnsChallenges[index].status == STATUS_PENDING) {
const response = await findOrCreateRecord(dnsProvider.token, CF_Provider.dnsChallenges[index].domain, CF_Provider.dnsChallenges[index].answer, dnsProvider.zone);
console.log(response);
}
}
const wait = CF_Provider.dnsChallenges.some(e => e.status == STATUS_PENDING);
if (wait) {
await new Promise((resolve) => {
console.log("Waiting a minute before asking server to verify the challenges");
setTimeout(async () => {
for (let index = 0; index < CF_Provider.dnsChallenges.length; index++) {
if (CF_Provider.dnsChallenges[index].status == STATUS_PENDING) {
const auth = await acme.postAsGetChal(account.location, nextNonce, acmeKeyChain.privateKey, CF_Provider.dnsChallenges[index].url, acmeDirectory);
auth.get.status ? console.log("Next Nonce", (nextNonce = auth.nonce), "Authed Challenge") : console.error("Error getting auth", auth.error);
}
}
resolve();
}, 60000);
});
} else {
console.log("Already Completed Challenges");
}
return false;
}
}
export function extractZoneName(split) {
let name = "";
for (let index = 1; index < split.length; index++) {
name += `${split[index]}`
if (index + 1 < split.length) {
name += ".";
}
}
return name;
}
async function getZone(apiToken, domainName) {
const response = await acme.fetchAndRetryUntilOk(`https://api.cloudflare.com/client/v4/zones`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
}
}, 2);
if (response && response.ok) {
const zones = await response.json();
for (let index = 0; index < zones.result.length; index++) {
if (zones.result[index].name.includes(domainName)) {
return zones.result[index].id;
}
if (zones.result[index].name.includes(extractZoneName(domainName.split('.')))) {
return zones.result[index].id;
}
}
}
return undefined;
}
async function getRecords(zoneId, apiToken) {
const response = await acme.fetchAndRetryUntilOk(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
}
}, 2);
if (response && response.ok) {
return await response.json();
}
else {
return undefined;
}
}
function findChallengeRecord(data, token) {
for (let index = 0; index < data.result.length; index++) {
const element = data.result[index];
if (element.type == "TXT" && element.content.includes(token)) {
return element;
}
}
return undefined;
}
async function findOrCreateRecord(apiToken, domainName, token, zone = undefined) {
if (zone == undefined) {
zone = await getZone(apiToken, domainName);
}
if (zone == undefined) {
return errorTemplate("cf:failed:zone", "Could not dertermine the DNS zone of the Domain Name Provided", 877771);
}
const zoneDnsRecords = await getRecords(zone, apiToken);
if (zoneDnsRecords == undefined) {
return errorTemplate("cf:failed:zoneDnsRecords", "Could not get DNS Records, check your zoneId and apiToken", 877772);
}
let challengeRecord = findChallengeRecord(zoneDnsRecords, token);
challengeRecord && console.log("Found", challengeRecord.content);
if (challengeRecord == undefined) {
const newRecord = await createRecord(domainName, token, zone, apiToken, 60);
if (newRecord) {
challengeRecord = newRecord.result;
challengeRecord.changed = true;
challengeRecord && console.log("Created", challengeRecord.content);
}
}
return {
get: challengeRecord
}
}
async function createRecord(domainName, challengeToken, dnsZoneId, cloudFlareApiToken, ttl = 3600) {
domainName.includes("*.") && (domainName = domainName.slice(2));
const dnsRecord = {
comment: "Domain verification record",
name: "_acme-challenge." + domainName,
content: `"${challengeToken}"`,
ttl: ttl,
type: "TXT"
};
const response = await acme.fetchAndRetryUntilOk(`https://api.cloudflare.com/client/v4/zones/${dnsZoneId}/dns_records`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${cloudFlareApiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(dnsRecord)
}, 2);
if (response && response.ok) {
return await response.json();
}
else {
return undefined;
}
}
async function updateRecord(challengeToken, dnsZoneId, cloudFlareApiToken, dnsRecordId) {
const dnsRecord = {
content: `"${challengeToken}"`,
};
const response = await acme.fetchAndRetryUntilOk(`https://api.cloudflare.com/client/v4/zones/${dnsZoneId}/dns_records/${dnsRecordId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${cloudFlareApiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(dnsRecord)
});
if (response && response.ok) {
return await response.json();
}
else {
return undefined;
}
}
function errorTemplate(type, details, status) {
return {
error: {
type: type,
detail: details,
status: status
}
}
}