Rename v1 to web-check-live

This commit is contained in:
Alicia Sykes
2024-05-08 21:23:03 +01:00
parent e5738d1f5b
commit 7e27143a90
77 changed files with 240 additions and 240 deletions

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;