diff --git a/package.json b/package.json index 608026c..140a0f0 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@api.global/typedrequest": "^3.0.23", "@api.global/typedrequest-interfaces": "^3.0.19", "@api.global/typedsocket": "^3.0.1", + "@cloudflare/workers-types": "^4.20240502.0", "@design.estate/dees-comms": "^1.0.24", "@push.rocks/lik": "^6.0.15", "@push.rocks/smartchok": "^1.0.34", @@ -68,7 +69,9 @@ "@push.rocks/smartjson": "^5.0.19", "@push.rocks/smartlog": "^3.0.3", "@push.rocks/smartlog-destination-devtools": "^1.0.10", + "@push.rocks/smartlog-interfaces": "^3.0.0", "@push.rocks/smartmanifest": "^2.0.2", + "@push.rocks/smartmatch": "^2.0.0", "@push.rocks/smartmime": "^1.0.5", "@push.rocks/smartopen": "^2.0.0", "@push.rocks/smartpath": "^5.0.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 240ff25..1a2d3e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: '@api.global/typedsocket': specifier: ^3.0.1 version: 3.0.1 + '@cloudflare/workers-types': + specifier: ^4.20240502.0 + version: 4.20240502.0 '@design.estate/dees-comms': specifier: ^1.0.24 version: 1.0.24 @@ -44,9 +47,15 @@ dependencies: '@push.rocks/smartlog-destination-devtools': specifier: ^1.0.10 version: 1.0.10 + '@push.rocks/smartlog-interfaces': + specifier: ^3.0.0 + version: 3.0.0 '@push.rocks/smartmanifest': specifier: ^2.0.2 version: 2.0.2 + '@push.rocks/smartmatch': + specifier: ^2.0.0 + version: 2.0.0 '@push.rocks/smartmime': specifier: ^1.0.5 version: 1.0.6 @@ -232,6 +241,10 @@ packages: regenerator-runtime: 0.14.1 dev: false + /@cloudflare/workers-types@4.20240502.0: + resolution: {integrity: sha512-OB1jIyPOzyOcuZFHWhsQnkRLN6u8+jmU9X3T4KZlGgn3Ivw8pBiswhLOp+yFeChR3Y4/5+V0hPFRko5SReordg==} + dev: false + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 410f9d9..8bcaf65 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@api.global/typedserver', - version: '3.0.31', + version: '3.0.32', description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.' } diff --git a/ts_edgeworker/00_commitinfo_data.ts b/ts_edgeworker/00_commitinfo_data.ts new file mode 100644 index 0000000..2e899f5 --- /dev/null +++ b/ts_edgeworker/00_commitinfo_data.ts @@ -0,0 +1,8 @@ +/** + * autocreated commitinfo by @pushrocks/commitinfo + */ +export const commitinfo = { + name: 'cloudflare-workers', + version: '1.0.192', + description: 'cloudflare-workers' +} diff --git a/ts_edgeworker/analytics/analyzer.ts b/ts_edgeworker/analytics/analyzer.ts new file mode 100644 index 0000000..9d5ae23 --- /dev/null +++ b/ts_edgeworker/analytics/analyzer.ts @@ -0,0 +1,83 @@ + +import type { EdgeWorker } from '../classes.edgeworker.js'; +import type { WorkerEvent } from '../classes.workerevent.js'; +import * as plugins from '../plugins.js'; +import { SmartlogDestination } from './smartlog.js'; + +export interface IAnalyticsData { + requestAgent: string; + requestUrl: string; + requestMethod: string; + requestStartTime: number; + responseStatus: number; + responseEndTime: number; +} + +export class Analyzer { + cworkerEventRef: WorkerEvent; + + public data: IAnalyticsData = { + requestAgent: 'unknown', + requestMethod: 'unknown', + requestUrl: 'unknown', + requestStartTime: 0, + responseStatus: 0, + responseEndTime: 0, + }; + + public finishedDeferred = plugins.smartpromise.defer(); + + constructor(cworkerEventRefArg: WorkerEvent) { + this.cworkerEventRef = cworkerEventRefArg; + this.smartlog.addLogDestination(new SmartlogDestination(this.cworkerEventRef.options.edgeWorkerRef)); + } + public smartlog = new plugins.smartlog.Smartlog({ + logContext: { + environment: 'production', + runtime: "cloudflare_workers", + zone: 'servezone', + company: 'Lossless GmbH', + companyunit: 'Lossless Cloud', + containerName: 'cloudflare_workers' + } + }); + + public setRequestData (optionsArg: { + requestAgent: string; + requestUrl: string; + requestMethod: string; + }) { + this.data = { + ...this.data, + ...{ + requestAgent: optionsArg.requestAgent, + requestUrl: optionsArg.requestUrl, + requestMethod: optionsArg.requestMethod, + requestStartTime: Date.now() + } + }; + + } + + public setResponseData(optionsArg: { + responseStatus: number, + responseEndTime: number, + }) { + this.data = { + ...this.data, + ...{ + responseStatus: optionsArg.responseStatus, + responseEndTime: optionsArg.responseEndTime + } + }; + this.sendLogs(); + } + + public async sendLogs() { + await this.smartlog.log('info', ` + Got a ${this.data.requestMethod} request from ${this.data.requestAgent} to + ${this.data.requestUrl} + that took ${this.data.responseEndTime - this.data.requestStartTime}ms to resolve with status ${this.data.responseStatus}.`, this.data); + this.finishedDeferred.resolve(); + } +} \ No newline at end of file diff --git a/ts_edgeworker/analytics/smartlog.ts b/ts_edgeworker/analytics/smartlog.ts new file mode 100644 index 0000000..020a74c --- /dev/null +++ b/ts_edgeworker/analytics/smartlog.ts @@ -0,0 +1,26 @@ +import * as smartlogInterfaces from '@push.rocks/smartlog-interfaces'; +import type { EdgeWorker } from '../classes.edgeworker.js'; + +export class SmartlogDestination implements smartlogInterfaces.ILogDestination { + public edgeWorkerRef: EdgeWorker; + + constructor(edgeworkerRefArg: EdgeWorker) { + this.edgeWorkerRef = edgeworkerRefArg; + } + + public async handleLog(logPackageArg: smartlogInterfaces.ILogPackage) { + if (this.edgeWorkerRef.options.smartlogConfig) { + const requestBody: smartlogInterfaces.ILogPackageAuthenticated = { + auth: this.edgeWorkerRef.options.smartlogConfig.token, + logPackage: logPackageArg, + }; + await fetch(this.edgeWorkerRef.options.smartlogConfig.endpoint, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + } + } +} diff --git a/ts_edgeworker/classes.domainrouter.ts b/ts_edgeworker/classes.domainrouter.ts new file mode 100644 index 0000000..29b66f2 --- /dev/null +++ b/ts_edgeworker/classes.domainrouter.ts @@ -0,0 +1,67 @@ +import * as interfaces from './interfaces/index.js'; +import * as plugins from './plugins.js'; +import { WorkerEvent } from './classes.workerevent.js'; + +import * as domainInstructions from './domaininstructions/index.js'; + +export class DomainRouter { + private smartmatches: plugins.smartmatch.SmartMatch[] = []; + + constructor() { + for (const key of Object.keys(domainInstructions.instructionObject)) { + this.smartmatches.push(new plugins.smartmatch.SmartMatch(key)); + } + } + + /** + * + * @param cworkerevent + */ + public routeToResponder(cworkerevent: WorkerEvent) { + const match = this.smartmatches.find(smartmatchArg => { + return smartmatchArg.match(cworkerevent.request.url); + }); + cworkerevent.responderInstruction = match + ? domainInstructions.instructionObject[match.wildcard] + : { + type: 'cache' + }; + } + + /** + * rendertronRouter + */ + public checkWetherReRouteToRendertron(cworkerevent: WorkerEvent) { + let needsRendertron = false; + for (const botAgentIdentifier of domainInstructions.botUserAgents) { + if (needsRendertron) { + continue; + } + if ( + cworkerevent.request.headers.get('user-agent') && + cworkerevent.request.headers.get('user-agent').toLowerCase().includes(botAgentIdentifier.toLowerCase()) && + !cworkerevent.request.url.includes('lossless.one') + ) { + needsRendertron = true; + } + } + if (needsRendertron) { + cworkerevent.routedThroughRendertron = true; + } + } + + /** + * check wether this is a preflight request that should be handled + */ + public checkWetherIsPreflight (cworkerevent: WorkerEvent) { + if ( + cworkerevent.request.method === 'OPTIONS' && + cworkerevent.request.headers.get('Origin') !== null && + cworkerevent.request.headers.get('Access-Control-Request-Method') !== null && + cworkerevent.request.headers.get('Access-Control-Request-Headers') !== null + ) { + cworkerevent.isPreflight = true; + } + } + +} diff --git a/ts_edgeworker/classes.edgeworker.ts b/ts_edgeworker/classes.edgeworker.ts new file mode 100644 index 0000000..8d7835f --- /dev/null +++ b/ts_edgeworker/classes.edgeworker.ts @@ -0,0 +1,73 @@ +// imports +import { WorkerEvent } from './classes.workerevent.js'; +import { DomainRouter } from './classes.domainrouter.js'; +import * as plugins from './plugins.js'; +import * as responders from './responders/index.js'; + +export interface IEdgeWorkerOptions { + smartlogConfig?: { + endpoint: string; + token: string; + } +} + +export class EdgeWorker { + public options: IEdgeWorkerOptions; + domainRouter: DomainRouter; + + constructor(optionsArg: IEdgeWorkerOptions = {}) { + this.options = optionsArg; + this.domainRouter = new DomainRouter(); + addEventListener('fetch', this.fetchFunction as any); + } + + public async fetchFunction (eventArg: plugins.cloudflareTypes.FetchEvent) { + if (new URL(eventArg.request.url).pathname.startsWith('/socket.io')) { + return; + } + + const cworkerEvent = new WorkerEvent({ + edgeWorkerRef: this, + event: eventArg, + passThroughOnException: true + }); + + // lets answer basic reuest things + responders.timeoutResponder(cworkerEvent); + cworkerEvent.hasResponse ? null : await responders.urlFormattingResponder(cworkerEvent); + + // lets route the domain + this.domainRouter.routeToResponder(cworkerEvent); + this.domainRouter.checkWetherReRouteToRendertron(cworkerEvent); + this.domainRouter.checkWetherIsPreflight(cworkerEvent); + + // guardresponder + cworkerEvent.hasResponse ? null : await responders.guardResponder(cworkerEvent); + + // lets process all requests that need rendertron + cworkerEvent.hasResponse ? null : await responders.rendertronResponder(cworkerEvent); + + // lets process all requests that are preflight requests + cworkerEvent.hasResponse ? null : await responders.preflightResponder(cworkerEvent); + + switch (cworkerEvent.responderInstruction.type) { + case 'cache': + cworkerEvent.hasResponse ? null : await responders.cacheResponder(cworkerEvent); + break; + case 'origin': + cworkerEvent.hasResponse ? null : await responders.originResponder(cworkerEvent); + break; + case 'redirect': + cworkerEvent.hasResponse ? null : await responders.adsTxtResponder(cworkerEvent); + break; + case 'static': + cworkerEvent.hasResponse ? null : await responders.staticResponder(cworkerEvent); + break; + case 'ads.txt': + cworkerEvent.hasResponse ? null : await responders.adsTxtResponder(cworkerEvent); + break; + } + // cworkerEvent.hasResponse ? null : await responders.kvResponder(cworkerEvent); + cworkerEvent.hasResponse ? null : await responders.errorResponder(cworkerEvent); + }; +} diff --git a/ts_edgeworker/classes.kvhandler.ts b/ts_edgeworker/classes.kvhandler.ts new file mode 100644 index 0000000..365c85d --- /dev/null +++ b/ts_edgeworker/classes.kvhandler.ts @@ -0,0 +1,37 @@ +import * as interfaces from './interfaces/index.js'; +import * as plugins from './plugins.js'; + +declare var lokv: plugins.cloudflareTypes.KVNamespace; + +/** + * an abstraction for the workerd KV store + */ +export class KVHandler { + private getSafeIdentifier(urlString: string) { + return encodeURI(urlString); + } + + async getFromKv(keyIdentifier: string) { + const key = this.getSafeIdentifier(keyIdentifier); + const valueString = await lokv.get(key); + return valueString; + } + + async putInKv(keyIdentifier: string, valueForStorage: string) { + const key = this.getSafeIdentifier(keyIdentifier); + const value = valueForStorage; + await lokv.put(key, value); + return null; + } + + /** + * deletes a key/value from the cache + * @param keyIdentifier + */ + async deleteInKv(keyIdentifier: string) { + const cacheKey = this.getSafeIdentifier(keyIdentifier); + await lokv.delete(cacheKey); + } +} + +export const kvHandlerInstance = new KVHandler(); diff --git a/ts_edgeworker/classes.responsekv.ts b/ts_edgeworker/classes.responsekv.ts new file mode 100644 index 0000000..b6ccd77 --- /dev/null +++ b/ts_edgeworker/classes.responsekv.ts @@ -0,0 +1,46 @@ +import * as plugins from './plugins.js'; +import { kvHandlerInstance } from './classes.kvhandler.js'; + +declare var lokv: plugins.cloudflareTypes.KVNamespace; + +interface IKVResponseObject { + headers: { [key: string]: string }; + version: string; + body: string; +} + +export class ResponseKv { + public async storeResponse(urlIdentifier: string, responseArg: any) { + const headers: { [key: string]: string } = {}; + for (const kv of responseArg.headers.entries()) { + headers[kv[0]] = kv[1]; + } + const kvResponseForStorage: IKVResponseObject = { + headers, + version: '1.0.0', + body: await responseArg.text() + }; + await kvHandlerInstance.putInKv(urlIdentifier, JSON.stringify(kvResponseForStorage)); + } + + public async getResponse(urlIdentifier: string): Promise { + const kvValue = await kvHandlerInstance.getFromKv(urlIdentifier); + if (kvValue) { + let kvResponse: IKVResponseObject; + try { + kvResponse = JSON.parse(kvValue); + } catch (e) { + console.log(e); + return null; + } + const headers = new Headers(); + for (const key of Object.keys(kvResponse.headers)) { + headers.append(key, kvResponse.headers[key]); + } + headers.append('SERVEZONE_ROUTE', 'CLOUDFLARE_EDGE_LOKV'); + return new Response(kvResponse.body, { headers: headers }); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/ts_edgeworker/classes.workerevent.ts b/ts_edgeworker/classes.workerevent.ts new file mode 100644 index 0000000..8f1877c --- /dev/null +++ b/ts_edgeworker/classes.workerevent.ts @@ -0,0 +1,102 @@ +import * as interfaces from './interfaces/index.js'; +import * as plugins from './plugins.js'; +import * as helpers from './helpers/index.js'; +import { DomainRouter } from './classes.domainrouter.js'; +import { Analyzer } from './analytics/analyzer.js'; +import type { EdgeWorker } from './classes.edgeworker.js'; + +export interface ICworkerEventOptions { + event: plugins.cloudflareTypes.FetchEvent + edgeWorkerRef: EdgeWorker; + passThroughOnException?: boolean; +} + + +export class WorkerEvent { + public options: ICworkerEventOptions; + + public analyzer: Analyzer; + private responseDeferred: plugins.smartpromise.Deferred; + private waitUntilDeferred: plugins.smartpromise.Deferred; + + private response: Response = null; + private waitList = []; + + // routing settings + public responderInstruction: interfaces.IResponderInstruction; + public routedThroughRendertron: boolean = false; + public isPreflight: boolean = false; + + public request: plugins.cloudflareTypes.Request; + + public parsedUrl: URL; + + constructor(optionsArg: ICworkerEventOptions) { + this.options = optionsArg; + + // lets create an Analyzer for this request + this.analyzer = new Analyzer(this); + + // lets make sure we always answer + this.options.passThroughOnException ? this.options.event.passThroughOnException() : null; + + // lets set up some better asnyc behaviour + this.waitUntilDeferred = plugins.smartpromise.defer(); + this.responseDeferred = plugins.smartpromise.defer(); + this.addToWaitList(this.analyzer.finishedDeferred.promise); + + // lets entangle the event with this class instance + this.request = this.options.event.request; + + // lets start with analytics + this.analyzer.setRequestData({ + requestAgent: this.request.headers.get('user-agent'), + requestMethod: this.request.method, + requestUrl: this.request.url + }); + + + this.options.event.respondWith(this.responseDeferred.promise); + this.options.event.waitUntil(this.waitUntilDeferred.promise); + + // lets parse the url + this.parsedUrl = new URL(this.request.url); + + // lets check the waitlist + this.checkWaitList(); + console.log(`Got request for ${this.request.url}`); + } + + get hasResponse () { + let returnValue: boolean; + this.response ? returnValue = true : returnValue = false; + return returnValue; + } + + public addToWaitList(promiseArg: Promise) { + this.waitList.push(promiseArg); + } + + private async checkWaitList() { + await this.responseDeferred.promise; + const currentWaitList = this.waitList; + this.waitList = []; + await Promise.all(currentWaitList); + if (this.waitList.length > 0) { + this.checkWaitList(); + } else { + this.waitUntilDeferred.resolve(); + } + } + + public setResponse (responseArg: Response) { + this.response = responseArg; + this.responseDeferred.resolve(responseArg); + this.analyzer.setResponseData({ + responseStatus: this.response.status, + responseEndTime: Date.now() + }); + } + + +} \ No newline at end of file diff --git a/ts_edgeworker/domaininstructions/botuseragents.ts b/ts_edgeworker/domaininstructions/botuseragents.ts new file mode 100644 index 0000000..e0b7676 --- /dev/null +++ b/ts_edgeworker/domaininstructions/botuseragents.ts @@ -0,0 +1,36 @@ +export const botUserAgents = [ + // Baidu + 'baiduspider', + 'embedly', + + // Facebook + 'facebookexternalhit', + + // Ghost + 'Ghost', + + // Microsoft + 'bingbot', + 'BingPreview', + 'linkedinbot', + 'MissinglettrBot', + 'msnbot', + 'outbrain', + 'pinterest', + 'quora link preview', + 'rogerbot', + 'showyoubot', + 'slackbot', + 'TelegramBot', + + // Twitter + 'twitterbot', + 'vkShare', + 'W3C_Validator', + + // WhatsApp + 'whatsapp', + + // woorank + 'woorank' +]; diff --git a/ts_edgeworker/domaininstructions/domaininstructions.ts b/ts_edgeworker/domaininstructions/domaininstructions.ts new file mode 100644 index 0000000..1d480d6 --- /dev/null +++ b/ts_edgeworker/domaininstructions/domaininstructions.ts @@ -0,0 +1,7 @@ +import * as interfaces from '../interfaces/index.js'; + +export const instructionObject: { [key: string]: interfaces.IResponderInstruction } = { + '*/ads.txt': { + type: 'ads.txt', + } +}; diff --git a/ts_edgeworker/domaininstructions/index.ts b/ts_edgeworker/domaininstructions/index.ts new file mode 100644 index 0000000..c7d4552 --- /dev/null +++ b/ts_edgeworker/domaininstructions/index.ts @@ -0,0 +1,2 @@ +export * from './botuseragents.js'; +export * from './domaininstructions.js'; diff --git a/ts_edgeworker/helpers/checks.ts b/ts_edgeworker/helpers/checks.ts new file mode 100644 index 0000000..565794b --- /dev/null +++ b/ts_edgeworker/helpers/checks.ts @@ -0,0 +1,9 @@ +import * as plugins from '../plugins.js'; +declare var lokv: plugins.cloudflareTypes.KVNamespace; +export const checkLokv = () => { + if (!lokv) { + throw new Error('lokv not defined!'); + } else { + console.log('lokv present!'); + } +}; diff --git a/ts_edgeworker/helpers/index.ts b/ts_edgeworker/helpers/index.ts new file mode 100644 index 0000000..7ddd7b1 --- /dev/null +++ b/ts_edgeworker/helpers/index.ts @@ -0,0 +1 @@ +export * from './checks.js'; diff --git a/ts_edgeworker/index.ts b/ts_edgeworker/index.ts new file mode 100644 index 0000000..d8a2b3e --- /dev/null +++ b/ts_edgeworker/index.ts @@ -0,0 +1 @@ +export * from './classes.edgeworker.js'; \ No newline at end of file diff --git a/ts_edgeworker/interfaces/custom.ts b/ts_edgeworker/interfaces/custom.ts new file mode 100644 index 0000000..43e9026 --- /dev/null +++ b/ts_edgeworker/interfaces/custom.ts @@ -0,0 +1,9 @@ +import { WorkerEvent } from "../classes.workerevent.js"; + +export interface IResponderInstruction { + type: 'origin' | 'cache' | 'static' | 'redirect' | 'ads.txt'; + cacheClientSideForMin?: number; + redirectUrl?: string; +} + +export type TRequestResponser = (workerEventArg: WorkerEvent) => Promise; diff --git a/ts_edgeworker/interfaces/index.ts b/ts_edgeworker/interfaces/index.ts new file mode 100644 index 0000000..773934f --- /dev/null +++ b/ts_edgeworker/interfaces/index.ts @@ -0,0 +1 @@ +export * from './custom.js'; diff --git a/ts_edgeworker/plugins.ts b/ts_edgeworker/plugins.ts new file mode 100644 index 0000000..58b71ec --- /dev/null +++ b/ts_edgeworker/plugins.ts @@ -0,0 +1,21 @@ +// @pushrocks scope +import * as smartdelay from '@push.rocks/smartdelay'; +import * as smartlog from '@push.rocks/smartlog'; +import * as smartlogInterfaces from '@push.rocks/smartlog-interfaces'; +import * as smartmatch from '@push.rocks/smartmatch'; +import * as smartpromise from '@push.rocks/smartpromise'; + +export { + smartdelay, + smartlog, + smartlogInterfaces, + smartmatch, + smartpromise +}; + +// cloudflarea +import * as cloudflareTypes from '@cloudflare/workers-types'; + +export { + cloudflareTypes +} diff --git a/ts_edgeworker/responders/adstxt.responder.ts b/ts_edgeworker/responders/adstxt.responder.ts new file mode 100644 index 0000000..b61550e --- /dev/null +++ b/ts_edgeworker/responders/adstxt.responder.ts @@ -0,0 +1,14 @@ +import * as interfaces from '../interfaces/index.js'; +import { WorkerEvent } from '../classes.workerevent.js'; + +export const adsTxtResponder: interfaces.TRequestResponser = async (cWorkerEventArg: WorkerEvent) => { + if (cWorkerEventArg.responderInstruction.type === 'ads.txt') { + const response = new Response('google.com, pub-4104137977476459, DIRECT, f08c47fec0942fa0\n', { + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + } + }) + cWorkerEventArg.setResponse(response); + } + +}; \ No newline at end of file diff --git a/ts_edgeworker/responders/cache.responder.ts b/ts_edgeworker/responders/cache.responder.ts new file mode 100644 index 0000000..f87ef18 --- /dev/null +++ b/ts_edgeworker/responders/cache.responder.ts @@ -0,0 +1,92 @@ +import * as interfaces from '../interfaces/index.js'; +import * as plugins from '../plugins.js'; +import { WorkerEvent } from '../classes.workerevent.js'; +import { kvHandlerInstance } from '../classes.kvhandler.js'; + +declare const fetch: plugins.cloudflareTypes.Fetcher['fetch']; + +declare var caches: any; +export const cacheResponder: interfaces.TRequestResponser = async (cworkerEventArg: WorkerEvent) => { + const host = cworkerEventArg.request.headers.get('Host'); + const appHashKey = `${host.toLowerCase()}_appHash`; + const appHash = await kvHandlerInstance.getFromKv(appHashKey); + + const cache = caches.default; + let response: Response = await cache.match(cworkerEventArg.request); + + if ( + response && + response.headers.get('appHash') && + response.headers.get('appHash') !== appHash + ) { + response = null; + } + + if (response) { + cworkerEventArg.setResponse(response); + } else { + response = await handleNewRequest(cworkerEventArg.request); + if (response) { + cworkerEventArg.addToWaitList(new Promise(async (resolve, reject) => { + const newAppHash = response.headers.get('appHash'); + if (newAppHash) { + await kvHandlerInstance.putInKv(appHashKey, newAppHash); + } + resolve(); + })); + cworkerEventArg.addToWaitList(buildCacheResponse(cache, cworkerEventArg.request, response)); + cworkerEventArg.setResponse(response); + } + } +}; + +/** + * @param {Request} originalRequest + */ +const handleNewRequest = async (originalRequest: plugins.cloudflareTypes.Request): Promise => { + console.log('answering from origin'); + const originResponse: any = await fetch( + originalRequest + ); + + // lets capture status + if (originResponse.status > 399) { + return null; + } + + const responseClientPassThroughStream = new TransformStream(); + originResponse.body.pipeTo(responseClientPassThroughStream.writable); + + // build response for client + const clientHeaders = new Headers(); + for (const kv of originResponse.headers.entries()) { + clientHeaders.append(kv[0], kv[1]); + } + clientHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_ORIGIN_INITIAL'); + const responseForClient = new Response(responseClientPassThroughStream.readable, { + ...originResponse, + headers: clientHeaders + }); + + // lets return the responses + return responseForClient; +}; + +const buildCacheResponse = async (cache, matchRequest: plugins.cloudflareTypes.Request, originResponse: any) => { + const cacheHeaders = new Headers(); + for (const kv of originResponse.headers.entries()) { + cacheHeaders.append(kv[0], kv[1]); + } + cacheHeaders.delete('SERVEZONE_ROUTE'); + cacheHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_CACHE'); + cacheHeaders.delete('Cache-Control'); + cacheHeaders.append('Cache-Control', 'public, max-age=60'); + cacheHeaders.delete('Expires'); + cacheHeaders.append('Expires', new Date(Date.now() + 60 * 1000).toUTCString()); + + const responseForCache = new Response(await originResponse.clone().body, { + ...originResponse, + headers: cacheHeaders + }); + await cache.put(matchRequest, responseForCache); +}; diff --git a/ts_edgeworker/responders/error.responder.ts b/ts_edgeworker/responders/error.responder.ts new file mode 100644 index 0000000..1c9a8c3 --- /dev/null +++ b/ts_edgeworker/responders/error.responder.ts @@ -0,0 +1,6 @@ +import * as interfaces from '../interfaces/index.js'; +import { WorkerEvent } from '../classes.workerevent.js'; +export const errorResponder: interfaces.TRequestResponser = async (cWorkerEvent: WorkerEvent) => { + const errorResponse = await fetch('https://nullresolve.lossless.one/status/firewall'); + cWorkerEvent.setResponse(errorResponse); +}; \ No newline at end of file diff --git a/ts_edgeworker/responders/guard.responder.ts b/ts_edgeworker/responders/guard.responder.ts new file mode 100644 index 0000000..2574aa8 --- /dev/null +++ b/ts_edgeworker/responders/guard.responder.ts @@ -0,0 +1,8 @@ +import * as interfaces from '../interfaces/index.js'; +import { WorkerEvent } from '../classes.workerevent.js'; +export const guardResponder: interfaces.TRequestResponser = async (cWorkerEvent: WorkerEvent) => { + if (cWorkerEvent.parsedUrl.pathname.endsWith('.map')) { + const errorResponse = await fetch('https://nullresolve.lossless.one/status/firewall'); + cWorkerEvent.setResponse(errorResponse); + } +}; \ No newline at end of file diff --git a/ts_edgeworker/responders/index.ts b/ts_edgeworker/responders/index.ts new file mode 100644 index 0000000..68264ff --- /dev/null +++ b/ts_edgeworker/responders/index.ts @@ -0,0 +1,12 @@ +export * from './adstxt.responder.js'; +export * from './cache.responder.js'; +export * from './urlformatting.responder.js'; +export * from './error.responder.js'; +export * from './guard.responder.js'; +export * from './kv.responder.js'; +export * from './origin.responder.js'; +export * from './preflight.responder.js'; +export * from './redirect.reponder.js'; +export * from './rendertron.responder.js'; +export * from './static.responder.js'; +export * from './timeout.responder.js'; diff --git a/ts_edgeworker/responders/kv.responder.ts b/ts_edgeworker/responders/kv.responder.ts new file mode 100644 index 0000000..6498647 --- /dev/null +++ b/ts_edgeworker/responders/kv.responder.ts @@ -0,0 +1,34 @@ +import * as interfaces from '../interfaces/index.js'; +import * as plugins from '../plugins.js'; +import { WorkerEvent } from '../classes.workerevent.js'; +import { ResponseKv } from '../classes.responsekv.js'; + +declare const fetch: plugins.cloudflareTypes.Fetcher['fetch']; + +export const kvResponder: interfaces.TRequestResponser = async (cworkerEventArg: WorkerEvent) => { + const responseKvInstance = new ResponseKv(); + let response = await responseKvInstance.getResponse(cworkerEventArg.request.url); + if (response) { + console.log('Got response from KV'); + } else { + response = await handleNewRequest(cworkerEventArg.request, responseKvInstance); + } + cworkerEventArg.setResponse(response); +}; + +const handleNewRequest = async (request: plugins.cloudflareTypes.Request, responseKvInstance: ResponseKv) => { + const originResponse: any = await fetch(request); + // build response for cache + const cacheHeaders = new Headers(); + for (const kv of originResponse.headers.entries()) { + cacheHeaders.append(kv[0], kv[1]); + } + cacheHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_KVRESPONSE'); + cacheHeaders.append('Cache-Control', 'max-age=600'); + const responseForKV = new Response(await originResponse.body, { + ...originResponse, + headers: cacheHeaders + }); + await responseKvInstance.storeResponse(request.url, responseForKV.clone()); + return responseForKV.clone(); +}; diff --git a/ts_edgeworker/responders/origin.responder.ts b/ts_edgeworker/responders/origin.responder.ts new file mode 100644 index 0000000..c1d1e00 --- /dev/null +++ b/ts_edgeworker/responders/origin.responder.ts @@ -0,0 +1,31 @@ +import * as interfaces from '../interfaces/index.js'; +import * as plugins from '../plugins.js'; +import { WorkerEvent } from '../classes.workerevent.js'; + +declare const fetch: plugins.cloudflareTypes.Fetcher['fetch']; + +export const originResponder: interfaces.TRequestResponser = async (eventArg: WorkerEvent) => { + const originResponse: any = await fetch(eventArg.request); + // lets capture status + if (originResponse.status > 399) { + return; + } + + const headers = new Headers(); + for (const kv of originResponse.headers.entries()) { + headers.append(kv[0], kv[1]); + } + headers.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_FASTORIGIN'); + + const responsePassThroughStream = new TransformStream(); + originResponse.body.pipeTo(responsePassThroughStream.writable); + + // response + + const responseForClient = new Response(responsePassThroughStream.readable, { + ...originResponse, + headers, + }); + + eventArg.setResponse(responseForClient); +}; diff --git a/ts_edgeworker/responders/preflight.responder.ts b/ts_edgeworker/responders/preflight.responder.ts new file mode 100644 index 0000000..8f55dd8 --- /dev/null +++ b/ts_edgeworker/responders/preflight.responder.ts @@ -0,0 +1,18 @@ +import * as interfaces from '../interfaces/index.js'; +import { WorkerEvent } from '../classes.workerevent.js'; + +export const preflightResponder: interfaces.TRequestResponser = async (eventArg: WorkerEvent) => { + if (eventArg.isPreflight) { + const corsHeaders = new Headers(); + corsHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_PREFLIGHT'); + corsHeaders.append('Access-Control-Allow-Origin', '*'); + corsHeaders.append('Access-Control-Allow-Methods', '*'); + corsHeaders.append('Access-Control-Allow-Headers', '*'); + + eventArg.setResponse( + new Response(null, { + headers: corsHeaders, + }) + ); + } +}; diff --git a/ts_edgeworker/responders/redirect.reponder.ts b/ts_edgeworker/responders/redirect.reponder.ts new file mode 100644 index 0000000..7ad473b --- /dev/null +++ b/ts_edgeworker/responders/redirect.reponder.ts @@ -0,0 +1,9 @@ +import * as interfaces from '../interfaces/index.js'; +import { WorkerEvent } from '../classes.workerevent.js'; + +export const redirectResponder: interfaces.TRequestResponser = async (cWorkerEventArg: WorkerEvent) => { + if (cWorkerEventArg.responderInstruction.type === 'redirect') { + cWorkerEventArg.setResponse(Response.redirect(cWorkerEventArg.responderInstruction.redirectUrl, 302)); + } + +}; \ No newline at end of file diff --git a/ts_edgeworker/responders/rendertron.responder.ts b/ts_edgeworker/responders/rendertron.responder.ts new file mode 100644 index 0000000..ef14445 --- /dev/null +++ b/ts_edgeworker/responders/rendertron.responder.ts @@ -0,0 +1,22 @@ +import * as plugins from '../plugins.js'; +import { WorkerEvent } from '../classes.workerevent.js'; + +export const rendertronResponder = async (cworkerevent: WorkerEvent) => { + if (cworkerevent.routedThroughRendertron) { + const oldHeaders: any = cworkerevent.request.headers; + const rendertronHeaders = new Headers(); + for (const kv of oldHeaders.entries()) { + const headerName = kv[0]; + const headerValue = headerName === 'user-agent' ? 'Lossless Rendertron' : kv[1]; + rendertronHeaders.append(headerName, headerValue); + } + const rendertronRequest = new Request( + `https://rendertron.lossless.one/render/${cworkerevent.request.url}`, + { + method: cworkerevent.request.method, + headers: rendertronHeaders + } + ); + cworkerevent.setResponse(await fetch(rendertronRequest)); + } +}; diff --git a/ts_edgeworker/responders/static.responder.ts b/ts_edgeworker/responders/static.responder.ts new file mode 100644 index 0000000..00f27f9 --- /dev/null +++ b/ts_edgeworker/responders/static.responder.ts @@ -0,0 +1,31 @@ +import * as interfaces from '../interfaces/index.js'; +import { WorkerEvent } from '../classes.workerevent.js'; + +export const staticResponder: interfaces.TRequestResponser = async (cWorkerEventArg: WorkerEvent) => { + if (cWorkerEventArg.responderInstruction.type === 'static') { + const originResponse: any = await fetch( + `https://statichost.lossless.one/resolve?url=${encodeURI(cWorkerEventArg.request.url)}` + ); + + const cacheHeaders = new Headers(); + for (const kv of originResponse.headers.entries()) { + cacheHeaders.append(kv[0], kv[1]); + } + cacheHeaders.delete('SERVEZONE_ROUTE'); + cacheHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_STATICHOST'); + + if (cWorkerEventArg.responderInstruction.cacheClientSideForMin) { + cacheHeaders.delete('Cache-Control'); + cacheHeaders.append('Cache-Control', `public, max-age=${cWorkerEventArg.responderInstruction.cacheClientSideForMin * 60}`); + cacheHeaders.delete('Expires'); + cacheHeaders.append('Expires', new Date(Date.now() + cWorkerEventArg.responderInstruction.cacheClientSideForMin * 1000).toUTCString()); + } + + const responseForClient = new Response(await originResponse.clone().body, { + ...originResponse, + headers: cacheHeaders + }); + + cWorkerEventArg.setResponse(responseForClient); + } +}; diff --git a/ts_edgeworker/responders/timeout.responder.ts b/ts_edgeworker/responders/timeout.responder.ts new file mode 100644 index 0000000..1dc700a --- /dev/null +++ b/ts_edgeworker/responders/timeout.responder.ts @@ -0,0 +1,23 @@ +import * as interfaces from '../interfaces/index.js'; +import * as plugins from '../plugins.js'; +import { WorkerEvent } from '../classes.workerevent.js'; + +export const timeoutResponder: interfaces.TRequestResponser = async (cWorkerEvent: WorkerEvent) => { + await plugins.smartdelay.delayFor(10000); + if (cWorkerEvent.routedThroughRendertron) { + await plugins.smartdelay.delayFor(10000); + } + if (!cWorkerEvent.hasResponse) { + const errorResponse = await fetch( + `https://nullresolve.lossless.one/custom?title=${encodeURI( + `Lossless Network: Request Cancellation!` + )}&heading=${encodeURI(`Error: Request Cancellation`)}&text=${encodeURI( + `Lossless Network could not decide how to respond to this request within 5 seconds. Therefore it timed out and has been canceled. +

requestUrl: ${cWorkerEvent.request.url}
+ requestTime: ${Date.now()}
+ referenceNumber: xxxxxx

` + )}` + ); + cWorkerEvent.setResponse(errorResponse); + } +}; diff --git a/ts_edgeworker/responders/urlformatting.responder.ts b/ts_edgeworker/responders/urlformatting.responder.ts new file mode 100644 index 0000000..88c1bbb --- /dev/null +++ b/ts_edgeworker/responders/urlformatting.responder.ts @@ -0,0 +1,21 @@ +import * as interfaces from '../interfaces/index.js'; +import { WorkerEvent } from '../classes.workerevent.js'; + +export const urlFormattingResponder: interfaces.TRequestResponser = async (eventArg: WorkerEvent) => { + let shouldCorrect = false; + const correctedUrl = new URL(eventArg.request.url); + if (eventArg.parsedUrl.hostname.startsWith('www.')) { + shouldCorrect = true; + correctedUrl.hostname = eventArg.parsedUrl.hostname.substring( + 4, + eventArg.parsedUrl.hostname.length + ); + } + if (eventArg.parsedUrl.protocol.startsWith('http:')) { + shouldCorrect = true; + correctedUrl.protocol = 'https:'; + } + if (shouldCorrect) { + eventArg.setResponse(Response.redirect(`${correctedUrl.protocol}//${correctedUrl.host}${correctedUrl.pathname}${correctedUrl.search}`, 301)); + } +}; diff --git a/ts_edgeworker/versionhandler.ts b/ts_edgeworker/versionhandler.ts new file mode 100644 index 0000000..6007815 --- /dev/null +++ b/ts_edgeworker/versionhandler.ts @@ -0,0 +1,7 @@ +import * as interfaces from './interfaces/index.js'; + +export class VersionHandler { + +} + +export const versionHandlerInstance = new VersionHandler(); diff --git a/ts_web_serviceworker/serviceworker.classes.serviceworker.ts b/ts_web_serviceworker/serviceworker.classes.serviceworker.ts index fc1532d..80c1ca2 100644 --- a/ts_web_serviceworker/serviceworker.classes.serviceworker.ts +++ b/ts_web_serviceworker/serviceworker.classes.serviceworker.ts @@ -11,13 +11,14 @@ import { logger } from './serviceworker.logging.js'; import { UpdateManager } from './serviceworker.classes.updatemanager.js'; import { NetworkManager } from './serviceworker.classes.networkmanager.js'; import { TaskManager } from './serviceworker.classes.taskmanager.js'; +import { ServiceworkerBackend } from './classes.backend.js'; export class LosslessServiceWorker { // STATIC // INSTANCE public serviceWindowRef: interfaces.ServiceWindow; - public leleServiceWorkerBackend: plugins.leleServiceworker.LosslessServiceworkerBackend; + public leleServiceWorkerBackend: ServiceworkerBackend; public cacheManager: CacheManager; @@ -29,7 +30,7 @@ export class LosslessServiceWorker { constructor(selfArg: interfaces.ServiceWindow) { logger.log('info', `Service worker instantiating at ${Date.now()}`); this.serviceWindowRef = selfArg; - this.leleServiceWorkerBackend = plugins.leleServiceworker.getServiceWorkerBackend({ + this.leleServiceWorkerBackend = new ServiceworkerBackend({ self: selfArg, purgeCache: async (reqArg) => { await this.cacheManager.cleanCaches(), diff --git a/ts_web_serviceworker/serviceworker.classes.updatemanager.ts b/ts_web_serviceworker/serviceworker.classes.updatemanager.ts index 95d7183..295cd34 100644 --- a/ts_web_serviceworker/serviceworker.classes.updatemanager.ts +++ b/ts_web_serviceworker/serviceworker.classes.updatemanager.ts @@ -1,11 +1,12 @@ import * as plugins from './plugins.js'; +import * as interfaces from '../dist_ts_interfaces/index.js'; import { LosslessServiceWorker } from './serviceworker.classes.serviceworker.js'; import { logger } from './serviceworker.logging.js'; import { CacheManager } from './serviceworker.classes.cachemanager.js'; export class UpdateManager { public lastUpdateCheck: number = 0; - public lastVersionInfo: plugins.lointServiceworker.IRequest_Serviceworker_Backend_VersionInfo['response']; + public lastVersionInfo: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response']; public serviceworkerRef: LosslessServiceWorker; @@ -59,7 +60,7 @@ export class UpdateManager { */ public async getVersionInfoFromServer() { const getAppHashRequest = new plugins.typedrequest.TypedRequest< - plugins.lointServiceworker.IRequest_Serviceworker_Backend_VersionInfo + interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo >('/lsw-typedrequest', 'serviceworker_versionInfo'); const result = await getAppHashRequest.fire({}); return result; diff --git a/ts_web_serviceworker_client/interfaces/index.ts b/ts_web_serviceworker_client/interfaces/index.ts deleted file mode 100644 index fe62391..0000000 --- a/ts_web_serviceworker_client/interfaces/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import * as plugins from '../lele-serviceworker.plugins.js'; -