chore: Upgrades all deps, migrate to new astro, ts, etc. removes non-essential deps

This commit is contained in:
Alicia Sykes
2026-05-05 12:10:19 +01:00
parent 24e13885ba
commit 3aeb8cf86d
54 changed files with 5649 additions and 4558 deletions

View File

@@ -1,5 +1,5 @@
# Specify the Node.js version to use
ARG NODE_VERSION=20
ARG NODE_VERSION=22
# Specify the Debian version to use, the default is "bullseye"
ARG DEBIAN_VERSION=bullseye

108
api/_common/http.js Normal file
View File

@@ -0,0 +1,108 @@
// Thin fetch wrapper matching the axios shape used across the api: opts.params,
// opts.headers, opts.auth, opts.timeout, opts.validateStatus; returns
// { data, status, statusText, headers }; throws errors with response/code
const DEFAULT_TIMEOUT = 60000;
const buildAuth = (auth) => {
if (!auth?.username) return null;
return 'Basic ' + Buffer.from(`${auth.username}:${auth.password}`).toString('base64');
};
const appendParams = (url, params) => {
if (!params) return url;
const u = new URL(url);
for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v);
return u.href;
};
const headersToObject = (headers) => {
const out = {};
for (const [k, v] of headers.entries()) {
const key = k.toLowerCase();
if (key === 'set-cookie') {
out[key] = headers.getSetCookie ? headers.getSetCookie() : v.split(/, (?=[^;]+=)/);
} else {
out[key] = v;
}
}
return out;
};
// Auto-parse JSON when the response advertises it, fall back to raw text
const parseBody = async (response) => {
const ct = (response.headers.get('content-type') || '').toLowerCase();
const text = await response.text();
if (!text) return ct.includes('json') ? null : '';
if (ct.includes('json')) {
try { return JSON.parse(text); } catch { return text; }
}
return text;
};
const isOk = (status, validate) =>
validate ? validate(status) : (status >= 200 && status < 300);
const wrapNetworkError = (error) => {
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
const e = new Error(error.message || 'Request timed out');
e.code = 'ECONNABORTED';
return e;
}
const code = error.cause?.code;
if (code) {
const e = new Error(error.message);
e.code = code;
return e;
}
return error;
};
const send = async (method, url, body, opts = {}) => {
const finalUrl = appendParams(url, opts.params);
const headers = { ...opts.headers };
const authHeader = buildAuth(opts.auth);
if (authHeader) headers.authorization = authHeader;
const init = {
method,
headers,
signal: AbortSignal.timeout(opts.timeout || DEFAULT_TIMEOUT),
};
if (body !== undefined && body !== null) {
if (typeof body === 'object') {
init.body = JSON.stringify(body);
const hasCt = Object.keys(headers).some(k => k.toLowerCase() === 'content-type');
if (!hasCt) init.headers['content-type'] = 'application/json';
} else {
init.body = body;
}
}
let response;
try {
response = await fetch(finalUrl, init);
} catch (error) {
throw wrapNetworkError(error);
}
const data = await parseBody(response);
const result = {
data,
status: response.status,
statusText: response.statusText,
headers: headersToObject(response.headers),
};
if (!isOk(response.status, opts.validateStatus)) {
const err = new Error(`Request failed with status code ${response.status}`);
err.response = result;
throw err;
}
return result;
};
export const httpGet = (url, opts) => send('GET', url, null, opts);
export const httpPost = (url, body, opts) => send('POST', url, body, opts);

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
const convertTimestampToDate = (timestamp) => {
const [year, month, day, hour, minute, second] = [
@@ -50,7 +50,7 @@ const wayBackHandler = async (url) => {
const cdxUrl = `https://web.archive.org/cdx/search/cdx?url=${url}&output=json&fl=timestamp,statuscode,digest,length,offset`;
try {
const { data } = await axios.get(cdxUrl);
const { data } = await httpGet(cdxUrl);
// Check there's data
if (!data || !Array.isArray(data) || data.length <= 1) {

View File

@@ -1,21 +1,21 @@
import axios from 'axios';
import puppeteer from 'puppeteer';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
const getPuppeteerCookies = async (url) => {
const browser = await puppeteer.launch({
headless: 'new',
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
try {
const page = await browser.newPage();
const navigationPromise = page.goto(url, { waitUntil: 'networkidle2' });
const timeoutPromise = new Promise((_, reject) =>
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Puppeteer took too long!')), 3000)
);
await Promise.race([navigationPromise, timeoutPromise]);
return await page.cookies();
return await browser.cookies();
} finally {
await browser.close();
}
@@ -26,19 +26,13 @@ const cookieHandler = async (url) => {
let clientCookies = null;
try {
const response = await axios.get(url, {
withCredentials: true,
maxRedirects: 5,
});
const response = await httpGet(url);
headerCookies = response.headers['set-cookie'];
} catch (error) {
if (error.response) {
return { error: `Request failed with status ${error.response.status}: ${error.message}` };
} else if (error.request) {
return { error: `No response received: ${error.message}` };
} else {
return { error: `Error setting up request: ${error.message}` };
}
return { error: `No response received: ${error.message}` };
}
try {

View File

@@ -1,6 +1,6 @@
import { promises as dnsPromises } from 'dns';
import axios from 'axios';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { parseTarget } from './_common/parse-target.js';
import { upstreamError } from './_common/upstream.js';
@@ -14,8 +14,7 @@ const dnsHandler = async (url) => {
}
const results = await Promise.all(addresses.map(async (address) => {
const hostname = await dnsPromises.reverse(address).catch(() => null);
const dohDirectSupports = await axios
.get(`https://${address}/dns-query`)
const dohDirectSupports = await httpGet(`https://${address}/dns-query`)
.then(() => true)
.catch(() => false);
return { address, hostname, dohDirectSupports };

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { parseTarget } from './_common/parse-target.js';
import { upstreamError } from './_common/upstream.js';
@@ -8,7 +8,7 @@ const hasWaf = (waf) => ({ hasWaf: true, waf });
const firewallHandler = async (url) => {
const { href } = parseTarget(url);
try {
const response = await axios.get(href);
const response = await httpGet(href);
const headers = response.headers;
if (headers['server'] && headers['server'].includes('cloudflare')) {

View File

@@ -1,10 +1,10 @@
import axios from 'axios';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { upstreamError } from './_common/upstream.js';
const headersHandler = async (url) => {
try {
const response = await axios.get(url, {
const response = await httpGet(url, {
validateStatus: (status) => status >= 200 && status < 600,
});
return response.headers;

View File

@@ -1,10 +1,10 @@
import axios from 'axios';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { upstreamError } from './_common/upstream.js';
const httpsSecHandler = async (url) => {
try {
const { headers } = await axios.get(url);
const { headers } = await httpGet(url);
return {
strictTransportPolicy: !!headers['strict-transport-security'],
xFrameOptions: !!headers['x-frame-options'],

View File

@@ -1,70 +0,0 @@
import axios from 'axios';
import unzipper from 'unzipper';
import csv from 'csv-parser';
import fs from 'fs';
import middleware from './_common/middleware.js';
// Should also work with the following sources:
// https://www.domcop.com/files/top/top10milliondomains.csv.zip
// https://tranco-list.eu/top-1m.csv.zip
// https://www.domcop.com/files/top/top10milliondomains.csv.zip
// https://radar.cloudflare.com/charts/LargerTopDomainsTable/attachment?id=525&top=1000000
// https://statvoo.com/dl/top-1million-sites.csv.zip
const FILE_URL = 'https://s3-us-west-1.amazonaws.com/umbrella-static/top-1m.csv.zip';
const TEMP_FILE_PATH = '/tmp/top-1m.csv';
const rankHandler = async (url) => {
let domain = null;
try {
domain = new URL(url).hostname;
} catch (e) {
throw new Error('Invalid URL');
}
// Download and unzip the file if not in cache
if (!fs.existsSync(TEMP_FILE_PATH)) {
const response = await axios({
method: 'GET',
url: FILE_URL,
responseType: 'stream'
});
await new Promise((resolve, reject) => {
response.data
.pipe(unzipper.Extract({ path: '/tmp' }))
.on('close', resolve)
.on('error', reject);
});
}
// Parse the CSV and find the rank
return new Promise((resolve, reject) => {
const csvStream = fs.createReadStream(TEMP_FILE_PATH)
.pipe(csv({
headers: ['rank', 'domain'],
}))
.on('data', (row) => {
if (row.domain === domain) {
csvStream.destroy();
resolve({
domain: domain,
rank: row.rank,
isFound: true,
});
}
})
.on('end', () => {
resolve({
skipped: `Skipping, as ${domain} is not present in the Umbrella top 1M list.`,
domain: domain,
isFound: false,
});
})
.on('error', reject);
});
};
export const handler = middleware(rankHandler);
export default handler;

View File

@@ -1,13 +1,13 @@
import axios from 'axios';
import * as cheerio from 'cheerio';
import urlLib from 'url';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { upstreamError } from './_common/upstream.js';
const linkedPagesHandler = async (url) => {
let response;
try {
response = await axios.get(url);
response = await httpGet(url);
} catch (error) {
return upstreamError(error, 'Linked pages fetch');
}

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { parseTarget } from './_common/parse-target.js';
import { upstreamError } from './_common/upstream.js';
@@ -7,7 +7,7 @@ import { upstreamError } from './_common/upstream.js';
const locationHandler = async (url) => {
const { hostname } = parseTarget(url);
try {
const res = await axios.get(`https://ipapi.co/${hostname}/json/`, { timeout: 5000 });
const res = await httpGet(`https://ipapi.co/${hostname}/json/`, { timeout: 5000 });
if (res.data?.error) return { skipped: res.data.reason || 'Lookup unavailable' };
return res.data;
} catch (error) {

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { requireEnv, upstreamError } from './_common/upstream.js';
const qualityHandler = async (url) => {
@@ -12,7 +12,7 @@ const qualityHandler = async (url) => {
let data;
try {
data = (await axios.get(endpoint)).data;
data = (await httpGet(endpoint)).data;
} catch (error) {
return upstreamError(error, 'Quality check');
}

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { parseTarget } from './_common/parse-target.js';
import { upstreamError } from './_common/upstream.js';
@@ -10,7 +10,7 @@ const rankHandler = async (url) => {
? { auth: { username: TRANCO_USERNAME, password: TRANCO_API_KEY } }
: {};
try {
const response = await axios.get(
const response = await httpGet(
`https://tranco-list.eu/api/ranks/domain/${domain}`,
{ timeout: 5000, ...auth },
);

View File

@@ -1,21 +1,40 @@
import got from 'got';
import middleware from './_common/middleware.js';
import { upstreamError } from './_common/upstream.js';
const MAX_REDIRECTS = 12;
const TIMEOUT_MS = 10000;
const USER_AGENT = 'Mozilla/5.0 (compatible; WebCheck/2.0; +https://web-check.xyz)';
// Walks the redirect chain manually, recording each Location header as got did
const redirectsHandler = async (url) => {
const redirects = [url];
let current = url;
try {
await got(url, {
followRedirect: true,
maxRedirects: 12,
hooks: {
beforeRedirect: [
(_options, response) => { redirects.push(response.headers.location); },
],
},
});
for (let i = 0; i < MAX_REDIRECTS; i++) {
const response = await fetch(current, {
redirect: 'manual',
signal: AbortSignal.timeout(TIMEOUT_MS),
headers: { 'user-agent': USER_AGENT },
});
if (response.status < 300 || response.status >= 400) {
if (response.status >= 400) {
const err = new Error(`HTTP ${response.status}`);
err.response = { status: response.status };
throw err;
}
break;
}
const location = response.headers.get('location');
if (!location) break;
redirects.push(location);
current = new URL(location, current).href;
}
return { redirects };
} catch (error) {
if (error.cause?.code) error.code = error.cause.code;
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
error.code = 'ECONNABORTED';
}
return upstreamError(error, 'Redirect lookup');
}
};

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { parseTarget } from './_common/parse-target.js';
import { upstreamError } from './_common/upstream.js';
@@ -17,7 +17,7 @@ const parseRobotsTxt = (content) => {
const robotsHandler = async (url) => {
const { protocol, hostname } = parseTarget(url);
try {
const res = await axios.get(`${protocol}//${hostname}/robots.txt`);
const res = await httpGet(`${protocol}//${hostname}/robots.txt`);
const parsed = parseRobotsTxt(res.data || '');
return parsed.robots.length
? parsed

View File

@@ -1,5 +1,5 @@
import puppeteer from 'puppeteer-core';
import chromium from 'chrome-aws-lambda';
import chromium from '@sparticuz/chromium';
import { randomUUID } from 'crypto';
import { execFile } from 'child_process';
import { promises as fs } from 'fs';
@@ -46,9 +46,9 @@ const puppeteerScreenshot = async (targetUrl) => {
browser = await puppeteer.launch({
args: [...chromium.args, '--no-sandbox'],
defaultViewport: { width: 800, height: 600 },
executablePath: process.env.CHROME_PATH || '/usr/bin/chromium',
executablePath: process.env.CHROME_PATH || await chromium.executablePath(),
headless: true,
ignoreHTTPSErrors: true,
acceptInsecureCerts: true,
ignoreDefaultArgs: ['--disable-extensions'],
});
const page = await browser.newPage();

View File

@@ -1,8 +1,6 @@
import { URL } from 'url';
import followRedirects from 'follow-redirects';
import middleware from './_common/middleware.js';
const { https } = followRedirects;
import { httpGet } from './_common/http.js';
const SECURITY_TXT_PATHS = [
'/security.txt',
@@ -71,26 +69,15 @@ const securityTxtHandler = async (urlParam) => {
return { isPresent: false };
};
async function fetchSecurityTxt(baseURL, path) {
return new Promise((resolve, reject) => {
const url = new URL(path, baseURL);
https.get(url.toString(), { headers: { 'User-Agent': 'curl/8.0.0' } }, (res) => {
if (res.statusCode === 200) {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
resolve(data);
});
} else {
resolve(null);
}
}).on('error', (err) => {
reject(err);
});
// Returns the file body when the path 200s, else null so the next path is tried
const fetchSecurityTxt = async (baseURL, path) => {
const url = new URL(path, baseURL);
const res = await httpGet(url.toString(), {
headers: { 'User-Agent': 'curl/8.0.0' },
validateStatus: () => true,
});
}
return res.status === 200 ? res.data : null;
};
export const handler = middleware(securityTxtHandler);
export default handler;

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { parseTarget } from './_common/parse-target.js';
import { requireEnv, upstreamError } from './_common/upstream.js';
@@ -9,7 +9,7 @@ const shodanHandler = async (url) => {
if (auth.skipped) return auth;
const { hostname } = parseTarget(url);
try {
const res = await axios.get(
const res = await httpGet(
`https://api.shodan.io/shodan/host/${hostname}?key=${auth.value}`,
{ timeout: 8000 },
);

View File

@@ -1,6 +1,6 @@
import axios from 'axios';
import xml2js from 'xml2js';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { upstreamError } from './_common/upstream.js';
const HARD_TIMEOUT = 5000;
@@ -9,13 +9,13 @@ const MAX_CHILD_SITEMAPS = 25;
// Fetch and parse a sitemap XML
const fetchSitemap = async (sitemapUrl) => {
const res = await axios.get(sitemapUrl, { timeout: HARD_TIMEOUT });
const res = await httpGet(sitemapUrl, { timeout: HARD_TIMEOUT });
return new xml2js.Parser().parseStringPromise(res.data);
};
// Pull a Sitemap: line out of robots.txt
const findSitemapInRobots = async (baseUrl) => {
const robots = await axios.get(`${baseUrl}/robots.txt`, { timeout: HARD_TIMEOUT });
const robots = await httpGet(`${baseUrl}/robots.txt`, { timeout: HARD_TIMEOUT });
for (const line of robots.data.split('\n')) {
if (line.toLowerCase().startsWith('sitemap:')) {
return line.split(/\s+/)[1]?.trim() || null;

View File

@@ -1,12 +1,12 @@
import axios from 'axios';
import * as cheerio from 'cheerio';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { upstreamError } from './_common/upstream.js';
const socialTagsHandler = async (url) => {
let response;
try {
response = await axios.get(url);
response = await httpGet(url);
} catch (error) {
return upstreamError(error, 'Social tags fetch');
}

View File

@@ -1,6 +1,6 @@
import axios from 'axios';
import xml2js from 'xml2js';
import middleware from './_common/middleware.js';
import { httpPost } from './_common/http.js';
import { parseTarget } from './_common/parse-target.js';
import { requireEnv, upstreamError } from './_common/upstream.js';
@@ -8,7 +8,7 @@ const safeBrowsing = async (url) => {
const auth = requireEnv('GOOGLE_CLOUD_API_KEY', 'Google Safe Browsing');
if (auth.skipped) return auth;
try {
const res = await axios.post(
const res = await httpPost(
`https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${auth.value}`,
{
threatInfo: {
@@ -33,7 +33,7 @@ const safeBrowsing = async (url) => {
const urlHaus = async (url) => {
const { hostname } = parseTarget(url);
try {
const res = await axios.post(
const res = await httpPost(
'https://urlhaus-api.abuse.ch/v1/host/',
`host=${hostname}`,
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
@@ -47,7 +47,7 @@ const urlHaus = async (url) => {
const phishTank = async (url) => {
try {
const encoded = Buffer.from(url).toString('base64');
const res = await axios.post(
const res = await httpPost(
`https://checkurl.phishtank.com/checkurl/?url=${encoded}`,
null,
{ headers: { 'User-Agent': 'phishtank/web-check' }, timeout: 3000 },
@@ -63,7 +63,7 @@ const cloudmersive = async (url) => {
const auth = requireEnv('CLOUDMERSIVE_API_KEY', 'Cloudmersive');
if (auth.skipped) return auth;
try {
const res = await axios.post(
const res = await httpPost(
'https://api.cloudmersive.com/virus/scan/website',
`Url=${encodeURIComponent(url)}`,
{

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { parseTarget } from './_common/parse-target.js';
import { upstreamError } from './_common/upstream.js';
@@ -9,7 +9,7 @@ const SSL_LABS = 'https://api.ssllabs.com/api/v3/analyze';
const tlsLabsHandler = async (url) => {
const { hostname } = parseTarget(url);
try {
const res = await axios.get(SSL_LABS, {
const res = await httpGet(SSL_LABS, {
params: { host: hostname, fromCache: 'on', maxAge: 24, all: 'done' },
timeout: 8000,
headers: { 'User-Agent': 'web-check (https://web-check.xyz)' },

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { parseTarget } from './_common/parse-target.js';
import { requireEnv, upstreamError } from './_common/upstream.js';
@@ -9,7 +9,7 @@ const whoisProHandler = async (url) => {
const { hostname } = parseTarget(url);
let data;
try {
const res = await axios.get(
const res = await httpGet(
`https://api.whoapi.com/?domain=${hostname}&r=whois&apikey=${auth.value}`,
{ timeout: 8000 },
);

View File

@@ -1,7 +1,7 @@
import net from 'net';
import psl from 'psl';
import axios from 'axios';
import middleware from './_common/middleware.js';
import { httpPost } from './_common/http.js';
import { createLogger } from './_common/logger.js';
const log = createLogger('whois');
@@ -71,8 +71,8 @@ const fetchFromInternic = async (hostname) => new Promise((resolve, reject) => {
const fetchFromMyAPI = async (hostname) => {
try {
const response = await axios.post('https://whois-api-zeta.vercel.app/', {
domain: hostname
const response = await httpPost('https://whois-api-zeta.vercel.app/', {
domain: hostname,
});
return response.data;
} catch (error) {

View File

@@ -7,7 +7,7 @@ import partytown from '@astrojs/partytown';
import sitemap from '@astrojs/sitemap';
// Adapters
import vercelAdapter from '@astrojs/vercel/serverless';
import vercelAdapter from '@astrojs/vercel';
import netlifyAdapter from '@astrojs/netlify';
import nodeAdapter from '@astrojs/node';
import cloudflareAdapter from '@astrojs/cloudflare';
@@ -22,8 +22,8 @@ const unwrapEnvVar = (varName, fallbackValue) => {
// Determine the deploy target (vercel, netlify, cloudflare, node)
const deployTarget = unwrapEnvVar('PLATFORM', 'node').toLowerCase();
// Determine the output mode (server, hybrid or static)
const output = unwrapEnvVar('OUTPUT', 'hybrid');
// Determine the output mode (static or server). Mixed prerender supported in static mode
const output = unwrapEnvVar('OUTPUT', 'static');
// The FQDN of where the site is hosted (used for sitemaps & canonical URLs)
const site = unwrapEnvVar('SITE_URL', 'https://web-check.xyz');

View File

@@ -4,11 +4,10 @@
"version": "2.0.2",
"homepage": "https://web-check.xyz",
"engines": {
"node": "20.x"
"node": ">=22"
},
"scripts": {
"start": "node server",
"start-pm": "pm2 start server.js -i max",
"build": "astro check && astro build",
"dev:vercel": "PLATFORM='vercel' npx vercel dev",
"dev:netlify": "PLATFORM='netlify' npx netlify dev",
@@ -17,58 +16,63 @@
"dev": "concurrently -c magenta,cyan -n backend,frontend 'yarn dev:api' 'yarn dev:astro'"
},
"dependencies": {
"@astrojs/check": "^0.5.10",
"@astrojs/react": "^3.6.3",
"@astrojs/check": "^0.9.9",
"@astrojs/react": "^5.0.4",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.2.0",
"@fortawesome/free-regular-svg-icons": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/svelte-fontawesome": "^0.2.4",
"@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7",
"astro": "^4.16.19",
"axios": "^1.16.0",
"@sparticuz/chromium": "^148.0.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"astro": "^6.2.2",
"cheerio": "^1.2.0",
"chrome-aws-lambda": "^10.1.0",
"chromium": "^3.0.3",
"connect-history-api-fallback": "^2.0.0",
"cors": "^2.8.5",
"csv-parser": "^3.2.0",
"dotenv": "^16.6.1",
"express": "^4.21.2",
"express-rate-limit": "^7.5.1",
"framer-motion": "^11.18.2",
"got": "^14.6.6",
"pm2": "^5.4.3",
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"express-rate-limit": "^8.5.0",
"framer-motion": "^12.38.0",
"prop-types": "^15.8.1",
"psl": "^1.15.0",
"puppeteer": "^22.15.0",
"puppeteer-core": "^22.15.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"puppeteer": "^24.42.0",
"puppeteer-core": "^24.42.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-masonry-css": "^1.0.16",
"react-router-dom": "^6.30.3",
"react-router-dom": "^7.14.2",
"react-simple-maps": "^3.0.0",
"react-toastify": "^10.0.6",
"recharts": "^2.15.4",
"svelte": "^4.2.20",
"typescript": "^5.9.3",
"unzipper": "^0.11.6",
"url-parse": "^1.5.10",
"react-toastify": "^11.1.0",
"recharts": "^3.8.1",
"svelte": "^5.55.5",
"typescript": "^6.0.3",
"wappalyzer": "^6.10.66",
"xml2js": "^0.6.2"
},
"resolutions": {
"tar-fs": "^2.1.4",
"basic-ftp": "^5.2.0",
"picomatch": "^2.3.2",
"systeminformation": "^5.31.0",
"d3-color": "^3.1.0",
"braces": "^3.0.3",
"wappalyzer/**/ws": "^8.17.1",
"@vercel/routing-utils/path-to-regexp": "^6.3.0",
"uuid": "^14.0.0",
"yaml": "^2.8.3"
},
"devDependencies": {
"@astrojs/cloudflare": "^10.4.2",
"@astrojs/netlify": "^5.5.4",
"@astrojs/node": "^8.3.4",
"@astrojs/cloudflare": "^13.3.1",
"@astrojs/netlify": "^7.0.8",
"@astrojs/node": "^10.0.6",
"@astrojs/partytown": "^2.1.7",
"@astrojs/sitemap": "~3.4.1",
"@astrojs/svelte": "^5.7.3",
"@astrojs/sitemap": "^3.7.2",
"@astrojs/svelte": "^8.1.0",
"@astrojs/ts-plugin": "^1.10.7",
"@astrojs/vercel": "^7.8.2",
"concurrently": "^8.2.2",
"@astrojs/vercel": "^10.0.6",
"concurrently": "^9.2.1",
"nodemon": "^3.1.14",
"sass": "^1.99.0"
},

View File

@@ -1,38 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#9fef00" />
<meta
name="description"
content="All-in-one OSINT tool, for quickly checking a websites data"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Web Check</title>
<script defer data-domain="web-check.as93.net" src="https://no-track.as93.net/js/script.js"></script>
<!-- OpenGraph Social Tags -->
<meta property="og:title" content="Web Check">
<meta property="og:description" content="All-in-one Website OSINT Scanner">
<meta property="og:image" content="/banner.png">
<meta property="og:url" content="https://web-check.xyz">
<meta name="twitter:card" content="summary_large_image">
<!-- DarkReader -->
<meta name="darkreader-lock">
</head>
<body>
<noscript>
<b>Welcome to Web-Check, the free and open source tool for viewing all available information about a website.</b><br />
Get started by entering a URL, and clicking the "Scan" button, or view the code and docs
on <a href="https://github.com/lissy93/web-check">GitHub</a>.<br />
<small>Licensed under MIT, ©️ <a href="https://aliciasykes.com">Alicia Sykes</a> 2023.</small>
<br /><br />
JavaScript is required to continue, please enable it in your browser.
</noscript>
<div id="root"></div>
</body>
</html>

View File

@@ -5,7 +5,6 @@ import cors from 'cors';
import dotenv from 'dotenv';
import express from 'express';
import rateLimit from 'express-rate-limit';
import historyApiFallback from 'connect-history-api-fallback';
// Load environment variables from .env file
dotenv.config();
@@ -56,7 +55,7 @@ const makeLimiterResponseMsg = (retryAfter) => {
// Create rate limiters for each time frame
const limiters = limits.map(limit => rateLimit({
windowMs: limit.timeFrame * 1000,
max: limit.max,
limit: limit.max,
standardHeaders: true,
legacyHeaders: false,
message: { error: makeLimiterResponseMsg(limit.messageTime) }
@@ -181,14 +180,6 @@ if (process.env.DISABLE_GUI && process.env.DISABLE_GUI !== 'false') {
});
}
// Handle SPA routing
app.use(historyApiFallback({
rewrites: [
{ from: new RegExp(`^${API_DIR}/.*$`), to: (context) => context.parsedUrl.path },
{ from: /^.*$/, to: '/index.html' }
]
}));
// Anything left unhandled (which isn't an API endpoint), return a 404
app.use((req, res, next) => {
if (!req.path.startsWith(`${API_DIR}/`)) {

View File

@@ -107,8 +107,8 @@ document.addEventListener('DOMContentLoaded', () => {
<style lang="scss">
@import '@styles/typography.scss';
@import '@styles/global.scss';
@use '@styles/typography.scss';
@use '@styles/global.scss' as *;
.input-container {
position: relative;

View File

@@ -21,7 +21,7 @@ const { links } = Astro.props;
</div>
<style lang="scss">
@import '@styles/global.scss';
@use '@styles/global.scss' as *;
.button-wrap {
margin: 3rem auto;
display: flex;

View File

@@ -63,7 +63,7 @@ import Screenshots from "./Screenshots.astro"
</script>
<style lang="scss">
@import '@styles/global.scss';
@use '@styles/global.scss' as *;
.hero {
display: flex;
justify-content: space-around;

View File

@@ -13,8 +13,8 @@ const durationVariance = 1; // Variance for randomization to append to travel in
const delayBase = 500; // Base delay for meteor to respawn in milliseconds
const delayVariance = 1500; // Variance for randomization to append to respawn in milliseconds
const tailDuration = 0.25; // Duration for meteor tail to retract in seconds
const headEasing = [0.8, 0.6, 1, 1]; // Easing for meteor head
const tailEasing = [0.5, 0.6, 0.6, 1]; // Easing for meteor tail
const headEasing = [0.8, 0.6, 1, 1] as const; // Easing for meteor head
const tailEasing = [0.5, 0.6, 0.6, 1] as const; // Easing for meteor tail
const MeteorContainer = styled(motion.div)`
position: absolute;
@@ -135,7 +135,7 @@ const WebCheckHomeBackground = () => {
<StyledRect fill="url(#dot-pattern)" />
</StyledSvg>
{meteors.map(({ id, column, startRow, endRow, duration, tailVisible, animationStage, opacity }) => {
{meteors.map(({ id, column, startRow, endRow, duration, tailVisible, animationStage }) => {
return (
<MeteorContainer
key={id}

View File

@@ -54,7 +54,7 @@ const screenshots = [
</div>
<style lang="scss">
@import '@styles/global.scss';
@use '@styles/global.scss' as *;
.container {
display: flex;

View File

@@ -23,7 +23,7 @@ const ctaImageSrc = 'https://i.ibb.co/5jJ4bzZ/terminal-trove-cta.png';
</div>
<style lang="scss">
@import '@styles/global.scss';
@use '@styles/global.scss' as *;
.sponsored-but-dont-block {
background: var(--text-color);
color: var(--background);

View File

@@ -27,7 +27,7 @@ const currentYear = new Date().getFullYear();
</footer>
<style lang="scss">
@import '@styles/global.scss';
@use '@styles/global.scss' as *;
footer {
margin-top: 1rem;
background: var(--primary);

View File

@@ -1,5 +1,5 @@
---
import { ViewTransitions } from 'astro:transitions'
import { ClientRouter } from 'astro:transitions'
import MetaTags from '@layouts/MetaTags.astro';
import '@styles/typography.scss';
@@ -23,7 +23,7 @@ interface Props {
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<ViewTransitions />
<ClientRouter />
<MetaTags {...Astro.props } />
<slot name="head" />
<link href="/fonts/Hubot-Sans/Hubot-Sans.woff2"

View File

@@ -92,13 +92,13 @@ const makeBreadcrumbs = () => {
<!-- Non-tracking hit counter -->
{analytics.enable && (
<script defer data-domain={analytics.domain} src={analytics.script}></script>
<script is:inline defer data-domain={analytics.domain} src={analytics.script}></script>
)}
<!-- Schema.org markup for Google -->
{breadcrumbs && (
<script type="application/ld+json" set:html={JSON.stringify(makeBreadcrumbs())} />
<script is:inline type="application/ld+json" set:html={JSON.stringify(makeBreadcrumbs())} />
)}
{customSchemaJson && (
<script type="application/ld+json" set:html={JSON.stringify(customSchemaJson)} />
<script is:inline type="application/ld+json" set:html={JSON.stringify(customSchemaJson)} />
)}

View File

@@ -2,7 +2,6 @@
import BaseLayout from '@layouts/Base.astro';
import NavBar from '@components/scafold/Nav.astro';
import Footer from '@components/scafold/Footer.astro';
import Icon from '@components/molecules/Icon.svelte';
---
@@ -33,7 +32,7 @@ import Icon from '@components/molecules/Icon.svelte';
<style lang="scss">
@import '@styles/global.scss';
@use '@styles/global.scss' as *;
.web-check-page {
padding-top: 2rem;
margin: 0 auto;

View File

@@ -10,18 +10,11 @@ const isBossServer = import.meta.env.BOSS_SERVER === true;
const disableEverything = import.meta.env.VITE_DISABLE_EVERYTHING === true;
// Redirect strait to /check or /check/:url if running as self-hosted instance
if (!isBossServer) {
const searchUrl = new URLSearchParams(new URL(Astro.request.url).search).get('url');
const redirectUrl = searchUrl ? `/check/${encodeURIComponent(searchUrl)}` : '/check';
Astro.redirect(redirectUrl);
}
---
<BaseLayout>
<Fragment slot="head">
{!isBossServer && (<meta http-equiv="refresh" content="0; url=/check" />)}
<!-- {!isBossServer && (<meta http-equiv="refresh" content="0; url=/check" />)} -->
</Fragment>
{ disableEverything && <TempDisabled />}
<main>
@@ -33,7 +26,7 @@ if (!isBossServer) {
</BaseLayout>
<style lang="scss">
@import '@styles/global.scss';
@use '@styles/global.scss' as *;
main {
min-height: 100vh;
padding: 2rem;

View File

@@ -91,7 +91,7 @@ const cardData = [
<style lang="scss">
@import '@styles/global.scss';
@use '@styles/global.scss' as *;
.web-check-page {
padding-top: 2rem;
margin: 0 auto;

View File

@@ -2,7 +2,6 @@
import BaseLayout from '@layouts/Base.astro';
import NavBar from '@components/scafold/Nav.astro';
import Footer from '@components/scafold/Footer.astro';
import Icon from '@components/molecules/Icon.svelte';
---
@@ -90,7 +89,7 @@ import Icon from '@components/molecules/Icon.svelte';
<style lang="scss">
@import '@styles/global.scss';
@use '@styles/global.scss' as *;
.web-check-page {
padding-top: 2rem;
margin: 0 auto;

View File

@@ -41,7 +41,7 @@ import Footer from '@components/scafold/Footer.astro';
</script>
<style lang="scss">
@import '@styles/global.scss';
@use '@styles/global.scss' as *;
main {
height: 100vh;
padding-top: 2rem;

View File

@@ -1,7 +1,7 @@
/* Global Stylesheet */
@import './colors.scss';
@import './media-queries.scss';
@import './typography.scss';
@forward './colors';
@forward './media-queries';
@forward './typography';
/* CSS Reset - Normalize dimensions and spacing across browsers */
*,

View File

@@ -1,6 +1,5 @@
import styled from '@emotion/styled';
import type { ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { StyledCard } from 'web-check-live/components/Form/Card';
import Heading from 'web-check-live/components/Form/Heading';

View File

@@ -156,7 +156,7 @@ export const ExpandableRow = (props: RowProps) => {
</span>
{ row.plaintext && <PlainText>{row.plaintext}</PlainText> }
{ row.listResults && (<List>
{row.listResults.map((listItem: string, listIndex: number) => (
{row.listResults.map((listItem: string) => (
<li key={listItem}>{snip(listItem)}</li>
))}
</List>)}

View File

@@ -64,7 +64,7 @@ const MalwareCard = (props: {data: any, title: string, actionButtons: any }): JS
{urlHaus.urls && (
<Expandable>
<summary>Expand Results</summary>
{ urlHaus.urls.map((urlResult: any, index: number) => {
{ urlHaus.urls.map((urlResult: any) => {
const rows = [
{ lbl: 'ID', val: urlResult.id },
{ lbl: 'Status', val: urlResult.url_status },

View File

@@ -1,4 +1,4 @@
import React, { Component, type ErrorInfo, type ReactNode } from "react";
import { 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';

View File

@@ -3,18 +3,11 @@ interface Props {
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} />);
const Flag = ({ countryCode, width }: Props) => {
const dimensions = `${width}x${width * 0.75}`;
const country = countryCode.toLowerCase();
const src = `https://flagcdn.com/${dimensions}/${country}.png`;
return (<img src={src} alt={countryCode} />);
}
export default Flag;

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import { 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';

View File

@@ -1,5 +1,4 @@
import { BrowserRouter } from "react-router-dom";
import { StaticRouter } from "react-router-dom/server";
import { BrowserRouter, StaticRouter } from "react-router-dom";
import App from "./App.tsx";
export default ({ pathname }: { pathname: string }) => (

9
src/web-check-live/typings/jsx.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import type { JSX as ReactJSX } from 'react'
declare global {
namespace JSX {
type Element = ReactJSX.Element
}
}
export {}

View File

@@ -303,7 +303,7 @@ const About = (): JSX.Element => {
<Heading as="h2" size="medium" color={colors.primary}>Support Us</Heading>
<Section>
{supportUs.map((para, index: number) => (<p dangerouslySetInnerHTML={{__html: para}} />))}
{supportUs.map((para) => (<p dangerouslySetInnerHTML={{__html: para}} />))}
</Section>
<Heading as="h2" size="medium" color={colors.primary}>Terms & Info</Heading>
@@ -319,7 +319,7 @@ const About = (): JSX.Element => {
<hr />
<Heading as="h3" size="small" color={colors.primary}>Fair Use</Heading>
<ul>
{fairUse.map((para, index: number) => (<li>{para}</li>))}
{fairUse.map((para) => (<li>{para}</li>))}
</ul>
<hr />
<Heading as="h3" size="small" color={colors.primary}>Privacy</Heading>

View File

@@ -1,5 +1,5 @@
import styled from '@emotion/styled';
import { type ChangeEvent, type FormEvent, useState, useEffect } from 'react';
import { type ChangeEvent, type SyntheticEvent, useState, useEffect } from 'react';
import { Link, useNavigate, useLocation, type NavigateOptions } from 'react-router-dom';
import Heading from 'web-check-live/components/Form/Heading';
@@ -190,7 +190,7 @@ const Home = (): JSX.Element => {
}
};
const formSubmitEvent = (event: FormEvent<HTMLFormElement>) => {
const formSubmitEvent = (event: SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
submit();
}

9610
yarn.lock

File diff suppressed because it is too large Load Diff