|
1 | 1 | import createPrisma from '@/lib/create-prisma'
|
2 | 2 | import { verifyDomainDNS, issueDomainCertificate, checkCertificateStatus, getValidationValues } from '@/lib/domain-verification'
|
3 | 3 |
|
4 |
| -// This worker verifies the DNS and SSL certificates for domains that are pending or failed |
5 | 4 | export async function domainVerification ({ data: { domainId }, boss }) {
|
| 5 | + // establish connection to database |
6 | 6 | const models = createPrisma({ connectionParams: { connection_limit: 1 } })
|
7 |
| - console.log('domainVerification', domainId) |
8 | 7 | try {
|
| 8 | + // get domain from database |
9 | 9 | const domain = await models.customDomain.findUnique({ where: { id: domainId } })
|
10 |
| - |
| 10 | + // if we can't find the domain, bail without scheduling a retry |
11 | 11 | if (!domain) {
|
12 | 12 | throw new Error(`domain with ID ${domainId} not found`)
|
13 | 13 | }
|
14 | 14 |
|
15 |
| - const result = await verifyDomain(domain, models) |
16 |
| - |
17 |
| - if (result?.status === 'ACTIVE') { |
18 |
| - console.log(`domain ${domain.domain} verified`) |
19 |
| - return |
20 |
| - } |
21 |
| - |
22 |
| - if (result?.status === 'HOLD') { |
23 |
| - console.log(`domain ${domain.domain} is on hold after too many failed attempts`) |
24 |
| - return |
25 |
| - } |
26 |
| - |
27 |
| - if (result?.status === 'PENDING') { |
28 |
| - throw new Error(`domain ${domain.domain} is still pending verification, will retry`) |
| 15 | + // start verification process |
| 16 | + const result = await verifyDomain(domain, boss) |
| 17 | + |
| 18 | + // update the domain with the result |
| 19 | + await models.customDomain.update({ |
| 20 | + where: { id: domainId }, |
| 21 | + data: { ...domain, ...result } |
| 22 | + }) |
| 23 | + |
| 24 | + // if the result is PENDING it means we still have to verify the domain |
| 25 | + // if it's not PENDING, we stop the verification process. |
| 26 | + if (result.status === 'PENDING') { |
| 27 | + // we still need to verify the domain, schedule the job to run again in 5 minutes |
| 28 | + const jobId = await boss.send('domainVerification', { domainId }, { |
| 29 | + startAfter: 1000 * 60 * 5 // start the job after 5 minutes |
| 30 | + }) |
| 31 | + console.log(`domain ${domain.domain} is still pending verification, created job with ID ${jobId} to run in 5 minutes`) |
29 | 32 | }
|
30 | 33 | } catch (error) {
|
31 | 34 | console.error(`couldn't verify domain with ID ${domainId}: ${error.message}`)
|
32 |
| - throw error |
33 | 35 | }
|
34 | 36 | }
|
35 | 37 |
|
36 |
| -async function verifyDomain (domain, models) { |
37 |
| - // track verification |
38 |
| - const data = { ...domain, lastVerifiedAt: new Date() } |
39 |
| - data.verification = data.verification || { dns: {}, ssl: {} } |
40 |
| - data.failedAttempts = data.failedAttempts || 0 |
41 |
| - |
42 |
| - if (data.verification?.dns?.state !== 'VERIFIED') { |
43 |
| - await verifyDNS(data) |
| 38 | +async function verifyDomain (domain) { |
| 39 | + const lastVerifiedAt = new Date() |
| 40 | + const verification = domain.verification || { dns: {}, ssl: {} } |
| 41 | + let status = domain.status || 'PENDING' |
| 42 | + |
| 43 | + // step 1: verify DNS [CNAME and TXT] |
| 44 | + // if DNS is not already verified |
| 45 | + let dnsState = verification.dns.state || 'PENDING' |
| 46 | + if (dnsState !== 'VERIFIED') { |
| 47 | + dnsState = await verifyDNS(domain.domain, verification.dns.txt) |
| 48 | + |
| 49 | + // log the result, throw an error if we couldn't verify the DNS |
| 50 | + switch (dnsState) { |
| 51 | + case 'VERIFIED': |
| 52 | + console.log(`DNS verification for ${domain.domain} is ${dnsState}, proceeding to SSL verification`) |
| 53 | + break |
| 54 | + case 'PENDING': |
| 55 | + console.log(`DNS verification for ${domain.domain} is ${dnsState}, will retry DNS verification in 5 minutes`) |
| 56 | + break |
| 57 | + default: |
| 58 | + dnsState = 'PENDING' |
| 59 | + console.log(`couldn't verify DNS for ${domain.domain}, will retry DNS verification in 5 minutes`) |
| 60 | + } |
44 | 61 | }
|
45 | 62 |
|
46 |
| - if (data.verification?.dns?.state === 'VERIFIED' && (!data.verification?.ssl?.arn || data.verification?.ssl?.state === 'FAILED')) { |
47 |
| - await issueCertificate(data) |
| 63 | + // step 2: certificate issuance |
| 64 | + // if DNS is verified and we don't have a SSL certificate, issue one |
| 65 | + let sslArn = verification.ssl.arn || null |
| 66 | + let sslState = verification.ssl.state || 'PENDING' |
| 67 | + if (dnsState === 'VERIFIED' && !sslArn) { |
| 68 | + sslArn = await issueDomainCertificate(domain.domain) |
| 69 | + if (sslArn) { |
| 70 | + console.log(`SSL certificate issued for ${domain.domain} with ARN ${sslArn}, will verify with ACM in 5 minutes`) |
| 71 | + } else { |
| 72 | + console.log(`couldn't issue SSL certificate for ${domain.domain}, will retry certificate issuance in 5 minutes`) |
| 73 | + } |
48 | 74 | }
|
49 | 75 |
|
50 |
| - if (data.verification?.dns?.state === 'VERIFIED' && data.verification?.ssl?.state !== 'VERIFIED' && data.verification?.ssl?.arn) { |
51 |
| - await updateCertificateStatus(data) |
| 76 | + // step 3: get validation values from ACM |
| 77 | + // if we have a certificate and we don't already have the validation values |
| 78 | + let acmValidationCname = verification.ssl.cname || null |
| 79 | + let acmValidationValue = verification.ssl.value || null |
| 80 | + if (sslArn && !acmValidationCname && !acmValidationValue) { |
| 81 | + const { cname, value } = await getValidationValues(sslArn) |
| 82 | + acmValidationCname = cname |
| 83 | + acmValidationValue = value |
| 84 | + if (acmValidationCname && acmValidationValue) { |
| 85 | + console.log(`Validation values retrieved for ${domain.domain}, will check ACM validation status`) |
| 86 | + } else { |
| 87 | + console.log(`couldn't retrieve validation values for ${domain.domain}, will retry to request validation values from ACM in 5 minutes`) |
| 88 | + } |
52 | 89 | }
|
53 | 90 |
|
54 |
| - if (data.verification?.dns?.state === 'PENDING' || data.verification?.ssl?.state === 'PENDING') { |
55 |
| - data.failedAttempts += 1 |
56 |
| - // exponential backoff at the 11th attempt is roughly 48 hours |
57 |
| - if (data.failedAttempts > 11) { |
58 |
| - data.status = 'HOLD' |
59 |
| - data.failedAttempts = 0 |
| 91 | + // step 4: check if the certificate is validated by ACM |
| 92 | + // if DNS is verified and we have a SSL certificate |
| 93 | + // it can happen that we just issued the certificate and it's not yet validated by ACM |
| 94 | + if (dnsState === 'VERIFIED' && sslArn && sslState !== 'VERIFIED') { |
| 95 | + sslState = await checkCertificateStatus(sslArn) |
| 96 | + switch (sslState) { |
| 97 | + case 'VERIFIED': |
| 98 | + console.log(`SSL certificate for ${domain.domain} is ${sslState}, verification routine complete`) |
| 99 | + break |
| 100 | + case 'PENDING': |
| 101 | + console.log(`SSL certificate for ${domain.domain} is ${sslState}, will check again with ACM in 5 minutes`) |
| 102 | + break |
| 103 | + default: |
| 104 | + sslState = 'PENDING' |
| 105 | + console.log(`couldn't verify SSL certificate for ${domain.domain}, will retry certificate validation with ACM in 5 minutes`) |
60 | 106 | }
|
61 | 107 | }
|
62 | 108 |
|
63 |
| - if (data.verification?.dns?.state === 'VERIFIED' && data.verification?.ssl?.state === 'VERIFIED') { |
64 |
| - data.status = 'ACTIVE' |
| 109 | + // step 5: update the status of the domain |
| 110 | + // if the domain is fully verified, set the status to active |
| 111 | + if (dnsState === 'VERIFIED' && sslState === 'VERIFIED') { |
| 112 | + status = 'ACTIVE' |
| 113 | + } |
| 114 | + // if the domain has failed in some way and it's been 48 hours, put it on hold |
| 115 | + if (status !== 'ACTIVE' && domain.createdAt < new Date(Date.now() - 1000 * 60 * 60 * 24 * 2)) { |
| 116 | + status = 'HOLD' |
65 | 117 | }
|
66 | 118 |
|
67 |
| - return await models.customDomain.update({ where: { id: domain.id }, data }) |
68 |
| -} |
69 |
| - |
70 |
| -async function verifyDNS (data) { |
71 |
| - const { txtValid, cnameValid } = await verifyDomainDNS(data.domain, data.verification.dns.txt) |
72 |
| - console.log(`${data.domain}: TXT ${txtValid ? 'valid' : 'invalid'}, CNAME ${cnameValid ? 'valid' : 'invalid'}`) |
73 |
| - |
74 |
| - data.verification.dns.state = txtValid && cnameValid ? 'VERIFIED' : 'PENDING' |
75 |
| - return data |
76 |
| -} |
77 |
| - |
78 |
| -async function issueCertificate (data) { |
79 |
| - const certificateArn = await issueDomainCertificate(data.domain) |
80 |
| - console.log(`${data.domain}: Certificate issued: ${certificateArn}`) |
81 |
| - |
82 |
| - if (certificateArn) { |
83 |
| - const sslState = await checkCertificateStatus(certificateArn) |
84 |
| - console.log(`${data.domain}: Issued certificate status: ${sslState}`) |
85 |
| - |
86 |
| - if (sslState) data.verification.ssl.state = sslState |
87 |
| - data.verification.ssl.arn = certificateArn |
88 |
| - |
89 |
| - if (sslState !== 'VERIFIED') { |
90 |
| - try { |
91 |
| - const { cname, value } = await getValidationValues(certificateArn) |
92 |
| - data.verification.ssl.cname = cname |
93 |
| - data.verification.ssl.value = value |
94 |
| - } catch (error) { |
95 |
| - console.error(`Failed to get validation values for domain ${data.domain}:`, error) |
| 119 | + return { |
| 120 | + lastVerifiedAt, |
| 121 | + status, |
| 122 | + verification: { |
| 123 | + dns: { |
| 124 | + state: dnsState |
| 125 | + }, |
| 126 | + ssl: { |
| 127 | + arn: sslArn, |
| 128 | + state: sslState, |
| 129 | + cname: acmValidationCname, |
| 130 | + value: acmValidationValue |
96 | 131 | }
|
97 | 132 | }
|
98 |
| - } else { |
99 |
| - data.verification.ssl.state = 'PENDING' |
100 | 133 | }
|
101 |
| - |
102 |
| - return data |
103 | 134 | }
|
104 | 135 |
|
105 |
| -async function updateCertificateStatus (data) { |
106 |
| - const sslState = await checkCertificateStatus(data.verification.ssl.arn) |
107 |
| - console.log(`${data.domain}: Certificate status: ${sslState}`) |
108 |
| - if (sslState) data.verification.ssl.state = sslState |
109 |
| - return data |
| 136 | +async function verifyDNS (cname, txt) { |
| 137 | + const { cnameValid, txtValid } = await verifyDomainDNS(cname, txt) |
| 138 | + console.log(`${cname}: CNAME ${cnameValid ? 'valid' : 'invalid'}, TXT ${txtValid ? 'valid' : 'invalid'}`) |
| 139 | + |
| 140 | + const dnsState = cnameValid && txtValid ? 'VERIFIED' : 'PENDING' |
| 141 | + return dnsState |
110 | 142 | }
|
0 commit comments