Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/builder/app/builder/features/topbar/add-domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const AddDomain = ({
});
// enforce www subdomain when no support for cname flattening
// and root cname can conflict with MX or NS
if (!registrar.cnameFlattening) {
if (registrar.known && !registrar.cnameFlattening) {
domain = `www.${domain}`;
}
}
Expand Down
6 changes: 0 additions & 6 deletions apps/builder/app/shared/help.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ import {
} from "@webstudio-is/icons";

export const help = [
{
label: "Support hub",
url: "https://help.webstudio.is/",
icon: <LifeBuoyIcon />,
target: "embed",
},
{
label: "Video tutorials",
url: "https://wstd.us/101",
Expand Down
104 changes: 104 additions & 0 deletions packages/domain/src/rdap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
interface DNSList {
description: string;
publication: string;
services: [string[], string[]][];
version: string;
}

// Cache map to store TLD (top level domain) → RDAP server URL mappings
const dnsCache = new Map<string, string>();

/**
* Fetch the IANA DNS RDAP bootstrap JSON.
*/
const fetchDnsList = async (): Promise<undefined | DNSList> => {
try {
const response = await fetch("https://data.iana.org/rdap/dns.json", {
headers: {
accept: "application/json,application/rdap+json",
},
});
if (response.ok) {
return response.json();
}
} catch {
// empty block
}
};

/**
* Find the RDAP server URL for a given top-level domain (TLD).
*/
const findRdapServer = async (topLevelDomain: string) => {
if (dnsCache.size === 0) {
const dns = await fetchDnsList();
if (dns) {
for (const [domains, [server]] of dns.services) {
for (const domain of domains) {
dnsCache.set(domain, server);
}
}
}
}
return dnsCache.get(topLevelDomain);
};

/**
* Extract the top-level domain from a full domain string.
* Unicode is converted to ascii
*/
const getTopLevelDomain = (domain: string) => {
try {
return new URL(`https://${domain}`).hostname.split(".").at(-1);
} catch {
// invalid domain
}
};

const fetchRdap = async (
rdapServer: string,
domain: string
): Promise<undefined | string> => {
try {
const response = await fetch(`${rdapServer}domain/${domain}`, {
headers: {
accept: "application/json,application/rdap+json",
},
});
if (response.ok) {
return response.text();
}
} catch {
// empty block
}
};

/**
* Determine whether a domain is using Cloudflare nameservers.
* 1. Parse TLD from domain.
* 2. Lookup RDAP server for that TLD.
* 3. Fetch RDAP data for the domain.
* 4. Search the raw response for ".ns.cloudflare.com".
*/
export const isDomainUsingCloudflareNameservers = async (domain: string) => {
const topLevelDomain = getTopLevelDomain(domain);
if (!topLevelDomain) {
throw new Error("Could not parse the top level domain.");
}

const rdapServer = await findRdapServer(topLevelDomain);
if (!rdapServer) {
console.error(
"RDAP Server for the given top level domain could not be found."
);
return undefined;
}

const data = await fetchRdap(rdapServer, domain);
if (data) {
// detect by nameservers rather than registrar url
// sometimes stored as *.NS.CLOUDFLARE.COM
return data.toLowerCase().includes(".ns.cloudflare.com");
}
return false;
};
27 changes: 6 additions & 21 deletions packages/domain/src/trpc/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import { createProductionBuild } from "@webstudio-is/project-build/index.server"
import { router, procedure } from "@webstudio-is/trpc-interface/index.server";
import { Templates } from "@webstudio-is/sdk";
import { db } from "../db";

const rdap = [
"https://rdap.cloudflare.com/rdap/v1/domain/",
"https://rdap.namecheap.com/domain/",
];
import { isDomainUsingCloudflareNameservers } from "../rdap";

export const domainRouter = router({
getEntriToken: procedure.query(async ({ ctx }) => {
Expand All @@ -32,23 +28,12 @@ export const domainRouter = router({
findDomainRegistrar: procedure
.input(z.object({ domain: z.string() }))
.query(async ({ input }) => {
try {
for (const rdapEndpoint of rdap) {
const response = await fetch(`${rdapEndpoint}${input.domain}`);
if (response.ok) {
const data = await response.text();
return {
// detect by nameservers rather than registrar url
cnameFlattening: data.includes(".ns.cloudflare.com"),
};
}
}
} catch {
// empty block
}
const isCloudflare = await isDomainUsingCloudflareNameservers(
input.domain
);
return {
name: "other",
cnameFlattening: false,
known: isCloudflare !== undefined,
cnameFlattening: isCloudflare === true,
};
}),

Expand Down
Loading