import { Bytes, IncomingRequestCf, ModuleWorkerContext } from './deps_worker.ts'; import { WEBTAIL_APP_B64, WEBTAIL_APP_HASH } from './webtail_data.ts'; import { FAVICON_SVG, FAVICON_ICO_B64, FAVICON_VERSION } from './favicons.ts'; import { TWITTER_IMAGE_VERSION, TWITTER_IMAGE_PNG_B64 } from './twitter.ts'; import { Material } from './material.ts'; import { AppManifest } from './app_manifest.d.ts'; export default { async fetch(request: IncomingRequestCf, env: WorkerEnv, _ctx: ModuleWorkerContext): Promise { const url = new URL(request.url); if (url.pathname === '/') { const { version, flags, twitter } = env; const headers = computeHeaders('text/html; charset=utf-8'); return new Response(computeHtml(url, { version, flags, twitter }), { headers }); } else if (url.pathname === computeAppJsPath()) { return computeAppResponse(); } else if (url.pathname.startsWith('/fetch/')) { const fetchUrlStr = 'https://' + url.pathname.substring('/fetch/'.length); const fetchUrl = new URL(fetchUrlStr); const { method } = request; if (isFetchAllowed(method, fetchUrl)) { const headers = [...request.headers].filter(v => !v[0].startsWith('cf-')); const body = undefined; return await fetch(fetchUrlStr, { method, headers, body }); } throw new Response(`Unable to fetch ${fetchUrl}`, { status: 400 }); } else if (url.pathname === FAVICON_SVG_PATHNAME) { const headers = computeHeaders(SVG_MIME_TYPE, { immutable: true }); return new Response(FAVICON_SVG, { headers }); } else if (url.pathname === '/favicon.ico' || url.pathname === FAVICON_ICO_PATHNAME) { const headers = computeHeaders('image/x-icon', { immutable: url.pathname.includes(`${FAVICON_VERSION}.`) }); return new Response(Bytes.ofBase64(FAVICON_ICO_B64).array(), { headers }); } else if (url.pathname === MANIFEST_PATHNAME) { const headers = computeHeaders('application/manifest+json', { immutable: true }); return new Response(JSON.stringify(computeManifest(), undefined, 2), { headers }); } else if (url.pathname === TWITTER_IMAGE_PNG_PATHNAME) { const headers = computeHeaders('image/png', { immutable: true }); return new Response(Bytes.ofBase64(TWITTER_IMAGE_PNG_B64).array(), { headers }); } else if (url.pathname === '/robots.txt') { const headers = computeHeaders('text/plain; charset=utf-8'); return new Response('User-agent: *\nDisallow:\n', { headers }); } const headers = computeHeaders('text/html; charset=utf-8'); return new Response(NOT_FOUND, { status: 404, headers }); } }; export interface WorkerEnv { readonly version?: string; readonly flags?: string; readonly twitter?: string; } // const MANIFEST_VERSION = '1'; const FAVICON_SVG_PATHNAME = `/favicon.${FAVICON_VERSION}.svg`; const FAVICON_ICO_PATHNAME = `/favicon.${FAVICON_VERSION}.ico`; const MANIFEST_PATHNAME = `/app.${MANIFEST_VERSION}.webmanifest`; const TWITTER_IMAGE_PNG_PATHNAME = `/og-image.${TWITTER_IMAGE_VERSION}.png`; const SVG_MIME_TYPE = 'image/svg+xml'; function computeManifest(): AppManifest { const name = 'Webtail'; return { 'short_name': name, name: `${name} for Cloudflare Workers`, description: 'View live requests and logs from Cloudflare Workers from the comfort of your browser.', icons: [ { src: FAVICON_SVG_PATHNAME, type: SVG_MIME_TYPE, }, ], 'theme_color': Material.primaryColor900Hex, 'background_color': Material.backgroundColorHex, display: 'standalone', start_url: '/', lang: 'en-US', dir: 'ltr', }; } function computeHeaders(contentType: string, opts: { immutable?: boolean } = {}) { const { immutable } = opts; const headers = new Headers(); headers.set('Content-Type', contentType); if (immutable) headers.set('Cache-Control', 'public, max-age=604800, immutable'); return headers; } function isFetchAllowed(method: string, url: URL): boolean { return /^(GET|POST)$/.test(method) && url.origin === 'https://api.cloudflare.com' && url.pathname.startsWith('/client/v4/accounts/') && url.pathname.includes('/workers/scripts'); } function computeAppJsPath(): string { return `/app.${WEBTAIL_APP_HASH}.js`; } function computeAppResponse(): Response { const array = Bytes.ofBase64(WEBTAIL_APP_B64).array(); return new Response(array, { headers: computeHeaders('text/javascript; charset=utf-8', { immutable: true }) }); } function encodeHtml(value: string): string { return value.replace(/&/g, '&') .replace(/\"/g, '"') .replace(/'/g, ''') .replace(//g, '>'); } const ICONS_MANIFEST_AND_THEME_COLORS = ` `; const COMMON_STYLES = ` body { font-family: ${Material.sansSerifFontFamily}; background-color: ${Material.backgroundColorHex}; color: red; /* to catch non-explicit text colors */ text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; margin: 0; padding: 0; box-sizing: border-box; } #centered { display: flex; align-items: center; justify-content: center; height: 100vh; /* body2 */ font-size: 0.875rem; letter-spacing: 0.01786rem; font-weight: normal; line-height: 1.25rem; /* medium-emphasis text */ color: rgba(255, 255, 255, 0.60); } `; const NOT_FOUND = ` Not found ${ICONS_MANIFEST_AND_THEME_COLORS}
Not found
`; function computeHtml(url: URL, staticData: Record) { const { name, description } = computeManifest(); const { twitter } = staticData; const appJsPath = computeAppJsPath(); return ` ${encodeHtml(name)} ${twitter ? `` : ''} ${ICONS_MANIFEST_AND_THEME_COLORS}
${encodeHtml(name)} requires a current version of:
`; }