diff --git a/api/carbon.js b/api/carbon.js index a08d530..97531c4 100644 --- a/api/carbon.js +++ b/api/carbon.js @@ -7,25 +7,23 @@ const log = createLogger('carbon'); const TIMEOUT = 8000; const MAX_BYTES = 10 * 1024 * 1024; -// Sustainable Web Design model v3 constants, matches websitecarbon.com formula -const KWH_PER_GB = 0.81; +// Sustainable Web Design model v4 constants, matches websitecarbon.com formula +const KWH_PER_GB = 0.3; const FIRST_VISIT = 0.25; const RETURN_VISIT = 0.75; const RETURN_DATA_PCT = 0.02; -const GRID_INTENSITY = 442; +const GRID_INTENSITY = 494; const RENEWABLE_INTENSITY = 50; const LITRES_PER_GRAM = 0.5562; -// Reference median grams CO2 per visit, drawn from websitecarbon's published average. -// Used to estimate a percentile rank since we lack their measured-sites dataset -const REFERENCE_MEDIAN_GRAMS = 0.8; +// Median CO2 for an HTML-only fetch at HTTP Archive's ~30 KB median +const REFERENCE_MEDIAN_GRAMS = 0.001; -// Approximate percentile via log2 distance from the reference median. -// 1 doubling above median drops 25 points; clamp to [1, 99] +// Percentile rank via log2 distance from the median, clamped to [1, 95] const estimateCleanerThan = (grams) => { if (!grams || grams <= 0) return 0; - const pct = 50 - 25 * Math.log2(grams / REFERENCE_MEDIAN_GRAMS); - return Math.max(1, Math.min(99, Math.round(pct))); + const pct = 50 - 15 * Math.log2(grams / REFERENCE_MEDIAN_GRAMS); + return Math.max(1, Math.min(95, Math.round(pct))); }; // Stream the response, cap at MAX_BYTES so huge pages can't blow memory or time diff --git a/api/rank.js b/api/rank.js index b44c874..3625fa5 100644 --- a/api/rank.js +++ b/api/rank.js @@ -4,19 +4,24 @@ import { parseTarget } from './_common/parse-target.js'; import { upstreamError } from './_common/upstream.js'; const rankHandler = async (url) => { - const { hostname: domain } = parseTarget(url); + const { hostname } = parseTarget(url); const { TRANCO_USERNAME, TRANCO_API_KEY } = process.env; const auth = TRANCO_API_KEY ? { auth: { username: TRANCO_USERNAME, password: TRANCO_API_KEY } } : {}; + const fallback = hostname.startsWith('www.') ? hostname.slice(4) : `www.${hostname}`; + // Tranco indexes only one variant per site, so try as-is, then toggle www + const lookup = (domain) => httpGet(`https://tranco-list.eu/api/ranks/domain/${domain}`, auth); try { - const response = await httpGet(`https://tranco-list.eu/api/ranks/domain/${domain}`, auth); - if (!response.data?.ranks?.length) { - return { - skipped: `${domain} isn't ranked in the top 1 million sites yet`, - }; + const first = await lookup(hostname); + if (first.data?.ranks?.length) return first.data; + try { + const second = await lookup(fallback); + if (second.data?.ranks?.length) return second.data; + } catch { + // Ignore fallback failures (e.g. rate limit) and accept the empty first result } - return response.data; + return { skipped: `${hostname} isn't ranked in the top 1 million sites yet` }; } catch (error) { return upstreamError(error, 'Tranco rank lookup'); } diff --git a/api/subdomains.js b/api/subdomains.js index 5b03c39..e42b63d 100644 --- a/api/subdomains.js +++ b/api/subdomains.js @@ -45,10 +45,15 @@ const subdomainsHandler = async (url) => { params: { q: `%.${domain}`, output: 'json' }, headers: { Accept: 'application/json' }, }); - const rows = Array.isArray(res.data) ? res.data : []; - const all = collectSubdomains(rows, domain); + if (!Array.isArray(res.data)) { + return { error: 'Certificate Transparency lookup returned unexpected data, please retry' }; + } + const all = collectSubdomains(res.data, domain); if (!all.length) { - return { skipped: `No subdomains found for ${domain} in Certificate Transparency logs` }; + return { + skipped: `No subdomains found for ${domain} in Certificate Transparency logs`, + retryable: true, + }; } return { domain, diff --git a/package.json b/package.json index 073ceed..10d53bc 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "typecheck": "astro check", "lint": "eslint --config .config/eslint.config.js .", "format:check": "prettier --check --ignore-unknown '!yarn.lock' '!**/*.md' .", - "format:fix": "prettier --write --ignore-unknown '!yarn.lock' '!**/*.md' ." + "format:fix": "prettier --write --ignore-unknown '!yarn.lock' '!**/*.md' .", + "hold-my-beer": "yarn format:fix && yarn lint && yarn typecheck" }, "dependencies": { "@astrojs/check": "^0.9.9", diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png index 818bfea..126f0fe 100644 Binary files a/public/android-chrome-192x192.png and b/public/android-chrome-192x192.png differ diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png index 310e195..35ba8e8 100644 Binary files a/public/android-chrome-512x512.png and b/public/android-chrome-512x512.png differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index b00150d..ee7e75f 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png index 4c7a938..d422a7c 100644 Binary files a/public/favicon-16x16.png and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png index cba0476..8055a30 100644 Binary files a/public/favicon-32x32.png and b/public/favicon-32x32.png differ diff --git a/public/manifest.json b/public/manifest.json index 3febec9..4babde7 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,20 +1,45 @@ { + "name": "Web Check", "short_name": "Web Check", - "name": "Lissy93/Web-Check", + "description": "Web Check is the all-in-one OSINT and security tool, for revealing the inner workings of any website", + "id": "/", + "start_url": "/", + "scope": "/", + "display": "standalone", + "theme_color": "#d6fb41", + "background_color": "#111211", "icons": [ { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" + "src": "/favicon.svg", + "type": "image/svg+xml", + "sizes": "any" }, { - "src": "apple-touch-icon.png", + "src": "/favicon-16x16.png", + "type": "image/png", + "sizes": "16x16" + }, + { + "src": "/favicon-32x32.png", + "type": "image/png", + "sizes": "32x32" + }, + { + "src": "/apple-touch-icon.png", "type": "image/png", "sizes": "180x180" + }, + { + "src": "/android-chrome-192x192.png", + "type": "image/png", + "sizes": "192x192", + "purpose": "any" + }, + { + "src": "/android-chrome-512x512.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "any" } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#9fef00", - "background_color": "#141d2b" + ] } diff --git a/src/client/components/Form/Nav.tsx b/src/client/components/Form/Nav.tsx index 749b8ed..ae96729 100644 --- a/src/client/components/Form/Nav.tsx +++ b/src/client/components/Form/Nav.tsx @@ -20,7 +20,7 @@ const Nav = (props: { children?: ReactNode }) => { return (
- Web Check Icon + Web Check Icon Web Check diff --git a/src/client/components/misc/FancyBackground.tsx b/src/client/components/misc/FancyBackground.tsx index fa9c2d5..5415d69 100644 --- a/src/client/components/misc/FancyBackground.tsx +++ b/src/client/components/misc/FancyBackground.tsx @@ -315,12 +315,16 @@ const FancyBackground = (): JSX.Element => { useEffect(() => { App.setup(); App.draw(); - - var frame = function () { - App.evolve(); + const frameInterval = 25; + let lastTime = 0; + const frame = (now: number) => { + if (now - lastTime >= frameInterval) { + App.evolve(); + lastTime = now; + } requestAnimationFrame(frame); }; - frame(); + requestAnimationFrame(frame); }, [App]); return ( diff --git a/src/client/components/misc/Footer.tsx b/src/client/components/misc/Footer.tsx index fbd97ef..8114890 100644 --- a/src/client/components/misc/Footer.tsx +++ b/src/client/components/misc/Footer.tsx @@ -51,7 +51,7 @@ const Footer = (props: { isFixed?: boolean }): JSX.Element => { Web-Check is licensed under MIT - - © Alicia Sykes 2023 + © Alicia Sykes 2026 ); diff --git a/src/client/components/misc/Loader.tsx b/src/client/components/misc/Loader.tsx index 48248cd..67290b0 100644 --- a/src/client/components/misc/Loader.tsx +++ b/src/client/components/misc/Loader.tsx @@ -8,14 +8,15 @@ const LoaderContainer = styled(StyledCard)` margin: 0 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; + transition: + height 0.3s ease-in-out, + opacity 0.3s ease-in-out; p.loadTimeInfo { text-align: center; margin: 0; @@ -29,14 +30,9 @@ const LoaderContainer = styled(StyledCard)` height: 0; overflow: hidden; opacity: 0; - margin: -1rem 0 0 0; - padding: 0; - svg { - width: 0; - } - h4 { - font-size: 0; - } + margin-top: -1rem; + padding-top: 0; + padding-bottom: 0; } &.hide { display: none; diff --git a/src/client/jobs/registry.ts b/src/client/jobs/registry.ts index b050152..ca36214 100644 --- a/src/client/jobs/registry.ts +++ b/src/client/jobs/registry.ts @@ -102,11 +102,11 @@ const fetchAndPoll = (path: string) => }), ); -// Re-run on transient errors, returning the last error if all attempts fail +// Re-run on transient errors or when the server hints `retryable: true` const fetchAndRetry = (path: string) => retrying( path, - (r) => !!r?.error, + (r) => !!r?.error || !!r?.retryable, 3, 2000, (last) => last, @@ -207,7 +207,6 @@ export const jobs: JobSpec[] = [ { id: 'tls-labs', expectedAddressTypes: [...URL_ONLY], - noClientTimeout: true, cards: [ card('tls-security-audit', 'TLS Security Audit', ['security'], TlsSecurityAuditCard), card('tls-client-compat', 'TLS Client Compatibility', ['security'], TlsClientCompatCard), diff --git a/src/client/views/About.tsx b/src/client/views/About.tsx index 723ad5e..04649e2 100644 --- a/src/client/views/About.tsx +++ b/src/client/views/About.tsx @@ -121,12 +121,11 @@ const SponsorshipContainer = styled.div` } `; -const makeAnchor = (title: string): string => { - return title +const makeAnchor = (title: string): string => + title .toLowerCase() .replace(/[^\w\s]|_/g, '') .replace(/\s+/g, '-'); -}; const About = (): JSX.Element => { const location = useLocation(); diff --git a/src/client/views/Home.tsx b/src/client/views/Home.tsx index 37c295b..435e616 100644 --- a/src/client/views/Home.tsx +++ b/src/client/views/Home.tsx @@ -43,42 +43,15 @@ const SponsorCard = styled.div` box-shadow: 4px 4px 0px ${colors.bgShadowColor}; border-radius: 8px; padding: 1rem; - z-index: 5; margin: 1rem; width: calc(100% - 2rem); max-width: 60rem; z-index: 2; - .inner { - display: flex; - justify-content: space-between; - flex-wrap: wrap; - gap: 1rem; - p { - margin: 0.25rem 0; - } + p { + margin: 0.25rem 0; } a { - color: ${colors.textColor}; - } - img { - border-radius: 0.25rem; - box-shadow: 2px 2px 0px ${colors.fgShadowColor}; - transition: box-shadow 0.2s; - margin: 0 auto; - display: block; - width: 200px; - &:hover { - box-shadow: 4px 4px 0px ${colors.fgShadowColor}; - } - &:active { - box-shadow: -2px -2px 0px ${colors.fgShadowColor}; - } - } - .cta { - font-size: 0.78rem; - a { - color: ${colors.primary}; - } + color: ${colors.primary}; } `; @@ -124,14 +97,23 @@ const SiteFeaturesWrapper = styled(StyledCard)` font-size: 0.9rem; color: ${colors.textColor}; li { + position: relative; margin: 0.1rem 0; - text-indent: -1.2rem; + padding-left: 1.2rem; break-inside: avoid-column; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } li:before { content: '✓'; color: ${colors.primary}; - margin-right: 0.5rem; + position: absolute; + left: 0; + } + li:not(:last-child) a { + color: inherit; + text-decoration: none; } } a { @@ -139,6 +121,13 @@ const SiteFeaturesWrapper = styled(StyledCard)` } `; +// Build a URL-safe anchor id from a section title (e.g. "IP Info" -> "ip-info") +const makeAnchor = (title: string): string => + title + .toLowerCase() + .replace(/[^\w\s]|_/g, '') + .replace(/\s+/g, '-'); + const Home = (): JSX.Element => { const defaultPlaceholder = 'e.g. duck.com'; const [userInput, setUserInput] = useState(''); @@ -191,23 +180,6 @@ const Home = (): JSX.Element => { submit(); }; - // const findIpAddress = () => { - // setUserInput(''); - // setPlaceholder('Looking up your IP...'); - // setInputDisabled(true); - // fetch('https://ipapi.co/json/') - // .then(function(response) { - // response.json().then(jsonData => { - // setUserInput(jsonData.ip); - // setPlaceholder(defaultPlaceholder); - // setInputDisabled(true); - // }); - // }) - // .catch(function(error) { - // console.log('Failed to get IP address :\'(', error) - // }); - // }; - return ( @@ -238,43 +210,16 @@ const Home = (): JSX.Element => { - Sponsored by + Enjoying Web Check? - + . Every bit genuinely helps, thank you +

