-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add DNS-Map to show the global resolution of DNS requests (#101)
* 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
1 parent
0253b98
commit 7f0bd7b
Showing
10 changed files
with
2,568 additions
and
9 deletions.
There are no files selected for viewing
22 changes: 22 additions & 0 deletions
22
app/lookup/[domain]/map/_components/ResultsGlobe.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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't be sad, there'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; |
Oops, something went wrong.
7f0bd7b
There was a problem hiding this comment.
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