Skip to content

Commit d184c16

Browse files
committed
fix: improve www detection
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 39fad62 commit d184c16

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)