Writes get robots, refactors row to be reusable, adds loading progress logic

This commit is contained in:
Alicia Sykes 2023-06-22 15:24:14 +01:00
parent 2a7a5fa0f9
commit 6062473efc
14 changed files with 335 additions and 136 deletions

View File

@ -59,6 +59,16 @@
to = "/.netlify/functions/get-cookies" to = "/.netlify/functions/get-cookies"
status = 301 status = 301
force = true force = true
[[redirects]]
from = "/get-dns"
to = "/.netlify/functions/get-dns"
status = 301
force = true
[[redirects]]
from = "/read-robots-txt"
to = "/.netlify/functions/read-robots-txt"
status = 301
force = true
# For router history mode, ensure pages land on index # For router history mode, ensure pages land on index
[[redirects]] [[redirects]]

View File

@ -17,10 +17,14 @@ const StyledHeading = styled.h1<HeadingProps>`
display: flex; display: flex;
gap: 1rem; gap: 1rem;
align-items: center; align-items: center;
img { img { // Some titles have an icon
width: 2rem; width: 2rem;
border-radius: 4px; border-radius: 4px;
} }
a { // If a title is a link, keep title styles
color: inherit;
text-decoration: none;
}
${props => { ${props => {
switch (props.size) { switch (props.size) {
case 'xSmall': return `font-size: ${TextSizes.small};`; case 'xSmall': return `font-size: ${TextSizes.small};`;

View File

@ -1,21 +1,23 @@
import styled from 'styled-components'; import styled from 'styled-components';
import colors from 'styles/colors'; import colors from 'styles/colors';
interface RowProps { export interface RowProps {
lbl: string, lbl: string,
val: string, val: string,
key?: string, key?: string,
children?: JSX.Element[],
rowList?: RowProps[], rowList?: RowProps[],
title?: string,
} }
const StyledRow = styled.div` export const StyledRow = styled.div`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 0.25rem; padding: 0.25rem;
&:not(:last-child) { border-bottom: 1px solid ${colors.primary}; } &:not(:last-child) { border-bottom: 1px solid ${colors.primary}; }
span.lbl { font-weight: bold; } span.lbl { font-weight: bold; }
span.val { span.val {
max-width: 200px; max-width: 16rem;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -64,7 +66,6 @@ const formatDate = (dateString: string): string => {
const formatValue = (value: any): string => { const formatValue = (value: any): string => {
const isValidDate = (date: any) => date instanceof Date && !isNaN(date as any as number); const isValidDate = (date: any) => date instanceof Date && !isNaN(date as any as number);
if (isValidDate(new Date(value))) return formatDate(value); if (isValidDate(new Date(value))) return formatDate(value);
if (typeof value === 'object') return JSON.stringify(value);
if (typeof value === 'boolean') return value ? '✅' : '❌'; if (typeof value === 'boolean') return value ? '✅' : '❌';
return value; return value;
}; };
@ -74,11 +75,11 @@ const copyToClipboard = (text: string) => {
} }
export const ExpandableRow = (props: RowProps) => { export const ExpandableRow = (props: RowProps) => {
const { lbl, val, rowList } = props; const { lbl, val, key, title, rowList } = props;
return ( return (
<Details> <Details>
<StyledExpandableRow> <StyledExpandableRow key={key}>
<span className="lbl">{lbl}</span> <span className="lbl" title={title}>{lbl}</span>
<span className="val" title={val}>{val}</span> <span className="val" title={val}>{val}</span>
</StyledExpandableRow> </StyledExpandableRow>
{ rowList && { rowList &&
@ -86,7 +87,7 @@ export const ExpandableRow = (props: RowProps) => {
{ rowList?.map((row: RowProps, index: number) => { { rowList?.map((row: RowProps, index: number) => {
return ( return (
<SubRow key={row.key || `${row.lbl}-${index}`}> <SubRow key={row.key || `${row.lbl}-${index}`}>
<span className="lbl">{row.lbl}</span> <span className="lbl" title={row.title}>{row.lbl}</span>
<span className="val" title={row.val} onClick={() => copyToClipboard(row.val)}> <span className="val" title={row.val} onClick={() => copyToClipboard(row.val)}>
{formatValue(row.val)} {formatValue(row.val)}
</span> </span>
@ -100,10 +101,11 @@ export const ExpandableRow = (props: RowProps) => {
}; };
const Row = (props: RowProps) => { const Row = (props: RowProps) => {
const { lbl, val, key } = props; const { lbl, val, key, title, children } = props;
if (children) return <StyledRow key={key}>{children}</StyledRow>;
return ( return (
<StyledRow key={key}> <StyledRow key={key}>
<span className="lbl">{lbl}</span> <span className="lbl" title={title}>{lbl}</span>
<span className="val" title={val} onClick={() => copyToClipboard(val)}> <span className="val" title={val} onClick={() => copyToClipboard(val)}>
{formatValue(val)} {formatValue(val)}
</span> </span>

View File

@ -3,17 +3,18 @@ import styled from 'styled-components';
import colors from 'styles/colors'; import colors from 'styles/colors';
import Card from 'components/Form/Card'; import Card from 'components/Form/Card';
import Heading from 'components/Form/Heading'; import Heading from 'components/Form/Heading';
import Row, { ExpandableRow } from 'components/Form/Row'; import { ExpandableRow } from 'components/Form/Row';
const Outer = styled(Card)``; const Outer = styled(Card)``;
const CookiesCard = (props: { cookies: any }): JSX.Element => { const CookiesCard = (props: { cookies: any }): JSX.Element => {
const cookies = props.cookies; const cookies = props.cookies;
console.log('COOKIES: ', cookies);
return ( return (
<Outer> <Outer>
<Heading as="h3" size="small" align="left" color={colors.primary}>Cookies</Heading> <Heading as="h3" size="small" align="left" color={colors.primary}>Cookies</Heading>
{/* { subject && <DataRow lbl="Subject" val={subject?.CN} /> } */} {
cookies.length === 0 && <p>No cookies found.</p>
}
{ {
cookies.map((cookie: any, index: number) => { cookies.map((cookie: any, index: number) => {
const attributes = Object.keys(cookie.attributes).map((key: string) => { const attributes = Object.keys(cookie.attributes).map((key: string) => {

View File

@ -3,35 +3,12 @@ import styled from 'styled-components';
import colors from 'styles/colors'; import colors from 'styles/colors';
import Card from 'components/Form/Card'; import Card from 'components/Form/Card';
import Heading from 'components/Form/Heading'; import Heading from 'components/Form/Heading';
import Row from 'components/Form/Row';
const Outer = styled(Card)` const Outer = styled(Card)`
grid-row: span 2; grid-row: span 2;
`; `;
const Row = styled.div`
display: flex;
justify-content: space-between;
padding: 0.25rem;
&:not(:last-child) { border-bottom: 1px solid ${colors.primary}; }
span.lbl { font-weight: bold; }
span.val {
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`;
const DataRow = (props: { lbl: string, val: string }) => {
const { lbl, val } = props;
return (
<Row>
<span className="lbl">{lbl}</span>
<span className="val" title={val}>{val}</span>
</Row>
);
};
const HeadersCard = (props: { headers: any }): JSX.Element => { const HeadersCard = (props: { headers: any }): JSX.Element => {
const headers = props.headers; const headers = props.headers;
return ( return (
@ -40,7 +17,7 @@ const HeadersCard = (props: { headers: any }): JSX.Element => {
{ {
Object.keys(headers).map((header: string, index: number) => { Object.keys(headers).map((header: string, index: number) => {
return ( return (
<DataRow key={`header-${index}`} lbl={header} val={headers[header]} /> <Row key={`header-${index}`} lbl={header} val={headers[header]} />
) )
}) })
} }

View File

@ -4,6 +4,7 @@ import styled from 'styled-components';
import colors from 'styles/colors'; import colors from 'styles/colors';
import Card from 'components/Form/Card'; import Card from 'components/Form/Card';
import Heading from 'components/Form/Heading'; import Heading from 'components/Form/Heading';
import { ExpandableRow } from 'components/Form/Row';
const processScore = (percentile: number) => { const processScore = (percentile: number) => {
return `${Math.round(percentile * 100)}%`; return `${Math.round(percentile * 100)}%`;
@ -78,23 +79,14 @@ const ScoreItem = (props: { scoreId: any, audits: Audit[] }) => {
); );
}; };
const ExpandableRow = (props: { lbl: string, val: string, list: string[], audits: Audit[] }) => { const makeValue = (audit: Audit) => {
const { lbl, val, list, audits } = props; let score = audit.score;
return ( if (audit.displayValue) {
<Row> score = audit.displayValue;
<details> } else if (audit.scoreDisplayMode) {
<summary> score = audit.score === 1 ? '✅ Pass' : '❌ Fail';
<span className="lbl">{lbl}</span> }
<span className="val" title={val}>{val}</span> return score;
</summary>
<ScoreList>
{ list.map((li: string) => {
return <ScoreItem scoreId={li} audits={audits} />
}) }
</ScoreList>
</details>
</Row>
);
}; };
const LighthouseCard = (props: { lighthouse: any }): JSX.Element => { const LighthouseCard = (props: { lighthouse: any }): JSX.Element => {
@ -106,13 +98,15 @@ const LighthouseCard = (props: { lighthouse: any }): JSX.Element => {
<Heading as="h3" size="small" align="left" color={colors.primary}>Performance</Heading> <Heading as="h3" size="small" align="left" color={colors.primary}>Performance</Heading>
{ Object.keys(categories).map((title: string, index: number) => { { Object.keys(categories).map((title: string, index: number) => {
const scoreIds = categories[title].auditRefs.map((ref: { id: string }) => ref.id); const scoreIds = categories[title].auditRefs.map((ref: { id: string }) => ref.id);
const scoreList = scoreIds.map((id: string) => {
return { lbl: audits[id].title, val: makeValue(audits[id]), title: audits[id].description, key: id }
})
return ( return (
<ExpandableRow <ExpandableRow
key={`lighthouse-${index}`} key={`lighthouse-${index}`}
lbl={title} lbl={title}
val={processScore(categories[title].score)} val={processScore(categories[title].score)}
list={scoreIds} rowList={scoreList}
audits={audits}
/> />
); );
}) } }) }

View File

@ -0,0 +1,35 @@
import styled from 'styled-components';
import colors from 'styles/colors';
import Card from 'components/Form/Card';
import Heading from 'components/Form/Heading';
import Row, { RowProps } from 'components/Form/Row';
const Outer = styled(Card)`
.content {
max-height: 20rem;
overflow-y: auto;
}
`;
const RobotsTxtCard = (props: { robotTxt: RowProps[] }): JSX.Element => {
return (
<Outer>
<Heading as="h3" size="small" align="left" color={colors.primary}>Crawl Rules</Heading>
<div className="content">
{
props.robotTxt.length === 0 && <p>No crawl rules found.</p>
}
{
props.robotTxt.map((row: RowProps, index: number) => {
return (
<Row key={row.key || `${row.lbl}-${index}`} lbl={row.lbl} val={row.val} />
)
})
}
</div>
</Outer>
);
}
export default RobotsTxtCard;

View File

@ -6,7 +6,11 @@ import Heading from 'components/Form/Heading';
const Outer = styled(Card)` const Outer = styled(Card)`
overflow: auto; overflow: auto;
max-height: 20rem; max-height: 28rem;
img {
border-radius: 6px;
width: 100%;
}
`; `;
const ScreenshotCard = (props: { screenshot: string }): JSX.Element => { const ScreenshotCard = (props: { screenshot: string }): JSX.Element => {

View File

@ -7,27 +7,12 @@ import Heading from 'components/Form/Heading';
import LocationMap from 'components/misc/LocationMap'; import LocationMap from 'components/misc/LocationMap';
import Flag from 'components/misc/Flag'; import Flag from 'components/misc/Flag';
import { TextSizes } from 'styles/typography'; import { TextSizes } from 'styles/typography';
import Row, { StyledRow } from 'components/Form/Row';
const Outer = styled(Card)` const Outer = styled(Card)`
grid-row: span 2 grid-row: span 2
`; `;
const Row = styled.div`
display: flex;
justify-content: space-between;
padding: 0.25rem;
&:not(:last-child) { border-bottom: 1px solid ${colors.primary}; }
`;
const RowLabel = styled.span`
font-weight: bold;
`;
const RowValue = styled.span`
display: flex;
img { margin-left: 0.5rem; }
`;
const SmallText = styled.span` const SmallText = styled.span`
opacity: 0.5; opacity: 0.5;
font-size: ${TextSizes.xSmall}; font-size: ${TextSizes.xSmall};
@ -35,10 +20,16 @@ const SmallText = styled.span`
display: block; display: block;
`; `;
const MapRow = styled(Row)` const MapRow = styled(StyledRow)`
padding-top: 1rem;
flex-direction: column; flex-direction: column;
`; `;
const CountryValue = styled.span`
display: flex;
gap: 0.5rem;
`;
const ServerLocationCard = (location: ServerLocation): JSX.Element => { const ServerLocationCard = (location: ServerLocation): JSX.Element => {
const { const {
@ -50,28 +41,17 @@ const ServerLocationCard = (location: ServerLocation): JSX.Element => {
return ( return (
<Outer> <Outer>
<Heading as="h3" size="small" align="left" color={colors.primary}>Location</Heading> <Heading as="h3" size="small" align="left" color={colors.primary}>Location</Heading>
<Row> <Row lbl="City" val={`${postCode}, ${city}, ${region}`} />
<RowLabel>City</RowLabel> <Row lbl="" val="">
<RowValue> <b>Country</b>
{postCode}, { city }, { region } <CountryValue>
</RowValue>
</Row>
<Row>
<RowLabel>Country</RowLabel>
<RowValue>
{country} {country}
{ countryCode && <Flag countryCode={countryCode} width={28} /> } { countryCode && <Flag countryCode={countryCode} width={28} /> }
</RowValue> </CountryValue>
</Row> </Row>
{ timezone && <Row> <Row lbl="Timezone" val={timezone} />
<RowLabel>Timezone</RowLabel><RowValue>{timezone}</RowValue> <Row lbl="Languages" val={languages} />
</Row>} <Row lbl="Currency" val={`${currency} (${currencyCode})`} />
{ languages && <Row>
<RowLabel>Languages</RowLabel><RowValue>{languages}</RowValue>
</Row>}
{ currency && <Row>
<RowLabel>Currency</RowLabel><RowValue>{currency} ({currencyCode})</RowValue>
</Row>}
<MapRow> <MapRow>
<LocationMap lat={coords.latitude} lon={coords.longitude} label={`Server (${isp})`} /> <LocationMap lat={coords.latitude} lon={coords.longitude} label={`Server (${isp})`} />
<SmallText>Latitude: {coords.latitude}, Longitude: {coords.longitude} </SmallText> <SmallText>Latitude: {coords.latitude}, Longitude: {coords.longitude} </SmallText>

View File

@ -81,7 +81,6 @@ const Home = (): JSX.Element => {
fetch('https://ipapi.co/json/') fetch('https://ipapi.co/json/')
.then(function(response) { .then(function(response) {
response.json().then(jsonData => { response.json().then(jsonData => {
console.log(jsonData);
setUserInput(jsonData.ip); setUserInput(jsonData.ip);
setPlaceholder(defaultPlaceholder); setPlaceholder(defaultPlaceholder);
setInputDisabled(true); setInputDisabled(true);

View File

@ -13,6 +13,9 @@ import BuiltWithCard from 'components/Results/BuiltWith';
import LighthouseCard from 'components/Results/Lighthouse'; import LighthouseCard from 'components/Results/Lighthouse';
import ScreenshotCard from 'components/Results/Screenshot'; import ScreenshotCard from 'components/Results/Screenshot';
import SslCertCard from 'components/Results/SslCert'; 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 keys from 'utils/get-keys'; import keys from 'utils/get-keys';
import { determineAddressType, AddressType } from 'utils/address-type-checker'; import { determineAddressType, AddressType } from 'utils/address-type-checker';
@ -21,6 +24,8 @@ import {
getServerInfo, ServerInfo, getServerInfo, ServerInfo,
getHostNames, HostNames, getHostNames, HostNames,
makeTechnologies, TechnologyGroup, makeTechnologies, TechnologyGroup,
parseCookies, Cookie,
parseRobotsTxt,
Whois, Whois,
} from 'utils/result-processor'; } from 'utils/result-processor';
@ -49,23 +54,68 @@ const Header = styled(Card)`
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
`; `;
interface ResultsType {
serverLocation?: ServerLocation,
serverInfo?: ServerInfo,
hostNames?: HostNames | null,
};
const Results = (): JSX.Element => { const Results = (): JSX.Element => {
const [ results, setResults ] = useState<ResultsType>({}); const [ serverInfo, setServerInfo ] = useState<ServerInfo>();
const [ hostNames, setHostNames ] = useState<HostNames | null>();
const [ locationResults, setLocationResults ] = useState<ServerLocation>(); const [ locationResults, setLocationResults ] = useState<ServerLocation>();
const [ whoIsResults, setWhoIsResults ] = useState<Whois>(); const [ whoIsResults, setWhoIsResults ] = useState<Whois>();
const [ technologyResults, setTechnologyResults ] = useState<TechnologyGroup[]>(); const [ technologyResults, setTechnologyResults ] = useState<TechnologyGroup[]>();
const [ lighthouseResults, setLighthouseResults ] = useState<any>(); const [ lighthouseResults, setLighthouseResults ] = useState<any>();
const [ sslResults, setSslResults ] = useState<any>(); const [ sslResults, setSslResults ] = useState<any>();
const [ headersResults, setHeadersResults ] = useState<any>();
const [ robotsTxtResults, setRobotsTxtResults ] = useState<any>();
const [ cookieResults, setCookieResults ] = useState<Cookie[] | null>(null);
const [ screenshotResult, setScreenshotResult ] = useState<string>(); const [ screenshotResult, setScreenshotResult ] = useState<string>();
const [ ipAddress, setIpAddress ] = useState<undefined | string>(undefined); const [ ipAddress, setIpAddress ] = useState<undefined | string>(undefined);
const [ addressType, setAddressType ] = useState<AddressType>('empt'); const [ addressType, setAddressType ] = useState<AddressType>('empt');
const { address } = useParams(); const { address } = useParams();
type LoadingState = 'loading' | 'skipped' | 'success' | 'error';
interface LoadingJob {
name: string,
state: LoadingState,
error?: string,
}
const jobNames = [
'get-ip',
'ssl',
'cookies',
'robots-txt',
'headers',
'lighthouse',
'location',
'server-info',
'whois',
] as const;
const initialJobs = jobNames.map((job: string) => {
return {
name: job,
state: 'loading' as LoadingState,
}
});
const [ loadingJobs, setLoadingJobs ] = useState<LoadingJob[]>(initialJobs);
const updateLoadingJobs = (job: string, newState: LoadingState, error?: string) => {
setLoadingJobs((prevJobs) => {
const newJobs = prevJobs.map((loadingJob: LoadingJob) => {
if (loadingJob.name === job) {
return { ...loadingJob, error, state: newState };
}
return loadingJob;
});
if (newState === 'error') {
console.warn(`Error in ${job}: ${error}`);
}
return newJobs;
});
};
useEffect(() => { useEffect(() => {
@ -77,43 +127,102 @@ const Results = (): JSX.Element => {
/* Get IP address from URL */ /* Get IP address from URL */
useEffect(() => { useEffect(() => {
if (addressType !== 'url') return;
const fetchIpAddress = () => { const fetchIpAddress = () => {
fetch(`/find-url-ip?address=${address}`) fetch(`/find-url-ip?address=${address}`)
.then(function(response) { .then(function(response) {
response.json().then(jsonData => { response.json().then(jsonData => {
setIpAddress(jsonData.ip); setIpAddress(jsonData.ip);
updateLoadingJobs('get-ip', 'success');
}); });
}) })
.catch(function(error) { .catch(function(error) {
console.log(error) updateLoadingJobs('get-ip', 'error', error);
}); });
}; };
if (!ipAddress) { if (!ipAddress) {
fetchIpAddress(); fetchIpAddress();
} }
}, [address]); }, [address, addressType]);
/* Get SSL info */ /* Get SSL info */
useEffect(() => { useEffect(() => {
if (addressType !== 'url') return;
fetch(`/ssl-check?url=${address}`) fetch(`/ssl-check?url=${address}`)
.then(response => response.json()) .then(response => response.json())
.then(response => { .then(response => {
console.log(response); if (Object.keys(response).length > 0) {
setSslResults(response); setSslResults(response);
updateLoadingJobs('ssl', 'success');
} else {
updateLoadingJobs('ssl', 'error', 'No SSL Cert found');
}
}) })
.catch(err => console.error(err)); .catch(err => updateLoadingJobs('ssl', 'error', err));
}, [address]) }, [address, addressType])
/* Get Cookies */
useEffect(() => {
if (addressType !== 'url') 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') 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') 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 Lighthouse report */ /* Get Lighthouse report */
useEffect(() => { useEffect(() => {
if (addressType !== 'url') return;
fetch(`/lighthouse-report?url=${address}`) fetch(`/lighthouse-report?url=${address}`)
.then(response => response.json()) .then(response => response.json())
.then(response => { .then(response => {
setLighthouseResults(response.lighthouseResult); setLighthouseResults(response.lighthouseResult);
setScreenshotResult(response.lighthouseResult?.fullPageScreenshot?.screenshot?.data); setScreenshotResult(response.lighthouseResult?.fullPageScreenshot?.screenshot?.data);
updateLoadingJobs('lighthouse', 'success');
}) })
.catch(err => console.error(err)); .catch(err => {
}, [address]) // 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 */ /* Get IP address location info */
useEffect(() => { useEffect(() => {
@ -122,10 +231,11 @@ const Results = (): JSX.Element => {
.then(function(response) { .then(function(response) {
response.json().then(jsonData => { response.json().then(jsonData => {
setLocationResults(getLocation(jsonData)); setLocationResults(getLocation(jsonData));
updateLoadingJobs('location', 'success');
}); });
}) })
.catch(function(error) { .catch(function(error) {
console.log(error) updateLoadingJobs('location', 'error', error);
}); });
}; };
if (ipAddress) { if (ipAddress) {
@ -136,18 +246,20 @@ const Results = (): JSX.Element => {
/* Get hostnames and server info from Shodan */ /* Get hostnames and server info from Shodan */
useEffect(() => { useEffect(() => {
const applyShodanResults = (response: any) => { const applyShodanResults = (response: any) => {
const serverInfo = getServerInfo(response); setServerInfo(getServerInfo(response));
const hostNames = getHostNames(response); setHostNames(getHostNames(response));
setResults({...results, serverInfo, hostNames });
} }
const fetchShodanData = () => { const fetchShodanData = () => {
const apiKey = keys.shodan; const apiKey = keys.shodan;
fetch(`https://api.shodan.io/shodan/host/${ipAddress}?key=${apiKey}`) fetch(`https://api.shodan.io/shodan/host/${ipAddress}?key=${apiKey}`)
.then(response => response.json()) .then(response => response.json())
.then(response => { .then(response => {
if (!response.error) applyShodanResults(response) if (!response.error) {
applyShodanResults(response)
updateLoadingJobs('server-info', 'success');
}
}) })
.catch(err => console.error(err)); .catch(err => updateLoadingJobs('server-info', 'error', err));
}; };
@ -158,17 +270,21 @@ const Results = (): JSX.Element => {
/* Get BuiltWith tech stack */ /* Get BuiltWith tech stack */
useEffect(() => { useEffect(() => {
if (addressType !== 'url') return;
const apiKey = keys.builtWith; const apiKey = keys.builtWith;
const endpoint = `https://api.builtwith.com/v21/api.json?KEY=${apiKey}&LOOKUP=${address}`; const endpoint = `https://api.builtwith.com/v21/api.json?KEY=${apiKey}&LOOKUP=${address}`;
fetch(endpoint) fetch(endpoint)
.then(response => response.json()) .then(response => response.json())
.then(response => { .then(response => {
setTechnologyResults(makeTechnologies(response)); setTechnologyResults(makeTechnologies(response));
}); updateLoadingJobs('built-with', 'success');
}, [address]); })
.catch(err => updateLoadingJobs('built-with', 'error', err));
}, [address, addressType]);
/* Get WhoIs info for a given domain name */ /* Get WhoIs info for a given domain name */
useEffect(() => { useEffect(() => {
if (addressType !== 'url') return;
const applyWhoIsResults = (response: any) => { const applyWhoIsResults = (response: any) => {
const whoIsResults: Whois = { const whoIsResults: Whois = {
created: response.date_created, created: response.date_created,
@ -184,8 +300,9 @@ const Results = (): JSX.Element => {
.then(response => response.json()) .then(response => response.json())
.then(response => { .then(response => {
if (!response.error) applyWhoIsResults(response) if (!response.error) applyWhoIsResults(response)
updateLoadingJobs('whois', 'success');
}) })
.catch(err => console.error(err)); .catch(err => updateLoadingJobs('whois', 'error', err));
}; };
if (addressType === 'url') { if (addressType === 'url') {
@ -193,24 +310,40 @@ const Results = (): JSX.Element => {
} }
}, [addressType, address]); }, [addressType, address]);
const makeSiteName = (address: string): string => {
try {
return new URL(address).hostname.replace('www.', '');
} catch (error) {
return address;
}
}
return ( return (
<ResultsOuter> <ResultsOuter>
<Header as="header"> <Header as="header">
<Heading color={colors.primary} size="large">Results</Heading> <Heading color={colors.primary} size="large">
<Heading color={colors.textColor} size="medium"> <a href="/">Web Check</a>
{ address && <img width="32px" src={`https://icon.horse/icon/${new URL(address).hostname}`} alt="" /> }
{address}
</Heading> </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> </Header>
<ResultsContent> <ResultsContent>
{ locationResults && <ServerLocationCard {...locationResults} />} { locationResults && <ServerLocationCard {...locationResults} />}
{ results.serverInfo && <ServerInfoCard {...results.serverInfo} />} { sslResults && <SslCertCard sslCert={sslResults} />}
{ results?.hostNames && <HostNamesCard hosts={results?.hostNames} />} { headersResults && <HeadersCard headers={headersResults} />}
{ hostNames && <HostNamesCard hosts={hostNames} />}
{ whoIsResults && <WhoIsCard {...whoIsResults} />} { whoIsResults && <WhoIsCard {...whoIsResults} />}
{ lighthouseResults && <LighthouseCard lighthouse={lighthouseResults} />} { lighthouseResults && <LighthouseCard lighthouse={lighthouseResults} />}
{ cookieResults && <CookiesCard cookies={cookieResults} />}
{ screenshotResult && <ScreenshotCard screenshot={screenshotResult} />} { screenshotResult && <ScreenshotCard screenshot={screenshotResult} />}
{ technologyResults && <BuiltWithCard technologies={technologyResults} />} { technologyResults && <BuiltWithCard technologies={technologyResults} />}
{ sslResults && <SslCertCard sslCert={sslResults} />} { robotsTxtResults && <RobotsTxtCard robotTxt={robotsTxtResults} />}
{ serverInfo && <ServerInfoCard {...serverInfo} />}
</ResultsContent> </ResultsContent>
</ResultsOuter> </ResultsOuter>
); );

View File

@ -7,17 +7,29 @@ export type AddressType = 'ipV4' | 'ipV6' | 'url' | 'err' | 'empt';
/* Checks if a given string looks like a URL */ /* Checks if a given string looks like a URL */
const isUrl = (value: string):boolean => { const isUrl = (value: string):boolean => {
const urlRegex= new RegExp('' const urlPattern = new RegExp(
// + /(?:(?:(https?|ftp):)?\/\/)/.source '^(https?:\\/\\/)?' +
+ /(?:([^:\n\r]+):([^@\n\r]+)@)?/.source '(?!([0-9]{1,3}\\.){3}[0-9]{1,3})' + // Exclude IP addresses
+ /(?:(?:www\.)?([^/\n\r]+))/.source '(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*' + // Domain name or a subdomain
+ /(\/[^?\n\r]+)?/.source '([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$', // Second level domain
+ /(\?[^#\n\r]*)?/.source 'i' // Case-insensitive
+ /(#?[^\n\r]*)?/.source
); );
return urlRegex.test(value); return urlPattern.test(value);
}; };
// /* Checks if a given string looks like a URL */
// const isUrl = (value: string):boolean => {
// const urlRegex= new RegExp(''
// // + /(?:(?:(https?|ftp):)?\/\/)/.source
// + /(?:([^:\n\r]+):([^@\n\r]+)@)?/.source
// + /(?:(?:www\.)?([^/\n\r]+))/.source
// + /(\/[^?\n\r]+)?/.source
// + /(\?[^#\n\r]*)?/.source
// + /(#?[^\n\r]*)?/.source
// );
// return urlRegex.test(value);
// };
/* Checks if a given string looks like an IP Version 4 Address */ /* Checks if a given string looks like an IP Version 4 Address */
const isIpV4 = (value: string): boolean => { const isIpV4 = (value: string): boolean => {
const ipPart = '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)'; const ipPart = '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)';
@ -51,7 +63,8 @@ const isShort = (value: string): boolean => {
}; };
/* Returns the address type for a given address */ /* Returns the address type for a given address */
export const determineAddressType = (address: string): AddressType => { export const determineAddressType = (address: string | undefined): AddressType => {
if (!address) return 'err';
if (isShort(address)) return 'empt'; if (isShort(address)) return 'empt';
if (isUrl(address)) return 'url'; if (isUrl(address)) return 'url';
if (isIpV4(address)) return 'ipV4'; if (isIpV4(address)) return 'ipV4';

View File

@ -3,6 +3,7 @@ const keys = {
shodan: process.env.REACT_APP_SHODAN_API_KEY, shodan: process.env.REACT_APP_SHODAN_API_KEY,
whoApi: process.env.REACT_APP_WHO_API_KEY, whoApi: process.env.REACT_APP_WHO_API_KEY,
builtWith: process.env.REACT_APP_BUILT_WITH_API_KEY, builtWith: process.env.REACT_APP_BUILT_WITH_API_KEY,
googleCloud: process.env.REACT_APP_GOOGLE_CLOUD_API_KEY,
}; };
export default keys; export default keys;

View File

@ -57,6 +57,8 @@ export interface ServerInfo {
os?: string, os?: string,
ip?: string, ip?: string,
ports?: string, ports?: string,
loc?: string,
type?: string,
}; };
export const getServerInfo = (response: any): ServerInfo => { export const getServerInfo = (response: any): ServerInfo => {
@ -67,6 +69,8 @@ export const getServerInfo = (response: any): ServerInfo => {
os: response.os, os: response.os,
ip: response.ip_str, ip: response.ip_str,
ports: response.ports.toString(), ports: response.ports.toString(),
loc: response.city ? `${response.city}, ${response.country_name}` : '',
type: response.tags ? response.tags.toString() : '',
}; };
}; };
@ -115,3 +119,45 @@ export const makeTechnologies = (response: any): TechnologyGroup[] => {
}, {}); }, {});
return technologies; return technologies;
}; };
export type Cookie = {
name: string;
value: string;
attributes: Record<string, string>;
};
export const parseCookies = (cookiesHeader: string): Cookie[] => {
if (!cookiesHeader) return [];
return cookiesHeader.split(/,(?=\s[A-Za-z0-9]+=)/).map(cookieString => {
const [nameValuePair, ...attributePairs] = cookieString.split('; ').map(part => part.trim());
const [name, value] = nameValuePair.split('=');
const attributes: Record<string, string> = {};
attributePairs.forEach(pair => {
const [attributeName, attributeValue = ''] = pair.split('=');
attributes[attributeName] = attributeValue;
});
return { name, value, attributes };
});
}
type RobotsRule = {
lbl: string;
val: string;
};
export const parseRobotsTxt = (content: string): RobotsRule[] => {
const lines = content.split('\n');
const rules: RobotsRule[] = [];
lines.forEach(line => {
const match = line.match(/^(Allow|Disallow):\s*(\S*)$/);
if (match) {
const rule: RobotsRule = {
lbl: match[1],
val: match[2],
};
rules.push(rule);
}
});
return rules;
}