import { useState, useEffect, useCallback } from 'react'; import { useParams } from "react-router-dom"; import styled from 'styled-components'; import colors from 'styles/colors'; import Heading from 'components/Form/Heading'; import Card from 'components/Form/Card'; import ErrorBoundary from 'components/misc/ErrorBoundary'; import Footer from 'components/misc/Footer'; import { RowProps } from 'components/Form/Row'; import ServerLocationCard from 'components/Results/ServerLocation'; import ServerInfoCard from 'components/Results/ServerInfo'; import HostNamesCard from 'components/Results/HostNames'; import WhoIsCard from 'components/Results/WhoIs'; import BuiltWithCard from 'components/Results/BuiltWith'; import LighthouseCard from 'components/Results/Lighthouse'; import ScreenshotCard from 'components/Results/Screenshot'; import SslCertCard from 'components/Results/SslCert'; import HeadersCard from 'components/Results/Headers'; import CookiesCard from 'components/Results/Cookies'; import RobotsTxtCard from 'components/Results/RobotsTxt'; import DnsRecordsCard from 'components/Results/DnsRecords'; import RedirectsCard from 'components/Results/Redirects'; import TxtRecordCard from 'components/Results/TxtRecords'; import ServerStatusCard from 'components/Results/ServerStatus'; import ProgressBar, { LoadingJob, LoadingState, initialJobs } from 'components/misc/ProgressBar'; import keys from 'utils/get-keys'; import { determineAddressType, AddressType } from 'utils/address-type-checker'; import useMotherHook from 'hooks/motherOfAllHooks'; import { getLocation, ServerLocation, makeTechnologies, TechnologyGroup, parseCookies, Cookie, parseRobotsTxt, applyWhoIsResults, Whois, parseShodanResults, ShodanResults } from 'utils/result-processor'; const ResultsOuter = styled.div` display: flex; flex-direction: column; `; const ResultsContent = styled.section` width: 95vw; display: grid; grid-auto-flow: dense; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1rem; margin: auto; width: calc(100% - 2rem); padding-bottom: 1rem; `; const Header = styled(Card)` margin: 1rem; display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; padding: 0.5rem 1rem; `; const Results = (): JSX.Element => { const startTime = new Date().getTime(); const [ addressType, setAddressType ] = useState('empt'); const { address } = useParams(); const [ loadingJobs, setLoadingJobs ] = useState(initialJobs); const updateLoadingJobs = useCallback((job: string, newState: LoadingState, error?: string) => { const timeTaken = new Date().getTime() - startTime; setLoadingJobs((prevJobs) => { const newJobs = prevJobs.map((loadingJob: LoadingJob) => { if (loadingJob.name === job) { return { ...loadingJob, error, state: newState, timeTaken }; } return loadingJob; }); if (newState === 'success') { console.log( `%cFetch Success - ${job}%c\n\nThe ${job} job succeeded in ${timeTaken}ms`, `background: ${colors.success}; color: ${colors.background}; padding: 4px 8px; font-size: 16px;`, `color: ${colors.success};`, ); } if (newState === 'error') { console.log( `%cFetch Error - ${job}%c\n\nThe ${job} job failed with the following error:%c\n${error}`, `background: ${colors.danger}; padding: 4px 8px; font-size: 16px;`, `color: ${colors.danger};`, `color: ${colors.warning};`, ); } return newJobs; }); }, []); useEffect(() => { setAddressType(determineAddressType(address || '')); if (addressType === 'ipV4' && address) { setIpAddress(address); } }, []); const urlTypeOnly = ['url'] as AddressType[]; // Many jobs only run with these address types // Fetch and parse IP address for given URL const [ipAddress, setIpAddress] = useMotherHook({ jobId: 'get-ip', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`http://localhost:8888/.netlify/functions/find-url-ip?address=${address}`) .then(res => res.json()) .then(res => res.ip), }); // Fetch and parse SSL certificate info const [sslResults] = useMotherHook({ jobId: 'ssl', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`/ssl-check?url=${address}`).then((res) => res.json()), }); // Fetch and parse cookies info const [cookieResults] = useMotherHook<{cookies: Cookie[]}>({ jobId: 'cookies', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`/get-cookies?url=${address}`) .then(res => res.json()) .then(res => parseCookies(res.cookies)), }); // Fetch and parse crawl rules from robots.txt const [robotsTxtResults] = useMotherHook<{robots: RowProps[]}>({ jobId: 'robots-txt', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`/read-robots-txt?url=${address}`) .then(res => res.text()) .then(res => parseRobotsTxt(res)), }); // Fetch and parse headers const [headersResults] = useMotherHook({ jobId: 'headers', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`/get-headers?url=${address}`).then(res => res.json()), }); // Fetch and parse DNS records const [dnsResults] = useMotherHook({ jobId: 'dns', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`/get-dns?url=${address}`).then(res => res.json()), }); // Fetch and parse Lighthouse performance data const [lighthouseResults] = useMotherHook({ jobId: 'lighthouse', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`/lighthouse-report?url=${address}`) .then(res => res.json()) .then(res => res.lighthouseResult), }); // Get IP address location info const [locationResults] = useMotherHook({ jobId: 'location', updateLoadingJobs, addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, fetchRequest: () => fetch(`https://ipapi.co/${ipAddress}/json/`) .then(res => res.json()) .then(res => getLocation(res)), }); // Get hostnames and associated domains from Shodan const [shoadnResults] = useMotherHook({ jobId: 'shodan', updateLoadingJobs, addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, fetchRequest: () => fetch(`https://api.shodan.io/shodan/host/${ipAddress}?key=${keys.shodan}`) .then(res => res.json()) .then(res => parseShodanResults(res)), }); // Fetch and parse domain whois results const [whoIsResults] = useMotherHook({ jobId: 'whois', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`https://api.whoapi.com/?domain=${address}&r=whois&apikey=${keys.whoApi}`) .then(res => res.json()) .then(res => applyWhoIsResults(res)), }); // Fetch and parse built-with results const [technologyResults] = useMotherHook({ jobId: 'built-with', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`https://api.builtwith.com/v21/api.json?KEY=${keys.builtWith}&LOOKUP=${address}`) .then(res => res.json()) .then(res => makeTechnologies(res)), }); // Fetches DNS TXT records const [txtRecordResults] = useMotherHook({ jobId: 'txt-records', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`/get-txt?url=${address}`).then(res => res.json()), }); // Fetches URL redirects const [redirectResults] = useMotherHook({ jobId: 'redirects', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`/follow-redirects?url=${address}`).then(res => res.json()), }); // Get current status and response time of server const [serverStatusResults] = useMotherHook({ jobId: 'status', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`/server-status?url=${address}`).then(res => res.json()), }); /* Cancel remaining jobs after 10 second timeout */ useEffect(() => { const checkJobs = () => { loadingJobs.forEach(job => { if (job.state === 'loading') { updateLoadingJobs(job.name, 'timed-out'); } }); }; const timeoutId = setTimeout(checkJobs, 10000); return () => { clearTimeout(timeoutId); }; }, [loadingJobs, updateLoadingJobs]); // dependencies for the effect const makeSiteName = (address: string): string => { try { return new URL(address).hostname.replace('www.', ''); } catch (error) { return address; } } // A list of state sata, corresponding component and title for each card const resultCardData = [ { title: 'Server Location', result: locationResults, Component: ServerLocationCard }, { title: 'SSL Info', result: sslResults, Component: SslCertCard }, { title: 'Headers', result: headersResults, Component: HeadersCard }, { title: 'Host Names', result: shoadnResults?.hostnames, Component: HostNamesCard }, { title: 'Domain Info', result: whoIsResults, Component: WhoIsCard }, { title: 'DNS Records', result: dnsResults, Component: DnsRecordsCard }, { title: 'Performance', result: lighthouseResults, Component: LighthouseCard }, { title: 'Cookies', result: cookieResults, Component: CookiesCard }, { title: 'Screenshot', result: lighthouseResults?.fullPageScreenshot?.screenshot, Component: ScreenshotCard }, { title: 'Technologies', result: technologyResults, Component: BuiltWithCard }, { title: 'Crawl Rules', result: robotsTxtResults, Component: RobotsTxtCard }, { title: 'Server Info', result: shoadnResults?.serverInfo, Component: ServerInfoCard }, { title: 'Redirects', result: redirectResults, Component: RedirectsCard }, { title: 'TXT Records', result: txtRecordResults, Component: TxtRecordCard }, { title: 'Server Status', result: serverStatusResults, Component: ServerStatusCard }, ]; return (
Web Check Icon Web Check { address && { addressType === 'url' && } {makeSiteName(address)} }
{ resultCardData.map(({ title, result, Component }) => ( (result) ? ( ) : <> )) }