320 lines
12 KiB
TypeScript
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;
|