From 83272e4536cce4b9e97b749edd26154f8e24e409 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 7 Jul 2023 21:00:22 +0100 Subject: [PATCH] Brings all the changes together in results page --- src/hooks/motherOfAllHooks.ts | 90 ++++++++++---- src/pages/Results.tsx | 225 +++++++++++++++++++++++++--------- 2 files changed, 232 insertions(+), 83 deletions(-) diff --git a/src/hooks/motherOfAllHooks.ts b/src/hooks/motherOfAllHooks.ts index 1cbfba0..25a8cb5 100644 --- a/src/hooks/motherOfAllHooks.ts +++ b/src/hooks/motherOfAllHooks.ts @@ -1,26 +1,30 @@ import { useState, useEffect } from 'react'; +import { toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + import { LoadingState } from 'components/misc/ProgressBar'; import { AddressType } from 'utils/address-type-checker'; -type UpdateLoadingJobsFunction = (job: string, newState: LoadingState, error?: string) => void; - -interface AddressInfo { - address: string | undefined; - addressType: AddressType; - expectedAddressTypes: AddressType[]; -} - interface UseIpAddressProps { - addressInfo: AddressInfo; - updateLoadingJobs: UpdateLoadingJobsFunction; + // Unique identifier for this job type jobId: string; - fetchRequest: () => Promise; + // The actual fetch request + fetchRequest: () => Promise; + // Function to call to update the loading state in parent + updateLoadingJobs: (job: string, newState: LoadingState, error?: string, retry?: (data?: any) => void | null, data?: any) => void; + addressInfo: { + // The hostname/ip address that we're checking + address: string | undefined; + // The type of address (e.g. url, ipv4) + addressType: AddressType; + // The valid address types for this job + expectedAddressTypes: AddressType[]; + }; } type ResultType = any; -type ReturnType = [ResultType | undefined, React.Dispatch>]; - +type ReturnType = [ResultType | undefined, (data?: any) => void]; const useMotherOfAllHooks = (params: UseIpAddressProps): ReturnType => { // Destructure params @@ -30,6 +34,47 @@ const useMotherOfAllHooks = (params: UseIpAddressProps(); + // Fire off the HTTP fetch request, then set results and update loading / error state + const doTheFetch = () => { + return fetchRequest() + .then((res: any) => { + if (!res) { + updateLoadingJobs(jobId, 'error', res.error, reset); + throw new Error('No response'); + } + if (res.error) { + updateLoadingJobs(jobId, 'error', res.error, reset); + throw new Error(res.error); + } + // All went to plan, set results and mark as done + setResult(res); + updateLoadingJobs(jobId, 'success', '', undefined, res); + }) + .catch((err) => { + // Something fucked up, log the error + updateLoadingJobs(jobId, 'error', err.message, reset); + throw err; + }) + } + + // For when the user manually re-triggers the job + const reset = (data: any) => { + // If data is provided, then update state + if (data && !(data instanceof Event) && !data?._reactName) { + setResult(data); + } else { // Otherwise, trigger a data re-fetch + updateLoadingJobs(jobId, 'loading'); + const fetchyFetch = doTheFetch(); + const toastOptions = { + pending: `Updating Data (${jobId})`, + success: `Completed (${jobId})`, + error: `Failed to update (${jobId})`, + }; + // Initiate fetch, and show progress toast + toast.promise(fetchyFetch, toastOptions).catch(() => {}); + } + }; + useEffect(() => { // Still waiting for this upstream, cancel job if (!address || !addressType) { @@ -37,26 +82,17 @@ const useMotherOfAllHooks = (params: UseIpAddressProps { - // All went to plan, set results and mark as done - setResult(res); - updateLoadingJobs(jobId, 'success'); - }) - .catch((err) => { - // Something fucked up, log the error - updateLoadingJobs(jobId, 'error', err.message); - }); - + // Initiate the data fetching process + doTheFetch().catch(() => {}); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [address, addressType]); - return [result, setResult]; + return [result, reset]; }; export default useMotherOfAllHooks; diff --git a/src/pages/Results.tsx b/src/pages/Results.tsx index 88cfa4c..46da0cb 100644 --- a/src/pages/Results.tsx +++ b/src/pages/Results.tsx @@ -1,13 +1,15 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, ReactNode } from 'react'; import { useParams } from "react-router-dom"; import styled from 'styled-components'; +import { ToastContainer } from 'react-toastify'; import colors from 'styles/colors'; import Heading from 'components/Form/Heading'; import Card from 'components/Form/Card'; -import ErrorBoundary from 'components/misc/ErrorBoundary'; +import Modal from 'components/Form/Modal'; import Footer from 'components/misc/Footer'; import { RowProps } from 'components/Form/Row'; +import docs from 'utils/docs'; import ServerLocationCard from 'components/Results/ServerLocation'; @@ -27,8 +29,12 @@ import TxtRecordCard from 'components/Results/TxtRecords'; import ServerStatusCard from 'components/Results/ServerStatus'; import OpenPortsCard from 'components/Results/OpenPorts'; import TraceRouteCard from 'components/Results/TraceRoute'; +import CarbonFootprintCard from 'components/Results/CarbonFootprint'; +import SiteFeaturesCard from 'components/Results/SiteFeatures'; +import DnsSecCard from 'components/Results/DnsSec'; import ProgressBar, { LoadingJob, LoadingState, initialJobs } from 'components/misc/ProgressBar'; +import ActionButtons from 'components/misc/ActionButtons'; import keys from 'utils/get-keys'; import { determineAddressType, AddressType } from 'utils/address-type-checker'; @@ -69,6 +75,24 @@ const Header = styled(Card)` padding: 0.5rem 1rem; `; +const JobDocsContainer = styled.div` +p.doc-desc, p.doc-uses, ul { + margin: 0.25rem auto 1.5rem auto; +} +ul { + padding: 0 0.5rem 0 1rem; +} +ul li a { + color: ${colors.primary}; +} +h4 { + border-top: 1px solid ${colors.primary}; + color: ${colors.primary}; + opacity: 0.75; + padding: 0.5rem 0; +} +`; + const Results = (): JSX.Element => { const startTime = new Date().getTime(); @@ -77,28 +101,44 @@ const Results = (): JSX.Element => { const [ loadingJobs, setLoadingJobs ] = useState(initialJobs); - const updateLoadingJobs = useCallback((job: string, newState: LoadingState, error?: string) => { - const timeTaken = new Date().getTime() - startTime; + const [modalOpen, setModalOpen] = useState(false); + const [modalContent, setModalContent] = useState(<>); + + const updateLoadingJobs = useCallback((job: string, newState: LoadingState, error?: string, retry?: () => void, data?: any) => { + const now = new Date(); + const timeTaken = now.getTime() - startTime; setLoadingJobs((prevJobs) => { const newJobs = prevJobs.map((loadingJob: LoadingJob) => { if (loadingJob.name === job) { - return { ...loadingJob, error, state: newState, timeTaken }; + return { ...loadingJob, error, state: newState, timeTaken, retry }; } return loadingJob; }); + const timeString = `[${now.getHours().toString().padStart(2, '0')}:` + +`${now.getMinutes().toString().padStart(2, '0')}:` + + `${now.getSeconds().toString().padStart(2, '0')}]`; + + 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;`, + `%cFetch Success - ${job}%c\n\n${timeString}%c The ${job} job succeeded in ${timeTaken}ms` + + `\n%cRun %cwindow.webCheck['${job}']%c to inspect the raw the results`, + `background:${colors.success};color:${colors.background};padding: 4px 8px;font-size:16px;`, + `font-weight: bold; color: ${colors.success};`, `color: ${colors.success};`, + `color: #1d8242;`,`color: #1d8242;text-decoration:underline;`,`color: #1d8242;`, ); + if (!(window as any).webCheck) (window as any).webCheck = {}; + if (data) (window as any).webCheck[job] = data; } if (newState === 'error') { console.log( - `%cFetch Error - ${job}%c\n\nThe ${job} job failed with the following error:%c\n${error}`, + `%cFetch Error - ${job}%c\n\n${timeString}%c The ${job} job failed ` + +`after ${timeTaken}ms, with the following error:%c\n${error}`, `background: ${colors.danger}; padding: 4px 8px; font-size: 16px;`, + `font-weight: bold; color: ${colors.danger};`, `color: ${colors.danger};`, `color: ${colors.warning};`, ); @@ -108,11 +148,13 @@ const Results = (): JSX.Element => { }, []); useEffect(() => { - setAddressType(determineAddressType(address || '')); + if (!addressType || addressType === 'empt') { + setAddressType(determineAddressType(address || '')); + } if (addressType === 'ipV4' && address) { setIpAddress(address); } - }, []); + }, []); const urlTypeOnly = ['url'] as AddressType[]; // Many jobs only run with these address types @@ -127,7 +169,7 @@ const Results = (): JSX.Element => { }); // Fetch and parse SSL certificate info - const [sslResults] = useMotherHook({ + const [sslResults, updateSslResults] = useMotherHook({ jobId: 'ssl', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, @@ -135,7 +177,7 @@ const Results = (): JSX.Element => { }); // Fetch and parse cookies info - const [cookieResults] = useMotherHook<{cookies: Cookie[]}>({ + const [cookieResults, updateCookieResults] = useMotherHook<{cookies: Cookie[]}>({ jobId: 'cookies', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, @@ -145,7 +187,7 @@ const Results = (): JSX.Element => { }); // Fetch and parse crawl rules from robots.txt - const [robotsTxtResults] = useMotherHook<{robots: RowProps[]}>({ + const [robotsTxtResults, updateRobotsTxtResults] = useMotherHook<{robots: RowProps[]}>({ jobId: 'robots-txt', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, @@ -155,7 +197,7 @@ const Results = (): JSX.Element => { }); // Fetch and parse headers - const [headersResults] = useMotherHook({ + const [headersResults, updateHeadersResults] = useMotherHook({ jobId: 'headers', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, @@ -163,7 +205,7 @@ const Results = (): JSX.Element => { }); // Fetch and parse DNS records - const [dnsResults] = useMotherHook({ + const [dnsResults, updateDnsResults] = useMotherHook({ jobId: 'dns', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, @@ -171,7 +213,7 @@ const Results = (): JSX.Element => { }); // Fetch and parse Lighthouse performance data - const [lighthouseResults] = useMotherHook({ + const [lighthouseResults, updateLighthouseResults] = useMotherHook({ jobId: 'lighthouse', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, @@ -181,7 +223,7 @@ const Results = (): JSX.Element => { }); // Get IP address location info - const [locationResults] = useMotherHook({ + const [locationResults, updateLocationResults] = useMotherHook({ jobId: 'location', updateLoadingJobs, addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, @@ -192,8 +234,8 @@ const Results = (): JSX.Element => { // Get hostnames and associated domains from Shodan - const [shoadnResults] = useMotherHook({ - jobId: 'shodan', + const [shoadnResults, updateShodanResults] = useMotherHook({ + jobId: 'hosts', updateLoadingJobs, addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, fetchRequest: () => fetch(`https://api.shodan.io/shodan/host/${ipAddress}?key=${keys.shodan}`) @@ -203,7 +245,7 @@ const Results = (): JSX.Element => { // Check for open ports - const [portsResults] = useMotherHook({ + const [portsResults, updatePortsResults] = useMotherHook({ jobId: 'ports', updateLoadingJobs, addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, @@ -212,7 +254,7 @@ const Results = (): JSX.Element => { }); // Fetch and parse domain whois results - const [whoIsResults] = useMotherHook({ + const [whoIsResults, updateWhoIsResults] = useMotherHook({ jobId: 'whois', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, @@ -222,17 +264,17 @@ const Results = (): JSX.Element => { }); // 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)), - }); + // const [technologyResults, updateTechnologyResults] = 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({ + const [txtRecordResults, updateTxtRecordResults] = useMotherHook({ jobId: 'txt-records', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, @@ -240,7 +282,7 @@ const Results = (): JSX.Element => { }); // Fetches URL redirects - const [redirectResults] = useMotherHook({ + const [redirectResults, updateRedirectResults] = useMotherHook({ jobId: 'redirects', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, @@ -248,7 +290,7 @@ const Results = (): JSX.Element => { }); // Get current status and response time of server - const [serverStatusResults] = useMotherHook({ + const [serverStatusResults, updateServerStatusResults] = useMotherHook({ jobId: 'status', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, @@ -256,13 +298,37 @@ const Results = (): JSX.Element => { }); // Get trace route for a given hostname - const [traceRouteResults] = useMotherHook({ + const [traceRouteResults, updateTraceRouteResults] = useMotherHook({ jobId: 'trace-route', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`/trace-route?url=${address}`).then(res => res.json()), }); + // Fetch carbon footprint data for a given site + const [carbonResults, updateCarbonResults] = useMotherHook({ + jobId: 'carbon', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`/get-carbon?url=${address}`).then(res => res.json()), + }); + + // Get site features from BuiltWith + const [siteFeaturesResults, updateSiteFeaturesResults] = useMotherHook({ + jobId: 'features', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`/site-features?url=${address}`).then(res => res.json()), + }); + + // Get DNSSEC info + const [dnsSecResults, updateDnsSecResults] = useMotherHook({ + jobId: 'dnssec', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`/dns-sec?url=${address}`).then(res => res.json()), + }); + /* Cancel remaining jobs after 10 second timeout */ useEffect(() => { const checkJobs = () => { @@ -288,24 +354,66 @@ const Results = (): JSX.Element => { // 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: 'Trace Route', result: traceRouteResults, Component: TraceRouteCard }, - { 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 }, - { title: 'Open Ports', result: portsResults, Component: OpenPortsCard }, + { id: 'location', title: 'Server Location', result: locationResults, Component: ServerLocationCard, refresh: updateLocationResults }, + { id: 'ssl', title: 'SSL Info', result: sslResults, Component: SslCertCard, refresh: updateSslResults }, + { id: 'dns', title: 'Headers', result: headersResults, Component: HeadersCard, refresh: updateHeadersResults }, + { id: 'hosts', title: 'Host Names', result: shoadnResults?.hostnames, Component: HostNamesCard, refresh: updateShodanResults }, + { id: 'whois', title: 'Domain Info', result: whoIsResults, Component: WhoIsCard, refresh: updateWhoIsResults }, + { id: 'dns', title: 'DNS Records', result: dnsResults, Component: DnsRecordsCard, refresh: updateDnsResults }, + { id: 'lighthouse', title: 'Performance', result: lighthouseResults, Component: LighthouseCard, refresh: updateLighthouseResults }, + { id: 'cookies', title: 'Cookies', result: cookieResults, Component: CookiesCard, refresh: updateCookieResults }, + { id: 'trace-route', title: 'Trace Route', result: traceRouteResults, Component: TraceRouteCard, refresh: updateTraceRouteResults }, + { id: '', title: 'Screenshot', result: lighthouseResults?.fullPageScreenshot?.screenshot, Component: ScreenshotCard, refresh: updateLighthouseResults }, + // { title: 'Technologies', result: technologyResults, Component: BuiltWithCard, refresh: updateTechnologyResults }, + { id: 'robots-txt', title: 'Crawl Rules', result: robotsTxtResults, Component: RobotsTxtCard, refresh: updateRobotsTxtResults }, + { id: 'server-info', title: 'Server Info', result: shoadnResults?.serverInfo, Component: ServerInfoCard, refresh: updateShodanResults }, + { id: 'redirects', title: 'Redirects', result: redirectResults, Component: RedirectsCard, refresh: updateRedirectResults }, + { id: 'txt-records', title: 'TXT Records', result: txtRecordResults, Component: TxtRecordCard, refresh: updateTxtRecordResults }, + { id: 'status', title: 'Server Status', result: serverStatusResults, Component: ServerStatusCard, refresh: updateServerStatusResults }, + { id: 'ports', title: 'Open Ports', result: portsResults, Component: OpenPortsCard, refresh: updatePortsResults }, + { id: 'carbon', title: 'Carbon Footprint', result: carbonResults, Component: CarbonFootprintCard, refresh: updateCarbonResults }, + { id: 'features', title: 'Site Features', result: siteFeaturesResults, Component: SiteFeaturesCard, refresh: updateSiteFeaturesResults }, + { id: 'dnssec', title: 'DNSSEC', result: dnsSecResults, Component: DnsSecCard, refresh: updateDnsSecResults }, ]; + + const MakeActionButtons = (title: string, refresh: () => void, showInfo: (id: string) => void): ReactNode => { + const actions = [ + { label: `Info about ${title}`, onClick: showInfo, icon: 'ⓘ'}, + { label: `Re-fetch ${title} data`, onClick: refresh, icon: '↻'}, + ]; + return ( + + ); + }; + + const showInfo = (id: string) => { + const doc = docs.filter((doc: any) => doc.id === id)[0] || null; + setModalContent( + doc? ( + {doc.title} + About +

{doc.description}

+ Use Cases +

{doc.use}

+ Links +
    + {doc.resources.map((resource: string, index: number) => ( + + ))} +
+
) + : ( + +

No Docs provided for this widget yet

+
+ )); + setModalOpen(true); + }; + + const showErrorModal = (content: ReactNode) => { + setModalContent(content); + setModalOpen(true); + }; return ( @@ -321,20 +429,25 @@ const Results = (): JSX.Element => { } - + { - resultCardData.map(({ title, result, Component }) => ( - (result) ? ( - - - + resultCardData.map(({ id, title, result, refresh, Component }, index: number) => ( + (result && !result.error) ? ( + showInfo(id)) : undefined} + /> ) : <> )) }