Skip to content

Commit 4c2e7cd

Browse files
authored
fix: improve www detection (#5314)
We detect cloudflare to preserve root domain and add www for everything else. Now we check official dns registry for top level domains and check its data for required nameservers.
1 parent 03c7366 commit 4c2e7cd

File tree

2 files changed

+107
-21
lines changed

2 files changed

+107
-21
lines changed

packages/domain/src/rdap.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
interface DNSList {
2+
description: string;
3+
publication: string;
4+
services: [string[], string[]][];
5+
version: string;
6+
}
7+
8+
// Cache map to store TLD (top level domain) → RDAP server URL mappings
9+
const dnsCache = new Map<string, string>();
10+
11+
/**
12+
* Fetch the IANA DNS RDAP bootstrap JSON.
13+
*/
14+
const fetchDnsList = async (): Promise<undefined | DNSList> => {
15+
try {
16+
const response = await fetch("https://data.iana.org/rdap/dns.json", {
17+
headers: {
18+
accept: "application/json,application/rdap+json",
19+
},
20+
});
21+
if (response.ok) {
22+
return response.json();
23+
}
24+
} catch {
25+
// empty block
26+
}
27+
};
28+
29+
/**
30+
* Find the RDAP server URL for a given top-level domain (TLD).
31+
*/
32+
const findRdapServer = async (topLevelDomain: string) => {
33+
if (dnsCache.size === 0) {
34+
const dns = await fetchDnsList();
35+
if (dns) {
36+
for (const [domains, [server]] of dns.services) {
37+
for (const domain of domains) {
38+
dnsCache.set(domain, server);
39+
}
40+
}
41+
}
42+
}
43+
return dnsCache.get(topLevelDomain);
44+
};
45+
46+
/**
47+
* Extract the top-level domain from a full domain string.
48+
* Unicode is converted to ascii
49+
*/
50+
const getTopLevelDomain = (domain: string) => {
51+
try {
52+
return new URL(`https://${domain}`).hostname.split(".").at(-1);
53+
} catch {
54+
// invalid domain
55+
}
56+
};
57+
58+
const fetchRdap = async (
59+
rdapServer: string,
60+
domain: string
61+
): Promise<undefined | string> => {
62+
try {
63+
const response = await fetch(`${rdapServer}domain/${domain}`, {
64+
headers: {
65+
accept: "application/json,application/rdap+json",
66+
},
67+
});
68+
if (response.ok) {
69+
return response.text();
70+
}
71+
} catch {
72+
// empty block
73+
}
74+
};
75+
76+
/**
77+
* Determine whether a domain is using Cloudflare nameservers.
78+
* 1. Parse TLD from domain.
79+
* 2. Lookup RDAP server for that TLD.
80+
* 3. Fetch RDAP data for the domain.
81+
* 4. Search the raw response for ".ns.cloudflare.com".
82+
*/
83+
export const isDomainUsingCloudflareNameservers = async (domain: string) => {
84+
const topLevelDomain = getTopLevelDomain(domain);
85+
if (!topLevelDomain) {
86+
throw new Error("Could not parse the top level domain.");
87+
}
88+
89+
const rdapServer = await findRdapServer(topLevelDomain);
90+
if (!rdapServer) {
91+
throw new Error(
92+
"RDAP Server for the given top level domain could not be found."
93+
);
94+
}
95+
96+
if (rdapServer) {
97+
const data = await fetchRdap(rdapServer, domain);
98+
if (data) {
99+
// detect by nameservers rather than registrar url
100+
// sometimes stored as *.NS.CLOUDFLARE.COM
101+
return data.toLowerCase().includes(".ns.cloudflare.com");
102+
}
103+
}
104+
return false;
105+
};

packages/domain/src/trpc/domain.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ import { createProductionBuild } from "@webstudio-is/project-build/index.server"
55
import { router, procedure } from "@webstudio-is/trpc-interface/index.server";
66
import { Templates } from "@webstudio-is/sdk";
77
import { db } from "../db";
8-
9-
const rdap = [
10-
"https://rdap.cloudflare.com/rdap/v1/domain/",
11-
"https://rdap.namecheap.com/domain/",
12-
];
8+
import { isDomainUsingCloudflareNameservers } from "../rdap";
139

1410
export const domainRouter = router({
1511
getEntriToken: procedure.query(async ({ ctx }) => {
@@ -32,23 +28,8 @@ export const domainRouter = router({
3228
findDomainRegistrar: procedure
3329
.input(z.object({ domain: z.string() }))
3430
.query(async ({ input }) => {
35-
try {
36-
for (const rdapEndpoint of rdap) {
37-
const response = await fetch(`${rdapEndpoint}${input.domain}`);
38-
if (response.ok) {
39-
const data = await response.text();
40-
return {
41-
// detect by nameservers rather than registrar url
42-
cnameFlattening: data.includes(".ns.cloudflare.com"),
43-
};
44-
}
45-
}
46-
} catch {
47-
// empty block
48-
}
4931
return {
50-
name: "other",
51-
cnameFlattening: false,
32+
cnameFlattening: await isDomainUsingCloudflareNameservers(input.domain),
5233
};
5334
}),
5435

0 commit comments

Comments
 (0)