Files
web-check-2/src/pages/Results.tsx
2023-06-29 23:40:51 +01:00

320 lines
12 KiB
TypeScript

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<AddressType>('empt');
const { address } = useParams();
const [ loadingJobs, setLoadingJobs ] = useState<LoadingJob[]>(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<ServerLocation>({
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<ShodanResults>({
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<Whois>({
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<TechnologyGroup[]>({
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 (
<ResultsOuter>
<Header as="header">
<Heading color={colors.primary} size="large">
<img width="64" src="/web-check.png" alt="Web Check Icon" />
<a href="/">Web Check</a>
</Heading>
{ address &&
<Heading color={colors.textColor} size="medium">
{ addressType === 'url' && <img width="32px" src={`https://icon.horse/icon/${makeSiteName(address)}`} alt="" /> }
{makeSiteName(address)}
</Heading>
}
</Header>
<ProgressBar loadStatus={loadingJobs} />
<ResultsContent>
{
resultCardData.map(({ title, result, Component }) => (
(result) ? (
<ErrorBoundary title={title} key={title}>
<Component {...result} />
</ErrorBoundary>
) : <></>
))
}
</ResultsContent>
<Footer />
</ResultsOuter>
);
}
export default Results;