@@ -283,7 +228,11 @@ const Home = (): JSX.Element => {
    {docs.map((doc, index) => ( -
  • {doc.title}
  • +
  • + + {doc.title} + +
  • ))}
  • + more! diff --git a/src/layouts/MetaTags.astro b/src/layouts/MetaTags.astro index 772b15d..3527df1 100644 --- a/src/layouts/MetaTags.astro +++ b/src/layouts/MetaTags.astro @@ -16,7 +16,10 @@ const siteInfo = { titleLong: 'Web Check - X-Ray Vision for any Website', description: 'Web Check is the all-in-one OSINT and security tool, for revealing the inner workings of any website', - keywords: '', + keywords: + 'web check, OSINT, website security, security audit, DNS lookup, SSL check, ' + + 'tech stack detection, website analyzer, web tools, open source', + themeColor: '#c1fb41', author: 'Alicia Sykes', twitter: '@Lissy_Sykes', site: import.meta.env.SITE_URL || 'https://web-check.xyz', @@ -27,17 +30,16 @@ const siteInfo = { }, }; -// Set values for the meta tags, from props or defaults -const { - title = siteInfo.title, - description = siteInfo.description, - keywords = siteInfo.keywords, - breadcrumbs, - customSchemaJson, -} = Astro.props; +// Resolve per-page values, falling back to siteInfo defaults +const { title, description, keywords, breadcrumbs, customSchemaJson } = Astro.props; +const pageTitle = title ?? siteInfo.title; +const ogTitle = title ?? siteInfo.titleLong; +const pageDescription = description ?? siteInfo.description; +const pageKeywords = keywords ?? siteInfo.keywords; -// Set non-customizable values for meta tags, from the siteInfo -const { site, author, twitter, analytics, titleLong } = siteInfo; +const { site, author, twitter, analytics, themeColor } = siteInfo; +const canonical = new URL(Astro.url.pathname, site).href; +const ogImage = `${site}/banner.png`; // Given a map of breadcrumbs, return the JSON-LD for the BreadcrumbList schema const makeBreadcrumbs = () => { @@ -56,10 +58,12 @@ const makeBreadcrumbs = () => { --- -{title} - - +{pageTitle} + +{pageKeywords && } + + @@ -69,23 +73,23 @@ const makeBreadcrumbs = () => { - + + + - + - - - - - + + + + - - - - - + + + + diff --git a/src/pages/check/[...target].astro b/src/pages/check/[...target].astro index b4767fb..ee5b768 100644 --- a/src/pages/check/[...target].astro +++ b/src/pages/check/[...target].astro @@ -14,9 +14,12 @@ if (searchUrl) { const target = normalizeAddress(searchUrl); if (target) Astro.redirect(`/check/${target}`); } + +const { target } = Astro.params; +const pageTitle = target ? `${target} | Web Check` : 'Web Check'; --- - +