Skip to content

Commit

Permalink
Added support for multi values in DNS records
Browse files Browse the repository at this point in the history
  • Loading branch information
Cosmin-Parvulescu committed Jun 6, 2023
1 parent 785c6a2 commit 8fa6523
Show file tree
Hide file tree
Showing 14 changed files with 94 additions and 85 deletions.
60 changes: 7 additions & 53 deletions apps/console/app/routes/apps/$clientId/domain-wip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ import { BadRequestError } from '@proofzero/errors'
import { getRollupReqFunctionErrorWrapper } from '@proofzero/utils/errors'

import createStarbaseClient from '@proofzero/platform-clients/starbase'
import {
getAuthzHeaderConditionallyFromToken,
getDNSRecordValue,
} from '@proofzero/utils'
import { getAuthzHeaderConditionallyFromToken } from '@proofzero/utils'
import { generateTraceContextHeaders } from '@proofzero/platform-middleware/trace'

import type { CustomDomain } from '@proofzero/platform.starbase/src/types'
Expand All @@ -37,11 +34,6 @@ import { requireJWT } from '~/utilities/session.server'

import dangerVector from '~/images/danger.svg'

import createEmailClient from '@proofzero/platform-clients/email'
import { GetDNSSecurityValuesResult } from '@proofzero/platform/email/src/jsonrpc/methods/getDNSSecurityValues'

import { parse } from 'tldts'

type AppData = { customDomain?: CustomDomain; hostname: string; cname: string }

export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper(
Expand All @@ -61,15 +53,7 @@ export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper(

const { hostname } = new URL(PASSPORT_URL)

const emailClient = createEmailClient(
Email,
generateTraceContextHeaders(context.traceSpan)
)

const emailDNSSecurityValues =
await emailClient.getDNSSecurityValues.query()

return json({ customDomain, hostname, emailDNSSecurityValues })
return json({ customDomain, hostname })
}
)

