From 650d6af4bdcd47d9ed90245860a4b0dbc9bbfb91 Mon Sep 17 00:00:00 2001 From: "wille.io" Date: Thu, 23 May 2024 16:41:55 +0200 Subject: [PATCH] acme directory url now optional (Let's Encrypt is default); wait 15 seconds for acme server to catch up with added cloudflare dns record; readme: cloudflare cli example without sudo; added note for http challenges without sudo; etc. --- LICENSE | 2 +- README.md | 19 ++++++++++--------- acme.ts | 33 ++++++++++++++++++++++----------- cli.ts | 12 +++++++----- deno.json | 5 +++++ 5 files changed, 45 insertions(+), 26 deletions(-) create mode 100644 deno.json diff --git a/LICENSE b/LICENSE index cb85418..0956464 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Mike Wille +Copyright (c) 2024 Mike Wille Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e581ad0..acb90a9 100644 --- a/README.md +++ b/README.md @@ -16,28 +16,29 @@ Use the CLI as a standalone acme client, or use the acme.ts library to use it in ## CLI How to get & use the CLI: -``` -sudo deno install -A --allow-read=. --allow-write=. --allow-net --name acme --root /usr/local/ https://deno.land/x/acme@v0.3.1/cli.ts +```bash +sudo deno install -A --allow-read=. --allow-write=. --allow-net --name acme --root /usr/local/ https://deno.land/x/acme@v0.4.0/cli.ts # http challenge: sudo acme http example.com,subdomain.example.com # cloudflare dns challenge: -sudo acme cloudflare example.com,subdomain.example.com +acme cloudflare example.com,subdomain.example.com ``` +Note: For http challenges permissions to bind to port 80 are needed. Otherwise use the root user or use `sudo` - like in the example above. ## Library -To use acme as a library in your application, add the following: -``` -import * as ACME from "https://deno.land/x/acme@v0.3.1/acme.ts" +To use acme as a library in your application, add the following (minimal example with temporary & anonymous acme account creation): +```typescript +import * as ACME from "https://deno.land/x/acme@v0.4.0/acme.ts" // http challenge: -const { domainCertificates } = await ACME.getCertificatesWithHttp("example.com", "https://acme-staging-v02.api.letsencrypt.org/directory"); +const { domainCertificates } = await ACME.getCertificatesWithHttp("example.com"); console.log(domainCertificates); // cloudflare dns challenge: const cloudflareToken = Deno.env.get("CLOUDFLARE_TOKEN"); -const { domainCertificates } = await ACME.getCertificatesWithCloudflare(cloudflareToken, "example.com", "https://acme-staging-v02.api.letsencrypt.org/directory"); +const { domainCertificates } = await ACME.getCertificatesWithCloudflare(cloudflareToken, "example.com"); console.log(domainCertificates); ``` ## License -MIT +MIT \ No newline at end of file diff --git a/acme.ts b/acme.ts index 2ab53f1..f9718b8 100644 --- a/acme.ts +++ b/acme.ts @@ -34,20 +34,30 @@ async function createSession(acmeDirectoryUrl: string, options?: { pemAccountKey } -export async function getCertificateWithHttp(domainName: string, acmeDirectoryUrl: string, options?: { yourEmail?: string, +function getAcmeDirectoryUrl(acmeDirectoryUrl?: string): string +{ + if (acmeDirectoryUrl) + return acmeDirectoryUrl; + console.warn("IMPORTANT: By not supplying a acme directory url, you are always accepting Let's Encrypt's current general Terms of Service and their Subscriber Agreement which you can find at 'https://acme-v02.api.letsencrypt.org/directory' in json key 'meta.termsOfService'"); + return "https://acme-v02.api.letsencrypt.org/directory"; +} + + +export async function getCertificateWithHttp(domainName: string, options?: { acmeDirectoryUrl: string, yourEmail?: string, pemAccountKeys?: AccountKeys, csrInfo?: CSRInfo }) : Promise<{domainCertificate: DomainCertificate, pemAccountKeys: AccountKeys}> { - const ret = await getCertificatesWithHttp([{ domainName }], acmeDirectoryUrl, options); + const ret = await getCertificatesWithHttp([{ domainName }], options); return { domainCertificate: ret.domainCertificates[0], pemAccountKeys: ret.pemAccountKeys }; } -export async function getCertificatesWithHttp(domains: Domain[], acmeDirectoryUrl: string, options?: { yourEmail?: string, +export async function getCertificatesWithHttp(domains: Domain[], options?: { acmeDirectoryUrl: string, yourEmail?: string, pemAccountKeys?: AccountKeys, csrInfo?: CSRInfo }) : Promise<{domainCertificates: DomainCertificate[], pemAccountKeys: AccountKeys}> { - const session = await createSession(acmeDirectoryUrl, { pemAccountKeys: options?.pemAccountKeys, email: options?.yourEmail }); + const session = await createSession(getAcmeDirectoryUrl(options?.acmeDirectoryUrl), + { pemAccountKeys: options?.pemAccountKeys, email: options?.yourEmail }); return { domainCertificates: await new ACMEHttp(session, domains, options?.yourEmail, options?.csrInfo).getCertificates(), pemAccountKeys: await session.exportAccount(), @@ -55,20 +65,21 @@ export async function getCertificatesWithHttp(domains: Domain[], acmeDirectoryUr } -export async function getCertificateWithCloudflare(bearer: string, domainName: string, acmeDirectoryUrl: string, options: { yourEmail?: string, +export async function getCertificateWithCloudflare(bearer: string, domainName: string, options?: { acmeDirectoryUrl: string, yourEmail?: string, pemAccountKeys?: AccountKeys, csrInfo?: CSRInfo }) : Promise<{domainCertificate: DomainCertificate, pemAccountKeys: AccountKeys}> { - const ret = await getCertificatesWithCloudflare(bearer, [{ domainName }], acmeDirectoryUrl, options); + const ret = await getCertificatesWithCloudflare(bearer, [{ domainName }], options); return { domainCertificate: ret.domainCertificates[0], pemAccountKeys: ret.pemAccountKeys }; } -export async function getCertificatesWithCloudflare(bearer: string, domains: Domain[], acmeDirectoryUrl: string, options?: { yourEmail?: string, +export async function getCertificatesWithCloudflare(bearer: string, domains: Domain[], options?: { acmeDirectoryUrl: string, yourEmail?: string, pemAccountKeys?: AccountKeys, csrInfo?: CSRInfo }) : Promise<{domainCertificates: DomainCertificate[], pemAccountKeys: AccountKeys}> { - const session = await createSession(acmeDirectoryUrl, { pemAccountKeys: options?.pemAccountKeys, email: options?.yourEmail }); + const session = await createSession(getAcmeDirectoryUrl(options?.acmeDirectoryUrl), + { pemAccountKeys: options?.pemAccountKeys, email: options?.yourEmail }); return { domainCertificates: await new ACMECloudflare(bearer, session, domains, options?.yourEmail, options?.csrInfo).getCertificates(), pemAccountKeys: await session.exportAccount(), @@ -817,8 +828,8 @@ class ACMECloudflare extends ACMEBase // creating txt record(s) done - waiting for acme's dns to catch up - console.log("giving the acme server time (3s) to catch up..."); - await waitSeconds(3); + console.log("giving the acme server time (15s) to catch up..."); + await waitSeconds(15); // TODO: shorter wait, try again if 'invalid' for n times until not 'invalid' anymore // fire all challenges @@ -951,7 +962,7 @@ class ACMEHttp extends ACMEBase // TODO: don't use serve - serve((request: Request): Response => + serve((request: Request): Response => // TODO: AbortController { //console.log("!!!"); if (!request.url.endsWith("/.well-known/acme-challenge/" + token)) diff --git a/cli.ts b/cli.ts index 483cf71..cd41cb3 100644 --- a/cli.ts +++ b/cli.ts @@ -118,11 +118,13 @@ async function shared2(accountDirectory: string, accountKeys: ACME.AccountKeys | const command = new Command() .name("acme-cli") -.version("v0.3.1") +.version("v0.4.0") .description("Get certificates for your domains and or your domains their subdomains with the specified challenge type from an acme server. \r\n"+ "One certificate is created per challenge argument. \r\n"+ - "You can either get a certificate for a domain *and* its subdomains or for a domain only (without subdomains). It is not possible to get a certificate with only subdomains (without its parent domain).\r\n"+ - "Subdomains are added to the domain name with commas. Example: example.com,subdomain.example.com,another-subdomain.example.com") + "You can either get a certificate for a domain *and* its subdomains or for a domain only (without subdomains). It is not possible to get a certificate with only subdomains (without its parent domain). \r\n"+ + "Subdomains are added to the domain name with commas. Example: example.com,subdomain.example.com,another-subdomain.example.com \r\n" + + "IMPORTANT: By not supplying a acme directory url, you are always accepting Let's Encrypt's current general Terms of Service and their Subscriber Agreement which you can find at 'https://acme-v02.api.letsencrypt.org/directory' in json key 'meta.termsOfService'" +) .globalOption("-d, --directory ", "Https url to the acme server's acme directory.", { default: "https://acme-v02.api.letsencrypt.org/directory" as const }) .globalOption("-e, --email ", "Your email address for the acme server to notify you on notifications of your certificates") .globalOption("-a, --accountDirectory ", "The directory where the account keys of your acme server will be read from / written to", { default: `${Deno.env.get("HOME") || Deno.cwd()}/.deno-acme` as const }) @@ -143,7 +145,7 @@ const command = new Command() const { domains, accountKeys } = await shared1(accountDirectory, domainsWithSubdomains); const { domainCertificates, pemAccountKeys } = - await ACME.getCertificatesWithHttp(domains, directory, { yourEmail: email, pemAccountKeys: accountKeys, /*csrInfo*/ }); + await ACME.getCertificatesWithHttp(domains, { acmeDirectoryUrl: directory, yourEmail: email, pemAccountKeys: accountKeys, /*csrInfo*/ }); await shared2(accountDirectory, accountKeys, pemAccountKeys, domainCertificates); Deno.exit(0); @@ -164,7 +166,7 @@ const command = new Command() const { domains, accountKeys } = await shared1(accountDirectory, domainsWithSubdomains); const { domainCertificates, pemAccountKeys } = - await ACME.getCertificatesWithCloudflare(cloudflareBearer, domains, directory, { yourEmail: email, pemAccountKeys: accountKeys, /*csrInfo*/ }); + await ACME.getCertificatesWithCloudflare(cloudflareBearer, domains, { acmeDirectoryUrl: directory, yourEmail: email, pemAccountKeys: accountKeys, /*csrInfo*/ }); await shared2(accountDirectory, accountKeys, pemAccountKeys, domainCertificates); Deno.exit(0); diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..11aa656 --- /dev/null +++ b/deno.json @@ -0,0 +1,5 @@ +{ + "name": "@mw/acme", + "version": "0.4.0", + "exports": "./acme.ts" +} \ No newline at end of file