Skip to content

Commit

Permalink
feat: add DNS-Map to show the global resolution of DNS requests (#101)
Browse files Browse the repository at this point in the history
* add DnsMap

* Use hyperscript for globe markers

* Improve globe DNS presentation

* resolve merge conflicts & update codebase

* update layout

* add ip-adress list

* optimize layout

* improve loading state

* optimize layout

* improve array sorting

* improve alert

* correct hotkey

Thanks Felix, great work!

Co-Authored-By: Felix Wotschofsky <[email protected]>
  • Loading branch information
maaaathis and wotschofsky authored Feb 8, 2024
1 parent 0253b98 commit 7f0bd7b
Show file tree
Hide file tree
Showing 10 changed files with 2,568 additions and 9 deletions.
22 changes: 22 additions & 0 deletions app/lookup/[domain]/map/_components/ResultsGlobe.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.wrapper :global(.scene-container),
.wrapper :global(.scene-container > canvas) {
width: 100% !important;
height: auto !important;
}

.wrapper :global(.scene-container > div:not([class])) {
width: 100% !important;
}

.wrapper :global(.scene-container > div:not([class]) .marker-wrapper) {
pointer-events: auto !important;
}

.wrapper :global(.scene-container > div:not([class]):hover .marker-wrapper) {
opacity: 0.6;
}

.wrapper :global(.scene-container > div:not([class]) .marker-wrapper:hover) {
opacity: 1 !important;
z-index: 100 !important;
}
158 changes: 158 additions & 0 deletions app/lookup/[domain]/map/_components/ResultsGlobe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
'use client';

import h from 'hyperscript';
import naturalCompare from 'natural-compare-lite';
import { useTheme } from 'next-themes';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { type FC, useEffect, useRef, useState } from 'react';

import { cn } from '@/lib/utils';

import styles from './ResultsGlobe.module.css';

const Globe = dynamic(() => import('react-globe.gl'), { ssr: false });

export const runtime = 'edge';

type ResultsGlobeProps = {
domain: string;
markers: {
code: string;
name: string;
lat: number;
lng: number;
results: {
A: string[];
AAAA: string[];
CNAME: string[];
};
}[];
};

const ResultsGlobe: FC<ResultsGlobeProps> = ({ domain, markers }) => {
const router = useRouter();
const { resolvedTheme } = useTheme();
const wrapperRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState<number | undefined>(undefined);

// Fix to avoid misalignment of labels after resize
useEffect(() => {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
setWidth(entry.contentRect.width);
}
});

if (wrapperRef.current) {
observer.observe(wrapperRef.current);
}

return () => observer.disconnect();
}, [wrapperRef]);

return (
<div ref={wrapperRef} className={cn(styles.wrapper, 'w-full')}>
<Globe
// Map from https://github.com/vasturiano/three-globe
globeImageUrl={
resolvedTheme === 'dark'
? '/assets/earth-night.jpg'
: '/assets/earth-day.jpg'
}
backgroundColor="rgba(0,0,0,0)"
width={width}
htmlElementsData={markers}
// @ts-expect-error
htmlElement={(d: ResultsGlobeProps['markers'][number]) => {
return h(
'div.marker-wrapper',

h(
'div.flex.flex-col.gap-2.rounded-lg.bg-background.p-2.shadow-md',

h('h3.text-sm.font-bold', d.name),
d.results.A.length
? h(
'div',
d.results.A.sort(naturalCompare)
.slice(0, d.results.A.length > 5 ? 4 : 5)
.map((value) =>
h('p.text-xs.text-muted-foreground', value)
),
d.results.A.length > 5
? h(
'p.text-xs.text-muted-foreground.italic',
`and ${d.results.A.length - 4} more`
)
: undefined
)
: undefined,

d.results.AAAA.length
? h(
'div',
d.results.AAAA.sort(naturalCompare)
.slice(0, d.results.AAAA.length > 5 ? 4 : 5)
.map((value) =>
h('p.text-xs.text-muted-foreground', value)
),
d.results.AAAA.length > 5
? h(
'p.text-xs.text-muted-foreground.italic',
`and ${d.results.AAAA.length - 4} more`
)
: undefined
)
: undefined,

d.results.CNAME.length
? h(
'div',
d.results.CNAME.sort(naturalCompare)
.slice(0, d.results.CNAME.length > 5 ? 4 : 5)
.map((value) =>
h('p.text-xs.text-muted-foreground', value)
),
d.results.CNAME.length > 5
? h(
'p.text-xs.text-muted-foreground.italic',
`and ${d.results.CNAME.length - 4} more`
)
: undefined
)
: undefined,

d.results.A.length === 0 &&
d.results.AAAA.length === 0 &&
d.results.CNAME.length === 0
? h(
'p.text-xs.text-muted-foreground.italic',
'No records found!'
)
: undefined,

h(
'p.text-xs.text-muted-foreground.italic',
h(
'a.underline.decoration-dotted',
{
href: `/lookup/${domain}/dns?resolver=cloudflare&location=${d.code}`,
onclick: (event: MouseEvent) => {
event.preventDefault();
const el = event.target as HTMLAnchorElement;
router.push(el.href);
},
},
'View full results'
)
)
)
);
}}
/>
</div>
);
};

export default ResultsGlobe;
24 changes: 24 additions & 0 deletions app/lookup/[domain]/map/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';

import { type FC, ReactElement, useEffect } from 'react';

type DnsMapErrorProps = {
error: Error & { digest?: string };
reset: () => void;
};

