Rename v1 to web-check-live
This commit is contained in:
59
src/web-check-live/components/misc/ActionButtons.tsx
Normal file
59
src/web-check-live/components/misc/ActionButtons.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import styled from '@emotion/styled';
|
||||
import Button from 'web-check-live/components/Form/Button';
|
||||
import colors from 'web-check-live/styles/colors';
|
||||
|
||||
const ActionButtonContainer = styled.div`
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
right: 0.25rem;
|
||||
opacity: 0.75;
|
||||
display: flex;
|
||||
gap: 0.125rem;
|
||||
align-items: baseline;
|
||||
`;
|
||||
|
||||
interface Action {
|
||||
label: string;
|
||||
icon: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const actionButtonStyles = `
|
||||
padding: 0 0.25rem;
|
||||
font-size: 1.25rem;
|
||||
text-align: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
color: ${colors.textColor};
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
color: ${colors.primary};
|
||||
background: ${colors.backgroundDarker};
|
||||
box-shadow: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const ActionButtons = (props: { actions: any }): JSX.Element => {
|
||||
const actions = props.actions;
|
||||
if (!actions) return (<></>);
|
||||
return (
|
||||
<ActionButtonContainer>
|
||||
{ actions.map((action: Action, index: number) =>
|
||||
<Button
|
||||
key={`action-${index}`}
|
||||
styles={actionButtonStyles}
|
||||
onClick={action.onClick}
|
||||
title={action.label}>
|
||||
{action.icon}
|
||||
</Button>
|
||||
)}
|
||||
</ActionButtonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionButtons;
|
||||
252
src/web-check-live/components/misc/AdditionalResources.tsx
Normal file
252
src/web-check-live/components/misc/AdditionalResources.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
import styled from '@emotion/styled';
|
||||
import colors from 'web-check-live/styles/colors';
|
||||
import { Card } from 'web-check-live/components/Form/Card';
|
||||
|
||||
const ResourceListOuter = styled.ul`
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
|
||||
li a.resource-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: ${colors.background};
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: ${colors.textColor};
|
||||
height: 100%;
|
||||
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
font-family: PTMono;
|
||||
box-sizing: border-box;
|
||||
width: -moz-available;
|
||||
box-shadow: 3px 3px 0px ${colors.backgroundDarker};
|
||||
&:hover {
|
||||
box-shadow: 5px 5px 0px ${colors.backgroundDarker};
|
||||
a { opacity: 1; }
|
||||
}
|
||||
&:active {
|
||||
box-shadow: -3px -3px 0px ${colors.fgShadowColor};
|
||||
}
|
||||
}
|
||||
img {
|
||||
width: 2.5rem;
|
||||
border-radius: 4px;
|
||||
margin: 0.25rem 0.1rem 0.1rem 0.1rem;
|
||||
}
|
||||
p, a {
|
||||
margin: 0;
|
||||
}
|
||||
.resource-link {
|
||||
color: ${colors.primary};
|
||||
opacity: 0.75;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
.resource-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
.resource-lower {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.resource-details {
|
||||
max-width: 20rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
.resource-description {
|
||||
color: ${colors.textColorSecondary};
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Note = styled.small`
|
||||
margin-top: 1rem;
|
||||
opacity: 0.5;
|
||||
display: block;
|
||||
a { color: ${colors.primary}; }
|
||||
`;
|
||||
|
||||
const CardStyles = `
|
||||
margin: 0 auto 1rem auto;
|
||||
width: 95vw;
|
||||
position: relative;
|
||||
transition: all 0.2s ease-in-out;
|
||||
`;
|
||||
|
||||
const resources = [
|
||||
{
|
||||
title: 'SSL Labs Test',
|
||||
link: 'https://ssllabs.com/ssltest/analyze.html',
|
||||
icon: 'https://i.ibb.co/6bVL8JK/Qualys-ssl-labs.png',
|
||||
description: 'Analyzes the SSL configuration of a server and grades it',
|
||||
},
|
||||
{
|
||||
title: 'Virus Total',
|
||||
link: 'https://virustotal.com',
|
||||
icon: 'https://i.ibb.co/dWFz0RC/Virustotal.png',
|
||||
description: 'Checks a URL against multiple antivirus engines',
|
||||
searchLink: 'https://www.virustotal.com/gui/domain/{URL}',
|
||||
},
|
||||
{
|
||||
title: 'Shodan',
|
||||
link: 'https://shodan.io/',
|
||||
icon: 'https://i.ibb.co/SBZ8WG4/shodan.png',
|
||||
description: 'Search engine for Internet-connected devices',
|
||||
searchLink: 'https://www.shodan.io/search/report?query={URL}',
|
||||
},
|
||||
{
|
||||
title: 'Archive',
|
||||
link: 'https://archive.org/',
|
||||
icon: 'https://i.ibb.co/nfKMvCm/Archive-org.png',
|
||||
description: 'View previous versions of a site via the Internet Archive',
|
||||
searchLink: 'https://web.archive.org/web/*/{URL}',
|
||||
},
|
||||
{
|
||||
title: 'URLScan',
|
||||
link: 'https://urlscan.io/',
|
||||
icon: 'https://i.ibb.co/cYXt8SH/Url-scan.png',
|
||||
description: 'Scans a URL and provides information about the page',
|
||||
},
|
||||
{
|
||||
title: 'Sucuri SiteCheck',
|
||||
link: 'https://sitecheck.sucuri.net/',
|
||||
icon: 'https://i.ibb.co/K5pTP1K/Sucuri-site-check.png',
|
||||
description: 'Checks a URL against blacklists and known threats',
|
||||
searchLink: 'https://www.ssllabs.com/ssltest/analyze.html?d={URL}',
|
||||
},
|
||||
{
|
||||
title: 'Domain Tools',
|
||||
link: 'https://whois.domaintools.com/',
|
||||
icon: 'https://i.ibb.co/zJfCKjM/Domain-tools.png',
|
||||
description: 'Run a WhoIs lookup on a domain',
|
||||
searchLink: 'https://whois.domaintools.com/{URL}',
|
||||
},
|
||||
{
|
||||
title: 'NS Lookup',
|
||||
link: 'https://nslookup.io/',
|
||||
icon: 'https://i.ibb.co/BLSWvBv/Ns-lookup.png',
|
||||
description: 'View DNS records for a domain',
|
||||
searchLink: 'https://www.nslookup.io/domains/{URL}/dns-records/',
|
||||
},
|
||||
{
|
||||
title: 'DNS Checker',
|
||||
link: 'https://dnschecker.org/',
|
||||
icon: 'https://i.ibb.co/gyKtgZ1/Dns-checker.webp',
|
||||
description: 'Check global DNS propagation across multiple servers',
|
||||
searchLink: 'https://dnschecker.org/#A/{URL}',
|
||||
},
|
||||
{
|
||||
title: 'Censys',
|
||||
link: 'https://search.censys.io/',
|
||||
icon: 'https://i.ibb.co/j3ZtXzM/censys.png',
|
||||
description: 'Lookup hosts associated with a domain',
|
||||
searchLink: 'https://search.censys.io/search?resource=hosts&q={URL}',
|
||||
},
|
||||
{
|
||||
title: 'Page Speed Insights',
|
||||
link: 'https://developers.google.com/speed/pagespeed/insights/',
|
||||
icon: 'https://i.ibb.co/k68t9bb/Page-speed-insights.png',
|
||||
description: 'Checks the performance, accessibility and SEO of a page on mobile + desktop',
|
||||
searchLink: 'https://developers.google.com/speed/pagespeed/insights/?url={URL}',
|
||||
},
|
||||
{
|
||||
title: 'Built With',
|
||||
link: 'https://builtwith.com/',
|
||||
icon: 'https://i.ibb.co/5LXBDfD/Built-with.png',
|
||||
description: 'View the tech stack of a website',
|
||||
searchLink: 'https://builtwith.com/{URL}',
|
||||
},
|
||||
{
|
||||
title: 'DNS Dumpster',
|
||||
link: 'https://dnsdumpster.com/',
|
||||
icon: 'https://i.ibb.co/DtQ2QXP/Trash-can-regular.png',
|
||||
description: 'DNS recon tool, to map out a domain from it\'s DNS records',
|
||||
searchLink: '',
|
||||
},
|
||||
{
|
||||
title: 'BGP Tools',
|
||||
link: 'https://bgp.tools/',
|
||||
icon: 'https://i.ibb.co/zhcSnmh/Bgp-tools.png',
|
||||
description: 'View realtime BGP data for any ASN, Prefix or DNS',
|
||||
},
|
||||
{
|
||||
title: 'Similar Web',
|
||||
link: 'https://similarweb.com/',
|
||||
icon: 'https://i.ibb.co/9YX8x3c/Similar-web.png',
|
||||
description: 'View approx traffic and engagement stats for a website',
|
||||
searchLink: 'https://similarweb.com/website/{URL}',
|
||||
},
|
||||
{
|
||||
title: 'Blacklist Checker',
|
||||
link: 'https://blacklistchecker.com/',
|
||||
icon: 'https://i.ibb.co/7ygCyz3/black-list-checker.png',
|
||||
description: 'Check if a domain, IP or email is present on the top blacklists',
|
||||
searchLink: 'https://blacklistchecker.com/check?input={URL}',
|
||||
},
|
||||
{
|
||||
title: 'Cloudflare Radar',
|
||||
link: 'https://radar.cloudflare.com/',
|
||||
icon: 'https://i.ibb.co/DGZXRgh/Cloudflare.png',
|
||||
description: 'View traffic source locations for a domain through Cloudflare',
|
||||
searchLink: 'https://radar.cloudflare.com/domains/domain/{URL}',
|
||||
},
|
||||
{
|
||||
title: 'Mozilla Observatory',
|
||||
link: 'https://observatory.mozilla.org/',
|
||||
icon: 'https://i.ibb.co/hBWh9cj/logo-mozm-5e95c457fdd1.png',
|
||||
description: 'Assesses website security posture by analyzing various security headers and practices',
|
||||
searchLink: 'https://observatory.mozilla.org/analyze/{URL}',
|
||||
},
|
||||
];
|
||||
|
||||
const makeLink = (resource: any, scanUrl: string | undefined): string => {
|
||||
return (scanUrl && resource.searchLink) ? resource.searchLink.replaceAll('{URL}', scanUrl.replace('https://', '')) : resource.link;
|
||||
};
|
||||
|
||||
const AdditionalResources = (props: { url?: string }): JSX.Element => {
|
||||
return (<Card heading="External Tools for Further Research" styles={CardStyles}>
|
||||
<ResourceListOuter>
|
||||
{
|
||||
resources.map((resource, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
<a className="resource-wrap" target="_blank" rel="noreferrer" href={makeLink(resource, props.url)}>
|
||||
<p className="resource-title">{resource.title}</p>
|
||||
<span className="resource-link" onClick={()=> window.open(resource.link, '_blank')} title={`Open: ${resource.link}`}>
|
||||
{new URL(resource.link).hostname}
|
||||
</span>
|
||||
<div className="resource-lower">
|
||||
<img src={resource.icon} alt="" />
|
||||
<div className="resource-details">
|
||||
<p className="resource-description">{resource.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ResourceListOuter>
|
||||
<Note>
|
||||
These tools are not affiliated with Web-Check. Please use them at your own risk.<br />
|
||||
At the time of listing, all of the above were available and free to use
|
||||
- if this changes, please report it via GitHub (<a href="https://github.com/lissy93/web-check">lissy93/web-check</a>).
|
||||
</Note>
|
||||
</Card>);
|
||||
}
|
||||
|
||||
export default AdditionalResources;
|
||||
56
src/web-check-live/components/misc/DocContent.tsx
Normal file
56
src/web-check-live/components/misc/DocContent.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import styled from '@emotion/styled';
|
||||
import docs, { type Doc } from 'web-check-live/utils/docs';
|
||||
import colors from 'web-check-live/styles/colors';
|
||||
import Heading from 'web-check-live/components/Form/Heading';
|
||||
|
||||
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};
|
||||
}
|
||||
summary { color: ${colors.primary};}
|
||||
h4 {
|
||||
border-top: 1px solid ${colors.primary};
|
||||
color: ${colors.primary};
|
||||
opacity: 0.75;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const DocContent = (id: string) => {
|
||||
const doc = docs.filter((doc: Doc) => doc.id === id)[0] || null;
|
||||
return (
|
||||
doc? (<JobDocsContainer>
|
||||
<Heading as="h3" size="medium" color={colors.primary}>{doc.title}</Heading>
|
||||
<Heading as="h4" size="small">About</Heading>
|
||||
<p className="doc-desc">{doc.description}</p>
|
||||
<Heading as="h4" size="small">Use Cases</Heading>
|
||||
<p className="doc-uses">{doc.use}</p>
|
||||
<Heading as="h4" size="small">Links</Heading>
|
||||
<ul>
|
||||
{doc.resources.map((resource: string | { title: string, link: string } , index: number) => (
|
||||
typeof resource === 'string' ? (
|
||||
<li id={`link-${index}`}><a target="_blank" rel="noreferrer" href={resource}>{resource}</a></li>
|
||||
) : (
|
||||
<li id={`link-${index}`}><a target="_blank" rel="noreferrer" href={resource.link}>{resource.title}</a></li>
|
||||
)
|
||||
))}
|
||||
</ul>
|
||||
<details>
|
||||
<summary><Heading as="h4" size="small">Example</Heading></summary>
|
||||
<img width="300" src={doc.screenshot} alt="Screenshot" />
|
||||
</details>
|
||||
</JobDocsContainer>)
|
||||
: (
|
||||
<JobDocsContainer>
|
||||
<p>No Docs provided for this widget yet</p>
|
||||
</JobDocsContainer>
|
||||
));
|
||||
};
|
||||
|
||||
export default DocContent;
|
||||
63
src/web-check-live/components/misc/ErrorBoundary.tsx
Normal file
63
src/web-check-live/components/misc/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
import styled from '@emotion/styled';
|
||||
import Card from 'web-check-live/components/Form/Card';
|
||||
import Heading from 'web-check-live/components/Form/Heading';
|
||||
import colors from 'web-check-live/styles/colors';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
errorMessage: string | null;
|
||||
}
|
||||
|
||||
const ErrorText = styled.p`
|
||||
color: ${colors.danger};
|
||||
`;
|
||||
|
||||
class ErrorBoundary extends Component<Props, State> {
|
||||
public state: State = {
|
||||
hasError: false,
|
||||
errorMessage: null
|
||||
};
|
||||
|
||||
// Catch errors in any components below and re-render with error message
|
||||
public static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, errorMessage: error.message };
|
||||
}
|
||||
|
||||
|
||||
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error("Uncaught error:", error, errorInfo);
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<Card>
|
||||
{ this.props.title && <Heading color={colors.primary}>{this.props.title}</Heading> }
|
||||
<ErrorText>This component errored unexpectedly</ErrorText>
|
||||
<p>
|
||||
Usually this happens if the result from the server was not what was expected.
|
||||
Check the logs for more info. If you continue to experience this issue, please raise a ticket on the repository.
|
||||
</p>
|
||||
{
|
||||
this.state.errorMessage &&
|
||||
<details>
|
||||
<summary>Error Details</summary>
|
||||
<div>{this.state.errorMessage}</div>
|
||||
</details>
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
352
src/web-check-live/components/misc/FancyBackground.tsx
Normal file
352
src/web-check-live/components/misc/FancyBackground.tsx
Normal file
@@ -0,0 +1,352 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
|
||||
const FancyBackground = (): JSX.Element => {
|
||||
|
||||
const makeAbsolute = (elem: HTMLElement) => {
|
||||
elem.style.position = 'absolute';
|
||||
elem.style.top = '0';
|
||||
elem.style.left = '0';
|
||||
};
|
||||
|
||||
const maxBy = (array: any) => {
|
||||
const chaos = 30;
|
||||
const iteratee = (e: any) => e.field + chaos * Math.random();
|
||||
let result;
|
||||
if (array == null) { return result; }
|
||||
let computed;
|
||||
for (const value of array) {
|
||||
const current = iteratee(value);
|
||||
if (current != null && (computed === undefined ? current : current > computed)) {
|
||||
computed = current;
|
||||
result = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const App: any = useMemo(() => [], []);
|
||||
|
||||
App.setup = function () {
|
||||
|
||||
this.lifespan = 1000;
|
||||
this.popPerBirth = 1;
|
||||
this.maxPop = 300;
|
||||
this.birthFreq = 2;
|
||||
this.bgColor = '#141d2b';
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
canvas.style.opacity = '0.5';
|
||||
makeAbsolute(canvas);
|
||||
this.canvas = canvas;
|
||||
const container = document.getElementById('fancy-background');
|
||||
if (container) {
|
||||
container.style.color = this.bgColor;
|
||||
makeAbsolute(container);
|
||||
container.appendChild(canvas);
|
||||
}
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.width = this.canvas.width;
|
||||
this.height = this.canvas.height;
|
||||
this.dataToImageRatio = 1;
|
||||
this.ctx.imageSmoothingEnabled = false;
|
||||
this.ctx.webkitImageSmoothingEnabled = false;
|
||||
this.ctx.msImageSmoothingEnabled = false;
|
||||
this.xC = this.width / 2;
|
||||
this.yC = this.height / 2;
|
||||
|
||||
this.stepCount = 0;
|
||||
this.particles = [];
|
||||
|
||||
|
||||
// Build grid
|
||||
this.gridSize = 8; // Motion coords
|
||||
this.gridSteps = Math.floor(1000 / this.gridSize);
|
||||
this.grid = [];
|
||||
var i = 0;
|
||||
for (var xx = -500; xx < 500; xx += this.gridSize) {
|
||||
for (var yy = -500; yy < 500; yy += this.gridSize) {
|
||||
// Radial field, triangular function of r with max around r0
|
||||
var r = Math.sqrt(xx * xx + yy * yy),
|
||||
r0 = 100,
|
||||
field;
|
||||
|
||||
if (r < r0) field = (255 / r0) * r;
|
||||
else if (r > r0) field = 255 - Math.min(255, (r - r0) / 2);
|
||||
|
||||
this.grid.push({
|
||||
x: xx,
|
||||
y: yy,
|
||||
busyAge: 0,
|
||||
spotIndex: i,
|
||||
isEdge:
|
||||
xx === -500
|
||||
? 'left'
|
||||
: xx === -500 + this.gridSize * (this.gridSteps - 1)
|
||||
? 'right'
|
||||
: yy === -500
|
||||
? 'top'
|
||||
: yy === -500 + this.gridSize * (this.gridSteps - 1)
|
||||
? 'bottom'
|
||||
: false,
|
||||
field: field,
|
||||
});
|
||||
i++;
|
||||
}
|
||||
}
|
||||
this.gridMaxIndex = i;
|
||||
|
||||
// Counters for UI
|
||||
this.drawnInLastFrame = 0;
|
||||
this.deathCount = 0;
|
||||
|
||||
this.initDraw();
|
||||
};
|
||||
App.evolve = function () {
|
||||
var time1 = performance.now();
|
||||
|
||||
this.stepCount++;
|
||||
|
||||
// Increment all grid ages
|
||||
this.grid.forEach(function (e: any) {
|
||||
if (e.busyAge > 0) e.busyAge++;
|
||||
});
|
||||
|
||||
if (
|
||||
this.stepCount % this.birthFreq === 0 &&
|
||||
this.particles.length + this.popPerBirth < this.maxPop
|
||||
) {
|
||||
this.birth();
|
||||
}
|
||||
App.move();
|
||||
App.draw();
|
||||
|
||||
var time2 = performance.now();
|
||||
|
||||
// Update UI
|
||||
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;
|
||||
var gridSpotIndex = Math.floor(Math.random() * this.gridMaxIndex);
|
||||
var gridSpot = this.grid[gridSpotIndex];
|
||||
x = gridSpot.x;
|
||||
y = gridSpot.y;
|
||||
|
||||
var particle = {
|
||||
hue: 200, // + Math.floor(50*Math.random()),
|
||||
sat: 95, //30 + Math.floor(70*Math.random()),
|
||||
lum: 20 + Math.floor(40 * Math.random()),
|
||||
x: x,
|
||||
y: y,
|
||||
xLast: x,
|
||||
yLast: y,
|
||||
xSpeed: 0,
|
||||
ySpeed: 0,
|
||||
age: 0,
|
||||
ageSinceStuck: 0,
|
||||
attractor: {
|
||||
oldIndex: gridSpotIndex,
|
||||
gridSpotIndex: gridSpotIndex, // Pop at random position on grid
|
||||
},
|
||||
name: 'seed-' + Math.ceil(10000000 * Math.random()),
|
||||
};
|
||||
this.particles.push(particle);
|
||||
};
|
||||
App.kill = function (particleName: any) {
|
||||
const newArray = this.particles.filter(
|
||||
(seed: any) => seed.name !== particleName
|
||||
);
|
||||
this.particles = [...newArray];
|
||||
};
|
||||
App.move = function () {
|
||||
for (var i = 0; i < this.particles.length; i++) {
|
||||
// Get particle
|
||||
var p = this.particles[i];
|
||||
|
||||
// Save last position
|
||||
p.xLast = p.x;
|
||||
p.yLast = p.y;
|
||||
|
||||
// Attractor and corresponding grid spot
|
||||
var index = p.attractor.gridSpotIndex,
|
||||
gridSpot = this.grid[index];
|
||||
|
||||
// Maybe move attractor and with certain constraints
|
||||
if (Math.random() < 0.5) {
|
||||
// Move attractor
|
||||
if (!gridSpot.isEdge) {
|
||||
// Change particle's attractor grid spot and local move function's grid spot
|
||||
var topIndex = index - 1,
|
||||
bottomIndex = index + 1,
|
||||
leftIndex = index - this.gridSteps,
|
||||
rightIndex = index + this.gridSteps,
|
||||
topSpot = this.grid[topIndex],
|
||||
bottomSpot = this.grid[bottomIndex],
|
||||
leftSpot = this.grid[leftIndex],
|
||||
rightSpot = this.grid[rightIndex];
|
||||
|
||||
var maxFieldSpot = maxBy(
|
||||
[topSpot, bottomSpot, leftSpot, rightSpot]
|
||||
);
|
||||
|
||||
var potentialNewGridSpot = maxFieldSpot;
|
||||
if (
|
||||
potentialNewGridSpot.busyAge === 0 ||
|
||||
potentialNewGridSpot.busyAge > 15
|
||||
) {
|
||||
p.ageSinceStuck = 0;
|
||||
p.attractor.oldIndex = index;
|
||||
p.attractor.gridSpotIndex = potentialNewGridSpot.spotIndex;
|
||||
gridSpot = potentialNewGridSpot;
|
||||
gridSpot.busyAge = 1;
|
||||
} else p.ageSinceStuck++;
|
||||
} else p.ageSinceStuck++;
|
||||
|
||||
if (p.ageSinceStuck === 10) this.kill(p.name);
|
||||
}
|
||||
|
||||
// Spring attractor to center with viscosity
|
||||
const k = 8, visc = 0.4;
|
||||
var dx = p.x - gridSpot.x,
|
||||
dy = p.y - gridSpot.y,
|
||||
dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Spring
|
||||
var xAcc = -k * dx,
|
||||
yAcc = -k * dy;
|
||||
|
||||
p.xSpeed += xAcc;
|
||||
p.ySpeed += yAcc;
|
||||
|
||||
// Calm the f*ck down
|
||||
p.xSpeed *= visc;
|
||||
p.ySpeed *= visc;
|
||||
|
||||
// Store stuff in particle brain
|
||||
p.speed = Math.sqrt(p.xSpeed * p.xSpeed + p.ySpeed * p.ySpeed);
|
||||
p.dist = dist;
|
||||
|
||||
// Update position
|
||||
p.x += 0.1 * p.xSpeed;
|
||||
p.y += 0.1 * p.ySpeed;
|
||||
|
||||
// Get older
|
||||
p.age++;
|
||||
|
||||
// Kill if too old
|
||||
if (p.age > this.lifespan) {
|
||||
this.kill(p.name);
|
||||
this.deathCount++;
|
||||
}
|
||||
}
|
||||
};
|
||||
App.initDraw = function () {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.rect(0, 0, this.width, this.height);
|
||||
this.ctx.fillStyle = this.bgColor;
|
||||
this.ctx.fill();
|
||||
this.ctx.closePath();
|
||||
};
|
||||
App.draw = function () {
|
||||
this.drawnInLastFrame = 0;
|
||||
if (!this.particles.length) return false;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.rect(0, 0, this.width, this.height);
|
||||
this.ctx.fillStyle = this.bgColor;
|
||||
this.ctx.fill();
|
||||
this.ctx.closePath();
|
||||
|
||||
for (var i = 0; i < this.particles.length; i++) {
|
||||
var p = this.particles[i];
|
||||
|
||||
var last = this.dataXYtoCanvasXY(p.xLast, p.yLast),
|
||||
now = this.dataXYtoCanvasXY(p.x, p.y);
|
||||
var attracSpot = this.grid[p.attractor.gridSpotIndex],
|
||||
attracXY = this.dataXYtoCanvasXY(attracSpot.x, attracSpot.y);
|
||||
var oldAttracSpot = this.grid[p.attractor.oldIndex],
|
||||
oldAttracXY = this.dataXYtoCanvasXY(oldAttracSpot.x, oldAttracSpot.y);
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = '#9fef00';
|
||||
this.ctx.fillStyle = '#9fef00';
|
||||
|
||||
// Particle trail
|
||||
this.ctx.moveTo(last.x, last.y);
|
||||
this.ctx.lineTo(now.x, now.y);
|
||||
|
||||
this.ctx.lineWidth = 1.5 * this.dataToImageRatio;
|
||||
this.ctx.stroke();
|
||||
this.ctx.closePath();
|
||||
|
||||
// Attractor positions
|
||||
this.ctx.beginPath();
|
||||
this.ctx.lineWidth = 1.5 * this.dataToImageRatio;
|
||||
this.ctx.moveTo(oldAttracXY.x, oldAttracXY.y);
|
||||
this.ctx.lineTo(attracXY.x, attracXY.y);
|
||||
this.ctx.arc(
|
||||
attracXY.x,
|
||||
attracXY.y,
|
||||
1.5 * this.dataToImageRatio,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
false
|
||||
);
|
||||
|
||||
this.ctx.strokeStyle = '#9fef00';
|
||||
this.ctx.fillStyle = '#9fef00';
|
||||
|
||||
this.ctx.stroke();
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.closePath();
|
||||
|
||||
// UI counter
|
||||
this.drawnInLastFrame++;
|
||||
}
|
||||
};
|
||||
App.dataXYtoCanvasXY = function (x: number, y: number) {
|
||||
var zoom = 1.6;
|
||||
var xx = this.xC + x * zoom * this.dataToImageRatio,
|
||||
yy = this.yC + y * zoom * this.dataToImageRatio;
|
||||
|
||||
return { x: xx, y: yy };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
App.setup();
|
||||
App.draw();
|
||||
|
||||
var frame = function () {
|
||||
App.evolve();
|
||||
requestAnimationFrame(frame);
|
||||
};
|
||||
frame();
|
||||
}, [App]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div id='fancy-background'>
|
||||
<p><span className='dead'>0</span></p>
|
||||
<p><span className='alive'>0</span></p>
|
||||
<p><span className='drawn'>0</span></p>
|
||||
<p><span className='fps'>0</span></p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FancyBackground;
|
||||
20
src/web-check-live/components/misc/Flag.tsx
Normal file
20
src/web-check-live/components/misc/Flag.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
interface Props {
|
||||
countryCode: string,
|
||||
width: number,
|
||||
};
|
||||
|
||||
const Flag = ({ countryCode, width }: Props): JSX.Element => {
|
||||
|
||||
const getFlagUrl = (code: string, w: number = 64) => {
|
||||
const protocol = 'https';
|
||||
const cdn = 'flagcdn.com';
|
||||
const dimensions = `${width}x${width * 0.75}`;
|
||||
const country = countryCode.toLowerCase();
|
||||
const ext = 'png';
|
||||
return `${protocol}://${cdn}/${dimensions}/${country}.${ext}`;
|
||||
};
|
||||
|
||||
return (<img src={getFlagUrl(countryCode, width)} alt={countryCode} />);
|
||||
}
|
||||
|
||||
export default Flag;
|
||||
61
src/web-check-live/components/misc/Footer.tsx
Normal file
61
src/web-check-live/components/misc/Footer.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import styled from '@emotion/styled';
|
||||
import colors from 'web-check-live/styles/colors';
|
||||
|
||||
const StyledFooter = styled.footer`
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 0.5rem 0;
|
||||
background: ${colors.backgroundDarker};
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
opacity: 0.75;
|
||||
transition: all 0.2s ease-in-out;
|
||||
@media (min-width: 1024px) {
|
||||
justify-content: space-between;
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
span {
|
||||
margin: 0 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const Link = styled.a`
|
||||
color: ${colors.primary};
|
||||
font-weight: bold;
|
||||
border-radius: 4px;
|
||||
padding: 0.1rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background: ${colors.primary};
|
||||
color: ${colors.backgroundDarker};
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const Footer = (props: { isFixed?: boolean }): JSX.Element => {
|
||||
const licenseUrl = 'https://github.com/lissy93/web-check/blob/master/LICENSE';
|
||||
const authorUrl = 'https://aliciasykes.com';
|
||||
const githubUrl = 'https://github.com/lissy93/web-check';
|
||||
return (
|
||||
<StyledFooter style={props.isFixed ? {position: 'fixed'} : {}}>
|
||||
<span>
|
||||
View source at <Link href={githubUrl}>github.com/lissy93/web-check</Link>
|
||||
</span>
|
||||
<span>
|
||||
<Link href="/check/about">Web-Check</Link> is
|
||||
licensed under <Link href={licenseUrl}>MIT</Link> -
|
||||
© <Link href={authorUrl}>Alicia Sykes</Link> 2023
|
||||
</span>
|
||||
</StyledFooter>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
103
src/web-check-live/components/misc/Loader.tsx
Normal file
103
src/web-check-live/components/misc/Loader.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { StyledCard } from 'web-check-live/components/Form/Card';
|
||||
import Heading from 'web-check-live/components/Form/Heading';
|
||||
import colors from 'web-check-live/styles/colors';
|
||||
|
||||
const LoaderContainer = styled(StyledCard)`
|
||||
margin: 0 auto 1rem auto;
|
||||
width: 95vw;
|
||||
position: relative;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
height: 50vh;
|
||||
transition: all 0.3s ease-in-out;
|
||||
p.loadTimeInfo {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
color: ${colors.textColorSecondary};
|
||||
opacity: 0.5;
|
||||
}
|
||||
&.flex {
|
||||
display: flex;
|
||||
}
|
||||
&.finished {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
svg { width: 0; }
|
||||
h4 { font-size: 0; }
|
||||
}
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSvg = styled.svg`
|
||||
width: 200px;
|
||||
margin: 0 auto;
|
||||
path {
|
||||
fill: ${colors.primary};
|
||||
&:nth-child(2) { opacity: 0.8; }
|
||||
&:nth-child(3) { opacity: 0.5; }
|
||||
}
|
||||
`;
|
||||
|
||||
const Loader = (props: { show: boolean }): JSX.Element => {
|
||||
return (
|
||||
<LoaderContainer className={props.show ? '' : 'finished'}>
|
||||
<Heading as="h4" color={colors.primary}>Crunching data...</Heading>
|
||||
<StyledSvg version="1.1" id="L7" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 100 100" enableBackground="new 0 0 100 100">
|
||||
<path fill="#fff" d="M31.6,3.5C5.9,13.6-6.6,42.7,3.5,68.4c10.1,25.7,39.2,38.3,64.9,28.1l-3.1-7.9c-21.3,8.4-45.4-2-53.8-23.3
|
||||
c-8.4-21.3,2-45.4,23.3-53.8L31.6,3.5z">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
attributeType="XML"
|
||||
type="rotate"
|
||||
dur="2s"
|
||||
from="0 50 50"
|
||||
to="360 50 50"
|
||||
repeatCount="indefinite" />
|
||||
</path>
|
||||
<path fill="#fff" d="M42.3,39.6c5.7-4.3,13.9-3.1,18.1,2.7c4.3,5.7,3.1,13.9-2.7,18.1l4.1,5.5c8.8-6.5,10.6-19,4.1-27.7
|
||||
c-6.5-8.8-19-10.6-27.7-4.1L42.3,39.6z">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
attributeType="XML"
|
||||
type="rotate"
|
||||
dur="1s"
|
||||
from="0 50 50"
|
||||
to="-360 50 50"
|
||||
repeatCount="indefinite" />
|
||||
</path>
|
||||
<path fill="#fff" d="M82,35.7C74.1,18,53.4,10.1,35.7,18S10.1,46.6,18,64.3l7.6-3.4c-6-13.5,0-29.3,13.5-35.3s29.3,0,35.3,13.5
|
||||
L82,35.7z">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
attributeType="XML"
|
||||
type="rotate"
|
||||
dur="2s"
|
||||
from="0 50 50"
|
||||
to="360 50 50"
|
||||
repeatCount="indefinite" />
|
||||
</path>
|
||||
</StyledSvg>
|
||||
<p className="loadTimeInfo">
|
||||
It may take up-to a minute for all jobs to complete<br />
|
||||
You can view preliminary results as they come in below
|
||||
</p>
|
||||
</LoaderContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Loader;
|
||||
|
||||
|
||||
|
||||
59
src/web-check-live/components/misc/LocationMap.tsx
Normal file
59
src/web-check-live/components/misc/LocationMap.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
ComposableMap,
|
||||
Geographies,
|
||||
Geography,
|
||||
Annotation,
|
||||
} from 'react-simple-maps';
|
||||
|
||||
import colors from 'web-check-live/styles/colors';
|
||||
import MapFeatures from 'web-check-live/assets/data/map-features.json';
|
||||
|
||||
interface Props {
|
||||
lat: number,
|
||||
lon: number,
|
||||
label?: string,
|
||||
};
|
||||
|
||||
const MapChart = (location: Props) => {
|
||||
const { lat, lon, label } = location;
|
||||
|
||||
return (
|
||||
<ComposableMap
|
||||
projection="geoAzimuthalEqualArea"
|
||||
projectionConfig={{
|
||||
rotate: [0, 0, 0],
|
||||
center: [lon + 5, lat - 25],
|
||||
scale: 200
|
||||
}}
|
||||
>
|
||||
<Geographies
|
||||
geography={MapFeatures}
|
||||
fill={colors.backgroundDarker}
|
||||
stroke={colors.primary}
|
||||
strokeWidth={0.5}
|
||||
>
|
||||
{({ geographies }: any) =>
|
||||
geographies.map((geo: any) => (
|
||||
<Geography key={geo.rsmKey} geography={geo} />
|
||||
))
|
||||
}
|
||||
</Geographies>
|
||||
<Annotation
|
||||
subject={[lon, lat]}
|
||||
dx={-80}
|
||||
dy={-80}
|
||||
connectorProps={{
|
||||
stroke: colors.textColor,
|
||||
strokeWidth: 3,
|
||||
strokeLinecap: "round"
|
||||
}}
|
||||
>
|
||||
<text x="-8" textAnchor="end" fill={colors.textColor} fontSize={25}>
|
||||
{label || "Server"}
|
||||
</text>
|
||||
</Annotation>
|
||||
</ComposableMap>
|
||||
);
|
||||
};
|
||||
|
||||
export default MapChart;
|
||||
468
src/web-check-live/components/misc/ProgressBar.tsx
Normal file
468
src/web-check-live/components/misc/ProgressBar.tsx
Normal file
@@ -0,0 +1,468 @@
|
||||
import styled from '@emotion/styled';
|
||||
import colors from 'web-check-live/styles/colors';
|
||||
import Card from 'web-check-live/components/Form/Card';
|
||||
import Heading from 'web-check-live/components/Form/Heading';
|
||||
import { useState, useEffect, type ReactNode } from 'react';
|
||||
|
||||
|
||||
const LoadCard = styled(Card)`
|
||||
margin: 0 auto 1rem auto;
|
||||
width: 95vw;
|
||||
position: relative;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&.hidden {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ProgressBarContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 0.5rem;
|
||||
background: ${colors.bgShadowColor};
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const ProgressBarSegment = styled.div<{ color: string, color2: string, width: number }>`
|
||||
height: 1rem;
|
||||
display: inline-block;
|
||||
width: ${props => props.width}%;
|
||||
background: ${props => props.color};
|
||||
background: ${props => props.color2 ?
|
||||
`repeating-linear-gradient( 315deg, ${props.color}, ${props.color} 3px, ${props.color2} 3px, ${props.color2} 6px )`
|
||||
: props.color};
|
||||
transition: width 0.5s ease-in-out;
|
||||
`;
|
||||
|
||||
const Details = styled.details`
|
||||
transition: all 0.2s ease-in-out;
|
||||
summary {
|
||||
margin: 0.5rem 0;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
summary:before {
|
||||
content: "►";
|
||||
position: absolute;
|
||||
margin-left: -1rem;
|
||||
color: ${colors.primary};
|
||||
cursor: pointer;
|
||||
}
|
||||
&[open] summary:before {
|
||||
content: "▼";
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0.25rem;
|
||||
border-radius: 4px;
|
||||
width: fit-content;
|
||||
li b {
|
||||
cursor: pointer;
|
||||
}
|
||||
i {
|
||||
color: ${colors.textColorSecondary};
|
||||
}
|
||||
}
|
||||
p.error {
|
||||
margin: 0.5rem 0;
|
||||
opacity: 0.75;
|
||||
color: ${colors.danger};
|
||||
}
|
||||
`;
|
||||
|
||||
const StatusInfoWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.run-status {
|
||||
color: ${colors.textColorSecondary};
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const AboutPageLink = styled.a`
|
||||
color: ${colors.primary};
|
||||
`;
|
||||
|
||||
const SummaryContainer = styled.div`
|
||||
margin: 0.5rem 0;
|
||||
b {
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
opacity: 0.75;
|
||||
}
|
||||
&.error-info {
|
||||
color: ${colors.danger};
|
||||
}
|
||||
&.success-info {
|
||||
color: ${colors.success};
|
||||
}
|
||||
&.loading-info {
|
||||
color: ${colors.info};
|
||||
}
|
||||
.skipped {
|
||||
margin-left: 0.75rem;
|
||||
color: ${colors.warning};
|
||||
}
|
||||
.success {
|
||||
margin-left: 0.75rem;
|
||||
color: ${colors.success};
|
||||
}
|
||||
`;
|
||||
|
||||
const ReShowContainer = styled.div`
|
||||
position: relative;
|
||||
&.hidden {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
button { background: none;}
|
||||
`;
|
||||
|
||||
const DismissButton = styled.button`
|
||||
width: fit-content;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
background: ${colors.background};
|
||||
color: ${colors.textColorSecondary};
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: PTMono;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: ${colors.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const FailedJobActionButton = styled.button`
|
||||
margin: 0.1rem 0.1rem 0.1rem 0.5rem;
|
||||
background: ${colors.background};
|
||||
color: ${colors.textColorSecondary};
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: PTMono;
|
||||
cursor: pointer;
|
||||
border: 1px solid ${colors.textColorSecondary};
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
color: ${colors.primary};
|
||||
border: 1px solid ${colors.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const ErrorModalContent = styled.div`
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
pre {
|
||||
color: ${colors.danger};
|
||||
&.info {
|
||||
color: ${colors.warning};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export type LoadingState = 'success' | 'loading' | 'skipped' | 'error' | 'timed-out';
|
||||
|
||||
export interface LoadingJob {
|
||||
name: string,
|
||||
state: LoadingState,
|
||||
error?: string,
|
||||
timeTaken?: number,
|
||||
retry?: () => void,
|
||||
}
|
||||
|
||||
const jobNames = [
|
||||
'get-ip',
|
||||
'location',
|
||||
'ssl',
|
||||
'domain',
|
||||
'quality',
|
||||
'tech-stack',
|
||||
'server-info',
|
||||
'cookies',
|
||||
'headers',
|
||||
'dns',
|
||||
'hosts',
|
||||
'http-security',
|
||||
'social-tags',
|
||||
'trace-route',
|
||||
'security-txt',
|
||||
'dns-server',
|
||||
'firewall',
|
||||
'dnssec',
|
||||
'hsts',
|
||||
'threats',
|
||||
'mail-config',
|
||||
'archives',
|
||||
'rank',
|
||||
'screenshot',
|
||||
'tls-cipher-suites',
|
||||
'tls-security-config',
|
||||
'tls-client-support',
|
||||
'redirects',
|
||||
'linked-pages',
|
||||
'robots-txt',
|
||||
'status',
|
||||
'ports',
|
||||
// 'whois',
|
||||
'txt-records',
|
||||
'block-lists',
|
||||
'features',
|
||||
'sitemap',
|
||||
'carbon',
|
||||
] as const;
|
||||
|
||||
interface JobListItemProps {
|
||||
job: LoadingJob;
|
||||
showJobDocs: (name: string) => void;
|
||||
showErrorModal: (name: string, state: LoadingState, timeTaken: number | undefined, error: string, isInfo?: boolean) => void;
|
||||
barColors: Record<LoadingState, [string, string]>;
|
||||
}
|
||||
|
||||
const getStatusEmoji = (state: LoadingState): string => {
|
||||
switch (state) {
|
||||
case 'success':
|
||||
return '✅';
|
||||
case 'loading':
|
||||
return '🔄';
|
||||
case 'error':
|
||||
return '❌';
|
||||
case 'timed-out':
|
||||
return '⏸️';
|
||||
case 'skipped':
|
||||
return '⏭️';
|
||||
default:
|
||||
return '❓';
|
||||
}
|
||||
};
|
||||
|
||||
const JobListItem: React.FC<JobListItemProps> = ({ job, showJobDocs, showErrorModal, barColors }) => {
|
||||
const { name, state, timeTaken, retry, error } = job;
|
||||
const actionButton = retry && state !== 'success' && state !== 'loading' ?
|
||||
<FailedJobActionButton onClick={retry}>↻ Retry</FailedJobActionButton> : null;
|
||||
|
||||
const showModalButton = error && ['error', 'timed-out', 'skipped'].includes(state) &&
|
||||
<FailedJobActionButton onClick={() => showErrorModal(name, state, timeTaken, error, state === 'skipped')}>
|
||||
{state === 'timed-out' ? '■ Show Timeout Reason' : '■ Show Error'}
|
||||
</FailedJobActionButton>;
|
||||
|
||||
return (
|
||||
<li key={name}>
|
||||
<b onClick={() => showJobDocs(name)}>{getStatusEmoji(state)} {name}</b>
|
||||
<span style={{color: barColors[state][0]}}> ({state})</span>.
|
||||
<i>{timeTaken && state !== 'loading' ? ` Took ${timeTaken} ms` : ''}</i>
|
||||
{actionButton}
|
||||
{showModalButton}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const initialJobs = jobNames.map((job: string) => {
|
||||
return {
|
||||
name: job,
|
||||
state: 'loading' as LoadingState,
|
||||
retry: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
export const calculateLoadingStatePercentages = (loadingJobs: LoadingJob[]): Record<LoadingState | string, number> => {
|
||||
const totalJobs = loadingJobs.length;
|
||||
|
||||
// Initialize count object
|
||||
const stateCount: Record<LoadingState, number> = {
|
||||
'success': 0,
|
||||
'loading': 0,
|
||||
'timed-out': 0,
|
||||
'error': 0,
|
||||
'skipped': 0,
|
||||
};
|
||||
|
||||
// Count the number of each state
|
||||
loadingJobs.forEach((job) => {
|
||||
stateCount[job.state] += 1;
|
||||
});
|
||||
|
||||
// Convert counts to percentages
|
||||
const statePercentage: Record<LoadingState, number> = {
|
||||
'success': (stateCount['success'] / totalJobs) * 100,
|
||||
'loading': (stateCount['loading'] / totalJobs) * 100,
|
||||
'timed-out': (stateCount['timed-out'] / totalJobs) * 100,
|
||||
'error': (stateCount['error'] / totalJobs) * 100,
|
||||
'skipped': (stateCount['skipped'] / totalJobs) * 100,
|
||||
};
|
||||
|
||||
return statePercentage;
|
||||
};
|
||||
|
||||
const MillisecondCounter = (props: {isDone: boolean}) => {
|
||||
const { isDone } = props;
|
||||
const [milliseconds, setMilliseconds] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
// Start the timer as soon as the component mounts
|
||||
if (!isDone) {
|
||||
timer = setInterval(() => {
|
||||
setMilliseconds(milliseconds => milliseconds + 100);
|
||||
}, 100);
|
||||
}
|
||||
// Clean up the interval on unmount
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [isDone]); // If the isDone prop changes, the effect will re-run
|
||||
|
||||
return <span>{milliseconds} ms</span>;
|
||||
};
|
||||
|
||||
const RunningText = (props: { state: LoadingJob[], count: number }): JSX.Element => {
|
||||
const loadingTasksCount = jobNames.length - props.state.filter((val: LoadingJob) => val.state === 'loading').length;
|
||||
const isDone = loadingTasksCount >= jobNames.length;
|
||||
return (
|
||||
<p className="run-status">
|
||||
{ isDone ? 'Finished in ' : `Running ${loadingTasksCount} of ${jobNames.length} jobs - ` }
|
||||
<MillisecondCounter isDone={isDone} />
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const SummaryText = (props: { state: LoadingJob[], count: number }): JSX.Element => {
|
||||
const totalJobs = jobNames.length;
|
||||
let failedTasksCount = props.state.filter((val: LoadingJob) => val.state === 'error').length;
|
||||
let loadingTasksCount = props.state.filter((val: LoadingJob) => val.state === 'loading').length;
|
||||
let skippedTasksCount = props.state.filter((val: LoadingJob) => val.state === 'skipped').length;
|
||||
let successTasksCount = props.state.filter((val: LoadingJob) => val.state === 'success').length;
|
||||
|
||||
const jobz = (jobCount: number) => `${jobCount} ${jobCount === 1 ? 'job' : 'jobs'}`;
|
||||
|
||||
const skippedInfo = skippedTasksCount > 0 ? (<span className="skipped">{jobz(skippedTasksCount)} skipped </span>) : null;
|
||||
const successInfo = successTasksCount > 0 ? (<span className="success">{jobz(successTasksCount)} successful </span>) : null;
|
||||
const failedInfo = failedTasksCount > 0 ? (<span className="error">{jobz(failedTasksCount)} failed </span>) : null;
|
||||
|
||||
if (loadingTasksCount > 0) {
|
||||
return (
|
||||
<SummaryContainer className="loading-info">
|
||||
<b>Loading {totalJobs - loadingTasksCount} / {totalJobs} Jobs</b>
|
||||
{skippedInfo}
|
||||
</SummaryContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (failedTasksCount === 0) {
|
||||
return (
|
||||
<SummaryContainer className="success-info">
|
||||
<b>{successTasksCount} Jobs Completed Successfully</b>
|
||||
{skippedInfo}
|
||||
</SummaryContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SummaryContainer className="error-info">
|
||||
{successInfo}
|
||||
{skippedInfo}
|
||||
{failedInfo}
|
||||
</SummaryContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const ProgressLoader = (props: { loadStatus: LoadingJob[], showModal: (err: ReactNode) => void, showJobDocs: (job: string) => void }): JSX.Element => {
|
||||
const [ hideLoader, setHideLoader ] = useState<boolean>(false);
|
||||
const loadStatus = props.loadStatus;
|
||||
const percentages = calculateLoadingStatePercentages(loadStatus);
|
||||
|
||||
const loadingTasksCount = jobNames.length - loadStatus.filter((val: LoadingJob) => val.state === 'loading').length;
|
||||
const isDone = loadingTasksCount >= jobNames.length;
|
||||
|
||||
const makeBarColor = (colorCode: string): [string, string] => {
|
||||
const amount = 10;
|
||||
const darkerColorCode = '#' + colorCode.replace(/^#/, '').replace(
|
||||
/../g,
|
||||
colorCode => ('0' + Math.min(255, Math.max(0, parseInt(colorCode, 16) - amount)).toString(16)).slice(-2),
|
||||
);
|
||||
return [colorCode, darkerColorCode];
|
||||
};
|
||||
|
||||
const barColors: Record<LoadingState | string, [string, string]> = {
|
||||
'success': isDone ? makeBarColor(colors.primary) : makeBarColor(colors.success),
|
||||
'loading': makeBarColor(colors.info),
|
||||
'error': makeBarColor(colors.danger),
|
||||
'timed-out': makeBarColor(colors.warning),
|
||||
'skipped': makeBarColor(colors.neutral),
|
||||
};
|
||||
|
||||
const showErrorModal = (name: string, state: LoadingState, timeTaken: number | undefined, error: string, isInfo?: boolean) => {
|
||||
const errorContent = (
|
||||
<ErrorModalContent>
|
||||
<Heading as="h3">Error Details for {name}</Heading>
|
||||
<p>
|
||||
The {name} job failed with an {state} state after {timeTaken} ms.
|
||||
The server responded with the following error:
|
||||
</p>
|
||||
{ /* If isInfo == true, then add .info className to pre */}
|
||||
<pre className={isInfo ? 'info' : 'error'}>{error}</pre>
|
||||
</ErrorModalContent>
|
||||
);
|
||||
props.showModal(errorContent);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReShowContainer className={!hideLoader ? 'hidden' : ''}>
|
||||
<DismissButton onClick={() => setHideLoader(false)}>Show Load State</DismissButton>
|
||||
</ReShowContainer>
|
||||
<LoadCard className={hideLoader ? 'hidden' : ''}>
|
||||
<ProgressBarContainer>
|
||||
{Object.keys(percentages).map((state: string | LoadingState) =>
|
||||
<ProgressBarSegment
|
||||
color={barColors[state][0]}
|
||||
color2={barColors[state][1]}
|
||||
title={`${state} (${Math.round(percentages[state])}%)`}
|
||||
width={percentages[state]}
|
||||
key={`progress-bar-${state}`}
|
||||
/>
|
||||
)}
|
||||
</ProgressBarContainer>
|
||||
|
||||
<StatusInfoWrapper>
|
||||
<SummaryText state={loadStatus} count={loadStatus.length} />
|
||||
<RunningText state={loadStatus} count={loadStatus.length} />
|
||||
</StatusInfoWrapper>
|
||||
|
||||
<Details>
|
||||
<summary>Show Details</summary>
|
||||
<ul>
|
||||
{loadStatus.map((job: LoadingJob) => (
|
||||
<JobListItem key={job.name} job={job} showJobDocs={props.showJobDocs} showErrorModal={showErrorModal} barColors={barColors} />
|
||||
))}
|
||||
</ul>
|
||||
{ loadStatus.filter((val: LoadingJob) => val.state === 'error').length > 0 &&
|
||||
<p className="error">
|
||||
<b>Check the browser console for logs and more info</b><br />
|
||||
It's normal for some jobs to fail, either because the host doesn't return the required info,
|
||||
or restrictions in the lambda function, or hitting an API limit.
|
||||
</p>}
|
||||
<AboutPageLink href="/about" target="_blank" rel="noreferer" >Learn More about Web-Check</AboutPageLink>
|
||||
</Details>
|
||||
<DismissButton onClick={() => setHideLoader(true)}>Dismiss</DismissButton>
|
||||
</LoadCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default ProgressLoader;
|
||||
49
src/web-check-live/components/misc/SelfScanMsg.tsx
Normal file
49
src/web-check-live/components/misc/SelfScanMsg.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import colors from 'web-check-live/styles/colors';
|
||||
import { StyledCard } from 'web-check-live/components/Form/Card';
|
||||
|
||||
const StyledSelfScanMsg = styled(StyledCard)`
|
||||
margin: 0px auto 1rem;
|
||||
width: 95vw;
|
||||
a { color: ${colors.primary}; }
|
||||
b { font-weight: extra-bold; }
|
||||
span, i { opacity: 0.85; }
|
||||
img {
|
||||
width: 5rem;
|
||||
float: right;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const messages = [
|
||||
'Nice try! But scanning this app is like trying to tickle yourself. It just doesn\'t work!',
|
||||
'Recursive scanning detected. The universe might implode...or it might not. But let\'s not try to find out.',
|
||||
'Hey, stop checking us out! We\'re blushing... 😉',
|
||||
'Hmmm, scanning us, are you? We feel so special!',
|
||||
'Alert! Mirror scanning detected. Trust us, we\'re looking good 😉',
|
||||
'We\'re flattered you\'re trying to scan us, but we can\'t tickle ourselves!',
|
||||
'Oh, inspecting the inspector, aren\'t we? Inception much?',
|
||||
'Just a second...wait a minute...you\'re scanning us?! Well, that\'s an interesting twist!',
|
||||
'Scanning us? It\'s like asking a mirror to reflect on itself.',
|
||||
'Well, this is awkward... like a dog chasing its own tail!',
|
||||
'Ah, I see you\'re scanning this site... But alas, this did not cause an infinite recursive loop (this time)',
|
||||
];
|
||||
|
||||
const SelfScanMsg = () => {
|
||||
return (
|
||||
<StyledSelfScanMsg>
|
||||
<img src="https://i.ibb.co/0tQbCPJ/test2.png" alt="Self-Scan" />
|
||||
<b>{messages[Math.floor(Math.random() * messages.length)]}</b>
|
||||
<br />
|
||||
<span>
|
||||
But if you want to see how this site is built, why not check out
|
||||
the <a href='https://github.com/lissy93/web-check'>source code</a>?
|
||||
</span>
|
||||
<br />
|
||||
<i>Do me a favour, and drop the repo a Star while you're there</i> 😉
|
||||
</StyledSelfScanMsg>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelfScanMsg;
|
||||
107
src/web-check-live/components/misc/ViewRaw.tsx
Normal file
107
src/web-check-live/components/misc/ViewRaw.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import colors from 'web-check-live/styles/colors';
|
||||
import { Card } from 'web-check-live/components/Form/Card';
|
||||
import Button from 'web-check-live/components/Form/Button';
|
||||
|
||||
const CardStyles = `
|
||||
margin: 0 auto 1rem auto;
|
||||
width: 95vw;
|
||||
position: relative;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
a {
|
||||
color: ${colors.primary};
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
button {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
small {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIframe = styled.iframe`
|
||||
width: calc(100% - 2rem);
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
min-height: 50vh;
|
||||
height: 100%;
|
||||
margin: 1rem;
|
||||
background: ${colors.background};
|
||||
`;
|
||||
|
||||
const ViewRaw = (props: { everything: { id: string, result: any}[] }) => {
|
||||
const [resultUrl, setResultUrl] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const makeResults = () => {
|
||||
const result: {[key: string]: any} = {};
|
||||
props.everything.forEach((item: {id: string, result: any}) => {
|
||||
result[item.id] = item.result;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const fetchResultsUrl = async () => {
|
||||
const resultContent = makeResults();
|
||||
const response = await fetch('https://jsonhero.io/api/create.json', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: 'web-check results',
|
||||
content: resultContent,
|
||||
readOnly: true,
|
||||
ttl: 3600,
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
setError(`HTTP error! status: ${response.status}`);
|
||||
} else {
|
||||
setError(null);
|
||||
}
|
||||
await response.json().then(
|
||||
(data) => setResultUrl(data.location)
|
||||
)
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const blob = new Blob([JSON.stringify(makeResults(), null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'web-check-results.json';
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
return (
|
||||
<Card heading="View / Download Raw Data" styles={CardStyles}>
|
||||
<div className="controls">
|
||||
<Button onClick={handleDownload}>Download Results</Button>
|
||||
<Button onClick={fetchResultsUrl}>{resultUrl ? 'Update Results' : 'View Results'}</Button>
|
||||
{ resultUrl && <Button onClick={() => setResultUrl('') }>Hide Results</Button> }
|
||||
</div>
|
||||
{ resultUrl && !error &&
|
||||
<>
|
||||
<StyledIframe title="Results, via JSON Hero" src={resultUrl} />
|
||||
<small>Your results are available to view <a href={resultUrl}>here</a>.</small>
|
||||
</>
|
||||
}
|
||||
{ error && <p className="error">{error}</p> }
|
||||
<small>
|
||||
These are the raw results generated from your URL, and in JSON format.
|
||||
You can import these into your own program, for further analysis.
|
||||
</small>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewRaw;
|
||||
Reference in New Issue
Block a user