Expand Down Expand Up @@ -118,8 +102,7 @@ export default () => {
const fetcher = useFetcher()
const actionData = useActionData<AppData>()
const loaderData = useLoaderData<AppData>()
const { customDomain, hostname, emailDNSSecurityValues } =
fetcher.data || actionData || loaderData
const { customDomain, hostname } = fetcher.data || actionData || loaderData
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>()

useEffect(() => {
Expand Down Expand Up @@ -147,7 +130,6 @@ export default () => {
fetcher={fetcher}
customDomain={customDomain}
hostname={hostname}
emailDNSSecurityValues={emailDNSSecurityValues}
/>
)}
</section>
Expand Down Expand Up @@ -190,21 +172,19 @@ type HostnameStatusProps = {
fetcher: FetcherWithComponents<AppData>
customDomain: CustomDomain
hostname: string
emailDNSSecurityValues: GetDNSSecurityValuesResult
}

const HostnameStatus = ({
fetcher,
customDomain,
hostname,
emailDNSSecurityValues,
}: HostnameStatusProps) => {
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const isPreValidated =
customDomain.status === 'active' && customDomain.ssl.status === 'active'
const isValidated =
isPreValidated &&
customDomain.dns_records.every((r) => r.value === r.expected_value)
customDomain.dns_records.every((r) => r.value?.includes(r.expected_value))
const bgStatusColor = isValidated ? 'bg-green-600' : 'bg-orange-500'
const textStatusColor = isValidated ? 'text-green-600' : 'text-orange-500'
const statusText = isValidated ? 'Validated' : 'Not Validated'
Expand Down Expand Up @@ -284,39 +264,13 @@ const HostnameStatus = ({
Step 2: CNAME Record
</Text>
<div className="flex flex-col p-4 space-y-5 box-border border rounded-lg">
{/* <TXTRecord
title="SPF"
name={`${customDomain.hostname}`}
value={`v=spf1 include:${emailDNSSecurityValues.spfHost} ~all`}
statusColor="bg-orange-500"
/>
<TXTRecord
title="DKIM"
name={`${emailDNSSecurityValues.dkimSelector}._domainkey.${customDomain.hostname}`}
value={`v=DKIM1; p=${emailDNSSecurityValues.dkimPublicKey}`}
statusColor="bg-orange-500"
/>
<TXTRecord
title="DMARC"
name={`_dmarc.${customDomain.hostname}`}
value={`v=DMARC1; p=quarantine; rua=mailto:${emailDNSSecurityValues.dmarcEmail}`}
statusColor="bg-orange-500"
/>
<TXTRecord
title="DMARC"
name={`_dmarc.${parse(customDomain.hostname).domain}`}
value={`v=DMARC1; p=quarantine; sp=quarantine; rua=mailto:${emailDNSSecurityValues.dmarcEmail}`}
statusColor="bg-orange-500"
/> */}
{isPreValidated &&
Array.from(customDomain.dns_records || []).map((r) => (
<DNSRecord
title={r.record_type === 'CNAME' ? '' : r.name}
name={r.record_type !== 'CNAME' ? r.name : undefined}
title={''}
type={r.record_type}
validated={r.value === r.expected_value}
validated={r.value?.includes(r.expected_value) ?? false}
value={r.expected_value}
key={r.expected_value}
/>
Expand Down
1 change: 0 additions & 1 deletion apps/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
"react-helmet": "6.1.0",
"react-icons": "4.8.0",
"tiny-invariant": "1.3.1",
"tldts": "6.0.5",
"viem": "0.3.41",
"wagmi": "1.0.9"
},
Expand Down
4 changes: 0 additions & 4 deletions apps/console/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ services = [
{ binding = "Starbase", service = "starbase" },
{ binding = "Images", service = "images" },
{ binding = "Account", service = "account" },
{ binding = "Email", service = "email" },
]

# development
Expand Down Expand Up @@ -61,7 +60,6 @@ services = [
{ binding = "Starbase", service = "starbase-dev" },
{ binding = "Images", service = "images-dev" },
{ binding = "Account", service = "account-dev" },
{ binding = "Email", service = "email-dev" },
]


Expand Down Expand Up @@ -91,7 +89,6 @@ services = [
{ binding = "Starbase", service = "starbase-next" },
{ binding = "Images", service = "images-next" },
{ binding = "Account", service = "account-next" },
{ binding = "Email", service = "email-next" },
]

# kv_namespaces = [
Expand Down Expand Up @@ -123,7 +120,6 @@ services = [
{ binding = "Starbase", service = "starbase-current", environment = "production" },
{ binding = "Images", service = "images-current" },
{ binding = "Account", service = "account-current" },
{ binding = "Email", service = "email-current" },
]

# kv_namespaces = [
Expand Down
29 changes: 18 additions & 11 deletions packages/utils/getDNSRecordValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as dnsPacket from '@dnsquery/dns-packet'
export default async function (
domain: string,
recordType: 'TXT' | 'CNAME'
): Promise<string | null> {
): Promise<string[] | undefined> {
function getRandomInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
Expand Down Expand Up @@ -32,14 +32,21 @@ export default async function (

const responseBuffer = new Uint8Array(await dnsQuery.arrayBuffer())
const packet = dnsPacket.decode(responseBuffer)
if (!packet.answers || !packet.answers.length) return null

//We take only the first answer as if we're being specific enough with the
//domain, there should only be one
const response = packet.answers[0]
const recValue =
response.type === 'TXT'
? new TextDecoder().decode((response.data as Buffer[])[0])
: (response.data as string)
return recValue
if (!packet.answers || !packet.answers.length) return undefined

const td = new TextDecoder()
const values = packet.answers.map((a) => {
if (a.type === 'TXT') {
const strParts = new Array(a.data.length)
for (let i = 0; i < a.data.length; i++) {
strParts.push(td.decode(a.data[i] as Buffer))
}

return strParts.join('')
}

return a.data as string
})

return values
}
1 change: 1 addition & 0 deletions platform/starbase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"lodash": "4.17.21",
"multiformats": "10.0.2",
"tiny-invariant": "1.3.1",
"tldts": "6.0.5",
"ts-set-utils": "0.2.0"
}
}
9 changes: 8 additions & 1 deletion platform/starbase/src/jsonrpc/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { BaseContext, DeploymentMetadata } from '@proofzero/types'
import type { inferAsyncReturnType } from '@trpc/server'
import type { Environment } from '../types'
import createEdgesClient from '@proofzero/platform-clients/edges'
import createEmailClient from '@proofzero/platform-clients/email'
import { AccountURN } from '@proofzero/urns/account'
import { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'
import { ApplicationURN } from '@proofzero/urns/application'
import {
generateTraceContextHeaders,
generateTraceSpan,
TraceSpan,
} from '@proofzero/platform-middleware/trace'

/**
Expand All @@ -22,6 +22,8 @@ interface CreateInnerContextOptions
StarbaseApp: DurableObjectNamespace
Edges: Fetcher
edges?: ReturnType<typeof createEdgesClient>
Email: Fetcher
email?: ReturnType<typeof createEmailClient>
accountURN?: AccountURN
ownAppURNs?: ApplicationURN[]
apiKey?: string
Expand All @@ -44,9 +46,14 @@ export async function createContextInner(opts: CreateInnerContextOptions) {
opts.Edges,
generateTraceContextHeaders(traceSpan)
)
const email = createEmailClient(
opts.Email,
generateTraceContextHeaders(traceSpan)
)
return {
...opts,
edges,
email,
traceSpan,
}
}
Expand Down
5 changes: 3 additions & 2 deletions platform/starbase/src/jsonrpc/methods/createCustomDomain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ export const createCustomDomain: CreateCustomDomainMethod = async ({
)
const customDomain: CustomDomain = {
...customHostname,
dns_records: getExpectedCustomDomainDNSRecords(
dns_records: await getExpectedCustomDomainDNSRecords(
customHostname.hostname,
input.passportHostname
input.passportHostname,
ctx
),
}
const node = await getApplicationNodeByClientId(clientId, ctx.StarbaseApp)
Expand Down
4 changes: 2 additions & 2 deletions platform/starbase/src/jsonrpc/methods/getAppPublicProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export const getAppPublicProps = async ({
customDomain?.status === 'active' &&
customDomain?.ssl.status === 'active' &&
Boolean(
customDomain?.dns_records?.every(
(r) => r.expected_value === r.value
customDomain?.dns_records?.every((r) =>
r.value?.includes(r.expected_value)
)
),
},
Expand Down
2 changes: 1 addition & 1 deletion platform/starbase/src/jsonrpc/validators/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export const CustomDomainDNSRecordsSchema = z.array(
name: z.string(),
record_type: z.union([z.literal('TXT'), z.literal('CNAME')]),
expected_value: z.string(),
value: z.string().optional().nullable(),
value: z.array(z.string()).optional(),
})
)

Expand Down
2 changes: 1 addition & 1 deletion platform/starbase/src/nodes/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ export default class StarbaseApp extends DOProxy {
if (
customDomain.status === 'active' &&
customDomain.ssl.status === 'active' &&
stored.dns_records.every((rec) => rec.value === rec.expected_value)
stored.dns_records.every((rec) => rec.value?.includes(rec.expected_value))
)
return this.unsetCustomDomainAlarm()

Expand Down
1 change: 1 addition & 0 deletions platform/starbase/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface Environment {
ServiceDeploymentMetadata: DeploymentMetadata
StarbaseApp: DurableObjectNamespace
Edges: Fetcher
Email: Fetcher
INTERNAL_PASSPORT_SERVICE_NAME: string
INTERNAL_CLOUDFLARE_ZONE_ID: string
TOKEN_CLOUDFLARE_API: string
Expand Down
39 changes: 35 additions & 4 deletions platform/starbase/src/utils/cloudflare.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { InternalServerError } from '@proofzero/errors'
import { CustomDomain, CustomDomainDNSRecords } from '../types'
import { Context } from '../jsonrpc/context'
import { parse } from 'tldts'

const API_URL = 'https://api.cloudflare.com/client/v4'

Expand Down Expand Up @@ -150,17 +152,46 @@ export const deleteWorkerRoute = async (
)
}

export const getExpectedCustomDomainDNSRecords = (
export const getExpectedCustomDomainDNSRecords = async (
customHostname: string,
passportUrl: string
): CustomDomainDNSRecords => {
passportUrl: string,
ctx: Context
): Promise<CustomDomainDNSRecords> => {
const emailDNSSecurityValues = await ctx.email.getDNSSecurityValues.query()
const parsedHostname = parse(customHostname)
const result: CustomDomainDNSRecords = []

//Add other expected DNS records here, eg. DMARC, SPF, DKIM, etc
result.push({
name: customHostname,
record_type: 'CNAME',
expected_value: passportUrl,
})

result.push({
record_type: 'TXT',
name: customHostname,
expected_value: `v=spf1 include:${emailDNSSecurityValues.spfHost} ~all`,
})

result.push({
record_type: 'TXT',
name: `${emailDNSSecurityValues.dkimSelector}._domainkey.${customHostname}`,
expected_value: `v=DKIM1; p=${emailDNSSecurityValues.dkimPublicKey}`,
})

result.push({
record_type: 'TXT',
name: `_dmarc.${parsedHostname.domain}`,
expected_value: `v=DMARC1; p=quarantine; sp=quarantine; rua=mailto:${emailDNSSecurityValues.dmarcEmail}`,
})

if (parsedHostname.subdomain) {
result.push({
record_type: 'TXT',
name: `_dmarc.${customHostname}`,
expected_value: `v=DMARC1; p=quarantine; rua=mailto:${emailDNSSecurityValues.dmarcEmail}`,
})
}

return result
}
Loading

0 comments on commit 8fa6523

Please sign in to comment.