const DnsMapError: FC<DnsMapErrorProps> = ({ error }): ReactElement => {
useEffect(() => {
console.error(error);
}, [error]);

return (
<div className="mt-12 flex flex-col items-center gap-2">
<h2>Something went wrong!</h2>
<p className="mt-2 text-center text-sm text-muted-foreground">
Digest: {error.digest}
</p>
</div>
);
};
export default DnsMapError;
24 changes: 24 additions & 0 deletions app/lookup/[domain]/map/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';

import { FC, ReactElement } from 'react';

import { Skeleton } from '@/components/ui/skeleton';

const DnsMapLoading: FC = (): ReactElement => (
<div className="flex flex-col gap-8 lg:flex-row">
<div className="flex h-full basis-2/5">
<div className="my-auto grid grid-cols-2 gap-4">
{Array.from({ length: 18 }).map((_, i) => (
<Skeleton key={i} className="h-24 w-full rounded-xl" />
))}
</div>
</div>
<div className="order-first basis-3/5 lg:order-last">
<div className="mt-12 flex justify-center">
<Skeleton className="my-36 aspect-square w-3/5 rounded-full" />
</div>
</div>
</div>
);

export default DnsMapLoading;
147 changes: 147 additions & 0 deletions app/lookup/[domain]/map/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { isbot } from 'isbot';
import { CheckCircleIcon, InfoIcon, ShieldAlertIcon } from 'lucide-react';
import type { Metadata } from 'next';
import { headers } from 'next/headers';
import Link from 'next/link';
import type { FC } from 'react';

import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';

import ResultsGlobe from '@/app/lookup/[domain]/map/_components/ResultsGlobe';
import { REGIONS } from '@/lib/data';
import InternalDoHResolver from '@/lib/resolvers/InternalDoHResolver';

export const runtime = 'edge';

type MapResultsPageProps = {
params: {
domain: string;
};
};

export const generateMetadata = ({
params: { domain },
}: MapResultsPageProps): Metadata => ({
openGraph: {
url: `/lookup/${domain}/map`,
},
alternates: {
canonical: `/lookup/${domain}/map`,
},
});

const MapResultsPage: FC<MapResultsPageProps> = async ({
params: { domain },
}) => {
const userAgent = headers().get('user-agent');
const shouldBlockRequest = !userAgent || isbot(userAgent);

if (shouldBlockRequest) {
console.log('Bot detected, blocking request, UA:', userAgent);
return (
<Alert className="mx-auto mt-24 max-w-max">
<ShieldAlertIcon className="h-4 w-4" />
<AlertTitle>Bot or crawler detected!</AlertTitle>
<AlertDescription>
To protect our infrastructure, this page is not available for bots or
crawlers.
<br />
But don&apos;t be sad, there&apos;s to crawl here anyway.
</AlertDescription>
</Alert>
);
}

const markers = await Promise.all(
Object.entries(REGIONS).map(async ([code, data]) => {
const resolver = new InternalDoHResolver(code, 'cloudflare');
// TODO Optimize this to only required records
const results = await resolver.resolveAllRecords(domain);

return {
...data,
code,
results: {
A: results.A.map((r) => r.data),
AAAA: results.AAAA.map((r) => r.data),
CNAME: results.CNAME.map((r) => r.data),
},
};
})
);

const regionEntries = markers.map((marker) => (
<Link
href={`/lookup/${domain}/dns?resolver=cloudflare&location=${marker.code}`}
key={marker.code}
>
<Button variant="outline" className="flex h-full w-full flex-col">
<h3 className="mb-2 font-semibold">{marker.name}</h3>
<ul>
{marker.results.A.map((ip, index) => (
<li key={index} className="text-sm font-medium">
{ip}
</li>
))}
</ul>
</Button>
</Link>
));

const hasDifferentRecords = markers.some((marker, index) => {
if (index === 0) return false;

const previous = markers[index - 1].results;
const current = marker.results;

const compareRecords = (recordType: 'A' | 'AAAA' | 'CNAME') => {
return (
previous[recordType].length !== current[recordType].length ||
previous[recordType].some((ip, idx) => ip !== current[recordType][idx])
);
};

return (
compareRecords('A') || compareRecords('AAAA') || compareRecords('CNAME')
);
});

return (
<>
<Alert className="mx-auto my-6 max-w-max">
{hasDifferentRecords ? (
<>
<InfoIcon className="h-4 w-4" />
<AlertTitle>Different records detected!</AlertTitle>
<AlertDescription>
Not all regions have the same records for this domain. This{' '}
<i>could</i> be an indication for the use of GeoDNS.
<br /> Keep in mind however, that some providers rotate their IP
addresses, which can also lead to different results.
</AlertDescription>
</>
) : (
<>
<CheckCircleIcon className="h-4 w-4" />
<AlertTitle>All records are the same!</AlertTitle>
<AlertDescription>
All records are the same for all regions. Therefore propagation
was successful and the domain is not using GeoDNS.
</AlertDescription>
</>
)}
</Alert>
<div className="flex flex-col gap-8 lg:flex-row">
<div className="my-auto h-full basis-2/5">
<div className="grid w-full grid-cols-2 gap-4">{regionEntries}</div>
</div>
<div className="order-first basis-3/5 lg:order-last">
<ResultsGlobe domain={domain} markers={markers} />
</div>
</div>
</>
);
};

export default MapResultsPage;
Loading

1 comment on commit 7f0bd7b

@vercel
Copy link

@vercel vercel bot commented on 7f0bd7b Feb 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

digga – ./

digger.vercel.app
digga-git-main-maathis.vercel.app
digga-maathis.vercel.app
digga.dev

Please sign in to comment.