diff --git a/src/components/Form/Card.tsx b/src/components/Form/Card.tsx index c7f66c7..e5f188a 100644 --- a/src/components/Form/Card.tsx +++ b/src/components/Form/Card.tsx @@ -1,31 +1,31 @@ import styled from 'styled-components'; -// import Heading from 'components/Form/Heading'; +import ErrorBoundary from 'components/misc/ErrorBoundary'; +import Heading from 'components/Form/Heading'; import colors from 'styles/colors'; -export const Card = styled.section` +export const StyledCard = styled.section` background: ${colors.backgroundLighter}; box-shadow: 4px 4px 0px ${colors.bgShadowColor}; border-radius: 8px; padding: 1rem; `; -// interface CardProps { -// children: React.ReactNode; -// heading?: string, -// }; +interface CardProps { + children: React.ReactNode; + heading?: string, +}; -// const Card = (props: CardProps): JSX.Element => { -// const { children, heading } = props; -// return ( -// -// { heading && -// {heading} -// } -// {children} -// -// ); -// } - -export default Card; +export const Card = (props: CardProps): JSX.Element => { + const { children, heading } = props; + return ( + + + { heading && {heading} } + {children} + + + ); +} +export default StyledCard; diff --git a/src/components/Form/Row.tsx b/src/components/Form/Row.tsx index 757b782..c5e5418 100644 --- a/src/components/Form/Row.tsx +++ b/src/components/Form/Row.tsx @@ -6,7 +6,7 @@ import Heading from 'components/Form/Heading'; export interface RowProps { lbl: string, val: string, - key?: string, + // key?: string, children?: ReactNode, rowList?: RowProps[], title?: string, @@ -77,10 +77,10 @@ const copyToClipboard = (text: string) => { } export const ExpandableRow = (props: RowProps) => { - const { lbl, val, key, title, rowList } = props; + const { lbl, val, title, rowList } = props; return (
- + {lbl} {val} @@ -88,7 +88,7 @@ export const ExpandableRow = (props: RowProps) => { { rowList?.map((row: RowProps, index: number) => { return ( - + {row.lbl} copyToClipboard(row.val)}> {formatValue(row.val)} @@ -109,7 +109,7 @@ export const ListRow = (props: { list: string[], title: string }) => { {title} { list.map((entry: string, index: number) => { return ( - + { entry } )} @@ -119,10 +119,10 @@ export const ListRow = (props: { list: string[], title: string }) => { } const Row = (props: RowProps) => { - const { lbl, val, key, title, children } = props; - if (children) return {children}; + const { lbl, val, title, children } = props; + if (children) return {children}; return ( - + { lbl && {lbl} } copyToClipboard(val)}> {formatValue(val)} diff --git a/src/components/Results/RobotsTxt.tsx b/src/components/Results/RobotsTxt.tsx index 46c9321..bbe53ee 100644 --- a/src/components/Results/RobotsTxt.tsx +++ b/src/components/Results/RobotsTxt.tsx @@ -23,7 +23,7 @@ const RobotsTxtCard = (props: { robotTxt: RowProps[] }): JSX.Element => { { props.robotTxt.map((row: RowProps, index: number) => { return ( - + ) }) } diff --git a/src/components/misc/FancyBackground.tsx b/src/components/misc/FancyBackground.tsx index c6f4a99..cff5655 100644 --- a/src/components/misc/FancyBackground.tsx +++ b/src/components/misc/FancyBackground.tsx @@ -125,14 +125,17 @@ const FancyBackground = (): JSX.Element => { var time2 = performance.now(); // Update UI - document.getElementsByClassName('dead')[0].textContent = this.deathCount; - document.getElementsByClassName('alive')[0].textContent = - this.particles.length; - document.getElementsByClassName('fps')[0].textContent = Math.floor( - 1000 / (time2 - time1) - ).toString(); - document.getElementsByClassName('drawn')[0].textContent = - this.drawnInLastFrame; + const elemDead = document.getElementsByClassName('dead'); + if (elemDead && elemDead.length > 0) elemDead[0].textContent = this.deathCount; + + const elemAlive = document.getElementsByClassName('alive'); + if (elemAlive && elemAlive.length > 0) elemAlive[0].textContent = this.particles.length; + + const elemFPS = document.getElementsByClassName('fps'); + if (elemFPS && elemFPS.length > 0) elemFPS[0].textContent = Math.round(1000 / (time2 - time1)).toString(); + + const elemDrawn = document.getElementsByClassName('drawn'); + if (elemDrawn && elemDrawn.length > 0) elemDrawn[0].textContent = this.drawnInLastFrame; }; App.birth = function () { var x, y; diff --git a/src/components/misc/ProgressBar.tsx b/src/components/misc/ProgressBar.tsx index 8873fa2..75258fe 100644 --- a/src/components/misc/ProgressBar.tsx +++ b/src/components/misc/ProgressBar.tsx @@ -140,6 +140,7 @@ const jobNames = [ 'headers', 'lighthouse', 'location', + 'shodan', // 'server-info', 'whois', ] as const; @@ -307,6 +308,7 @@ const ProgressLoader = (props: { loadStatus: LoadingJob[] }): JSX.Element => { color2={barColors[state][1]} title={`${state} (${Math.round(percentages[state])}%)`} width={percentages[state]} + key={`progress-bar-${state}`} /> )} diff --git a/src/hooks/motherOfAllHooks.ts b/src/hooks/motherOfAllHooks.ts new file mode 100644 index 0000000..1cbfba0 --- /dev/null +++ b/src/hooks/motherOfAllHooks.ts @@ -0,0 +1,67 @@ +import { useState, useEffect } from 'react'; +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; + jobId: string; + fetchRequest: () => Promise; +} + +type ResultType = any; + +type ReturnType = [ResultType | undefined, React.Dispatch>]; + + +const useMotherOfAllHooks = (params: UseIpAddressProps): ReturnType => { + // Destructure params + const { addressInfo, fetchRequest, jobId, updateLoadingJobs } = params; + const { address, addressType, expectedAddressTypes } = addressInfo; + + // Build useState that will be returned + const [result, setResult] = useState(); + + useEffect(() => { + // Still waiting for this upstream, cancel job + if (!address || !addressType) { + return; + } + // This job isn't needed for this address type, cancel job + if (!expectedAddressTypes.includes(addressType)) { + // updateLoadingJobs(jobId, 'skipped'); + return; + } + + // Initiate fetch request, set results and update loading / error state + fetchRequest() + .then((res) => { + // 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); + }); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [address, addressType]); + + return [result, setResult]; +}; + +export default useMotherOfAllHooks; + +// I really fucking hate TypeScript sometimes.... +// Feels like a weak attempt at trying to make JavaScript less crappy, +// when the real solution would be to just switch to a proper, typed, safe language +// ... Either that, or I'm just really shit at it. diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 2c548a7..ad9130a 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -93,7 +93,7 @@ const Home = (): JSX.Element => { }); }) .catch(function(error) { - console.log(error) + console.log('Failed to get IP address :\'(', error) }); }; diff --git a/src/pages/Results.tsx b/src/pages/Results.tsx index 76a1cab..dde507a 100644 --- a/src/pages/Results.tsx +++ b/src/pages/Results.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, SetStateAction, Dispatch } from 'react'; import { useParams } from "react-router-dom"; import styled from 'styled-components'; @@ -7,6 +7,8 @@ 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'; @@ -24,14 +26,16 @@ import ProgressBar, { LoadingJob, LoadingState, initialJobs } from 'components/m import keys from 'utils/get-keys'; import { determineAddressType, AddressType } from 'utils/address-type-checker'; +import useMotherHook from 'hooks/motherOfAllHooks'; + + import { getLocation, ServerLocation, - getServerInfo, ServerInfo, - getHostNames, HostNames, makeTechnologies, TechnologyGroup, parseCookies, Cookie, parseRobotsTxt, - Whois, + applyWhoIsResults, Whois, + parseShodanResults, ShodanResults } from 'utils/result-processor'; const ResultsOuter = styled.div` @@ -62,19 +66,6 @@ const Header = styled(Card)` const Results = (): JSX.Element => { const startTime = new Date().getTime(); - const [ serverInfo, setServerInfo ] = useState(); - const [ hostNames, setHostNames ] = useState(); - const [ locationResults, setLocationResults ] = useState(); - const [ whoIsResults, setWhoIsResults ] = useState(); - const [ technologyResults, setTechnologyResults ] = useState(); - const [ lighthouseResults, setLighthouseResults ] = useState(); - const [ sslResults, setSslResults ] = useState(); - const [ headersResults, setHeadersResults ] = useState(); - const [ dnsResults, setDnsResults ] = useState(); - const [ robotsTxtResults, setRobotsTxtResults ] = useState(); - const [ cookieResults, setCookieResults ] = useState(null); - const [ screenshotResult, setScreenshotResult ] = useState(); - const [ ipAddress, setIpAddress ] = useState(undefined); const [ addressType, setAddressType ] = useState('empt'); const { address } = useParams(); @@ -91,14 +82,132 @@ const Results = (): JSX.Element => { }); if (newState === 'error') { - console.warn(`Error in ${job}: ${error}`); + console.log( + `%cWeb-Check Fetch 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; }); }, []); - /* Cancel remaining jobs after 20 second timeout */ + 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({ + 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({ + 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)), + }); + + /* Cancel remaining jobs after 10 second timeout */ useEffect(() => { const checkJobs = () => { loadingJobs.forEach(job => { @@ -113,237 +222,6 @@ const Results = (): JSX.Element => { }; }, [loadingJobs, updateLoadingJobs]); // dependencies for the effect - useEffect(() => { - setAddressType(determineAddressType(address || '')); - if (addressType === 'ipV4') { - setIpAddress(address); - } - }, []); - - /* Get IP address from URL */ - useEffect(() => { - if (addressType !== 'url') { - updateLoadingJobs('get-ip', 'skipped'); - return; - } - const fetchIpAddress = () => { - fetch(`/find-url-ip?address=${address}`) - .then(function(response) { - response.json().then(jsonData => { - setIpAddress(jsonData.ip); - updateLoadingJobs('get-ip', 'success'); - }); - }) - .catch(function(error) { - updateLoadingJobs('get-ip', 'error', error); - }); - }; - if (!ipAddress) { - fetchIpAddress(); - } - }, [address, addressType]); - - /* Get SSL info */ - useEffect(() => { - if (addressType !== 'url') { - updateLoadingJobs('ssl', 'skipped'); - return; - } - fetch(`/ssl-check?url=${address}`) - .then(response => response.json()) - .then(response => { - if (Object.keys(response).length > 0) { - setSslResults(response); - updateLoadingJobs('ssl', 'success'); - } else { - updateLoadingJobs('ssl', 'error', 'No SSL Cert found'); - } - }) - .catch(err => updateLoadingJobs('ssl', 'error', err)); - }, [address, addressType]) - - /* Get Cookies */ - useEffect(() => { - if (addressType !== 'url') { - updateLoadingJobs('cookies', 'skipped'); - return; - } - fetch(`/get-cookies?url=${address}`) - .then(response => response.json()) - .then(response => { - setCookieResults(parseCookies(response.cookies)); - updateLoadingJobs('cookies', 'success'); - }) - .catch(err => updateLoadingJobs('cookies', 'error', err)); - }, [address, addressType]) - - /* Get Robots.txt */ - useEffect(() => { - if (addressType !== 'url') { - updateLoadingJobs('robots-txt', 'skipped'); - return; - } - fetch(`/read-robots-txt?url=${address}`) - .then(response => response.text()) - .then(response => { - setRobotsTxtResults(parseRobotsTxt(response)); - updateLoadingJobs('robots-txt', 'success'); - }) - .catch(err => updateLoadingJobs('robots-txt', 'error', err)); - }, [address, addressType]) - - /* Get Headers */ - useEffect(() => { - if (addressType !== 'url') { - updateLoadingJobs('headers', 'skipped'); - return; - } - fetch(`/get-headers?url=${address}`) - .then(response => response.json()) - .then(response => { - setHeadersResults(response); - updateLoadingJobs('headers', 'success'); - }) - .catch(err => updateLoadingJobs('headers', 'error', err)); - }, [address, addressType]) - - /* Get DNS records */ - useEffect(() => { - if (addressType !== 'url') { - updateLoadingJobs('dns', 'skipped'); - return; - } - fetch(`/get-dns?url=${address}`) - .then(response => response.json()) - .then(response => { - setDnsResults(response); - updateLoadingJobs('dns', 'success'); - }) - .catch(err => updateLoadingJobs('dns', 'error', err)); - }, [address, addressType]) - - /* Get Lighthouse report */ - useEffect(() => { - if (addressType !== 'url') { - updateLoadingJobs('lighthouse', 'skipped'); - return; - } - fetch(`/lighthouse-report?url=${address}`) - .then(response => response.json()) - .then(response => { - setLighthouseResults(response.lighthouseResult); - setScreenshotResult(response.lighthouseResult?.fullPageScreenshot?.screenshot?.data); - updateLoadingJobs('lighthouse', 'success'); - }) - .catch(err => { - // if (err.errorType === 'TimeoutError') { - // Netlify limits to 10 seconds, we can try again client-side... - const params = 'category=PERFORMANCE&category=ACCESSIBILITY&category=BEST_PRACTICES&category=SEO&category=PWA&strategy=mobile'; - const endpoint = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${address}&${params}&key=${keys.googleCloud}`; - fetch(endpoint) - .then(response => response.json()) - .then(response => { - setLighthouseResults(response.lightHouseResult); - setScreenshotResult(response?.lighthouseResult?.fullPageScreenshot?.screenshot?.data); - updateLoadingJobs('lighthouse', 'success'); - }) - .catch(err => updateLoadingJobs('lighthouse', 'error', err)); - }); - }, [address, addressType]) - - - /* Get IP address location info */ - useEffect(() => { - const fetchIpLocation = () => { - fetch(`https://ipapi.co/${ipAddress}/json/`) - .then(function(response) { - response.json().then(jsonData => { - setLocationResults(getLocation(jsonData)); - updateLoadingJobs('location', 'success'); - }); - }) - .catch(function(error) { - updateLoadingJobs('location', 'error', error); - }); - }; - if (ipAddress) { - fetchIpLocation(); - } - }, [ipAddress]); - - /* Get hostnames and server info from Shodan */ - useEffect(() => { - const applyShodanResults = (response: any) => { - setServerInfo(getServerInfo(response)); - setHostNames(getHostNames(response)); - } - const fetchShodanData = () => { - const apiKey = keys.shodan; - fetch(`https://api.shodan.io/shodan/host/${ipAddress}?key=${apiKey}`) - .then(response => response.json()) - .then(response => { - if (!response.error) { - applyShodanResults(response) - updateLoadingJobs('server-info', 'success'); - } - }) - .catch(err => updateLoadingJobs('server-info', 'error', err)); - }; - - - if (ipAddress) { - fetchShodanData(); - } - }, [ipAddress]); - - /* Get BuiltWith tech stack */ - useEffect(() => { - if (addressType !== 'url') { - updateLoadingJobs('built-with', 'skipped'); - return; - } - const apiKey = keys.builtWith; - const endpoint = `https://api.builtwith.com/v21/api.json?KEY=${apiKey}&LOOKUP=${address}`; - fetch(endpoint) - .then(response => response.json()) - .then(response => { - setTechnologyResults(makeTechnologies(response)); - updateLoadingJobs('built-with', 'success'); - }) - .catch(err => updateLoadingJobs('built-with', 'error', err)); - }, [address, addressType]); - - /* Get WhoIs info for a given domain name */ - useEffect(() => { - if (addressType !== 'url') { - updateLoadingJobs('whois', 'skipped'); - return; - } - const applyWhoIsResults = (response: any) => { - const whoIsResults: Whois = { - created: response.date_created, - expires: response.date_expires, - updated: response.date_updated, - nameservers: response.nameservers, - }; - setWhoIsResults(whoIsResults); - } - const fetchWhoIsData = () => { - const apiKey = keys.whoApi; - fetch(`https://api.whoapi.com/?domain=${address}&r=whois&apikey=${apiKey}`) - .then(response => response.json()) - .then(response => { - if (!response.error) applyWhoIsResults(response) - updateLoadingJobs('whois', 'success'); - }) - .catch(err => updateLoadingJobs('whois', 'error', err)); - }; - - if (addressType === 'url') { - fetchWhoIsData(); - } - }, [addressType, address]); - const makeSiteName = (address: string): string => { try { return new URL(address).hostname.replace('www.', ''); @@ -377,7 +255,7 @@ const Results = (): JSX.Element => { { headersResults && } - { hostNames && } + { shoadnResults?.hostnames && } { whoIsResults && } @@ -392,7 +270,7 @@ const Results = (): JSX.Element => { { cookieResults && } - { screenshotResult && } + { lighthouseResults?.fullPageScreenshot?.screenshot?.data && } { technologyResults && } @@ -401,7 +279,7 @@ const Results = (): JSX.Element => { { robotsTxtResults && } - { serverInfo && } + { shoadnResults?.serverInfo && }