Repository: cloudflare/worker-speedtest-template Branch: master Commit: d9b0c5535594 Files: 12 Total size: 11.5 KB Directory structure: gitextract_n7fvj002/ ├── .github/ │ └── workflows/ │ └── semgrep.yml ├── .gitignore ├── .prettierrc ├── README.md ├── index.js ├── package.json ├── src/ │ ├── down.js │ ├── index.js │ ├── router.js │ ├── test.js │ └── up.js └── wrangler.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/semgrep.yml ================================================ on: pull_request: {} workflow_dispatch: {} push: branches: - main - master schedule: - cron: '0 0 * * *' name: Semgrep config jobs: semgrep: name: semgrep/ci runs-on: ubuntu-20.04 env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} SEMGREP_URL: https://cloudflare.semgrep.dev SEMGREP_APP_URL: https://cloudflare.semgrep.dev SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version container: image: returntocorp/semgrep steps: - uses: actions/checkout@v3 - run: semgrep ci ================================================ FILE: .gitignore ================================================ node_modules/* ================================================ FILE: .prettierrc ================================================ { "trailingComma": "none", "singleQuote": true } ================================================ FILE: README.md ================================================ ## Speed Test Worker for measuring download / upload connection speed from the client side, using the [Performance Timing API](https://w3c.github.io/perf-timing-primer/). ### Installation [`index.js`](https://github.com/cloudflare/worker-speedtest-template/blob/master/router.js) is the content of the Workers script. _Note:_ when running this as your own worker, your latency measurements may differ a small amount from the [official version](https://speed.cloudflare.com). This is due to the fact that we rely on an internal mechanism to determine the amount of server processing time, which is then subtracted from the measurement. #### Wrangler You can use [wrangler](https://github.com/cloudflare/wrangler) to generate a new Cloudflare Workers project based on this template by running the following command from your terminal: ``` wrangler generate myApp https://github.com/cloudflare/worker-speedtest-template ``` Before publishing your code you need to edit `wrangler.toml` file and add your Cloudflare `account_id` - more information about publishing your code can be found [in the documentation](https://workers.cloudflare.com/docs/quickstart/configuring-and-publishing/). Once you are ready, you can publish your code by running the following command: ``` wrangler publish ``` #### Serverless To deploy using serverless add a [`serverless.yml`](https://serverless.com/framework/docs/providers/cloudflare/) file. ### API Reference This worker exposes two endpoints designed to support the measuring of bandwidth and latency from the client side. #### Download **GET** `/down` Request binary content of a certain size | Param | Description | Required | Default | | ----- | -------------------------------------- | :------: | :-----: | | bytes | The size of the response body in bytes | no | 0 | Example: `/down?bytes=10000` #### Upload **POST** `/up` Receive content posted to the server The content is discarded by the endpoint. A response is sent once all the content has been received from the client. No query string parameters. ================================================ FILE: index.js ================================================ const handleRequest = require('./src'); addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); }); ================================================ FILE: package.json ================================================ { "name": "speedtest-worker", "private": true, "version": "1.0.0", "description": "Worker for testing connection speed", "author": "Vasco Asturiano ", "main": "index.js", "scripts": { "build": "wrangler build", "test": "NODE_ENV=test jest --verbose --no-cache", "test-dev": "NODE_ENV=test jest --watch --no-cache", "format": "prettier --write '**/*.{js,css,json,md}'" }, "husky": { "hooks": { "pre-commit": "yarn format", "pre-push": "yarn test && yarn format" } }, "dependencies": {}, "devDependencies": { "@babel/plugin-transform-runtime": "^7.4.4", "@babel/preset-env": "^7.4.5", "@babel/runtime": "^7.4.5", "@dollarshaveclub/cloudworker": "^0.0.11", "husky": "^2.4.1", "jest": "^24.8.0", "prettier": "^1.18.2", "request": "^2.88.0" } } ================================================ FILE: src/down.js ================================================ const DEFAULT_NUM_BYTES = 0; const MAX_BYTES = 1e8; const getQs = url => { const sp = url.split('?'); if (sp.length < 2) { return {}; // no qs } const qs = sp[1]; return Object.assign( {}, ...qs.split('&').map(s => { const sp = s.split('='); if (sp.length !== 2) { return {}; } return { [sp[0]]: sp[1] }; }) ); }; const genContent = (numBytes = 0) => '0'.repeat(Math.max(0, numBytes)); async function handleRequest(request) { const reqTime = new Date(); const qs = getQs(request.url); const numBytes = qs.hasOwnProperty('bytes') ? Math.min(MAX_BYTES, Math.abs(+qs.bytes)) : DEFAULT_NUM_BYTES; const res = new Response(genContent(numBytes)); res.headers.set('access-control-allow-origin', '*'); res.headers.set('timing-allow-origin', '*'); res.headers.set('cache-control', 'no-store'); res.headers.set('content-type', 'application/octet-stream'); request.cf && request.cf.colo && res.headers.set('cf-meta-colo', request.cf.colo); res.headers.set('cf-meta-request-time', +reqTime); res.headers.set( 'access-control-expose-headers', 'cf-meta-colo, cf-meta-request-time' ); return res; } module.exports = handleRequest; ================================================ FILE: src/index.js ================================================ const Router = require('./router'); const downHandler = require('./down'); const upHandler = require('./up'); async function handleRequest(request) { const r = new Router(); r.get('.*/down', downHandler); r.post('.*/up', upHandler); return await r.route(request); } module.exports = handleRequest; ================================================ FILE: src/router.js ================================================ /** * Helper functions that when passed a request will return a * boolean indicating if the request uses that HTTP method, * header, host or referrer. */ const Method = method => req => req.method.toLowerCase() === method.toLowerCase(); const Connect = Method('connect'); const Delete = Method('delete'); const Get = Method('get'); const Head = Method('head'); const Options = Method('options'); const Patch = Method('patch'); const Post = Method('post'); const Put = Method('put'); const Trace = Method('trace'); const Header = (header, val) => req => req.headers.get(header) === val; const Host = host => Header('host', host.toLowerCase()); const Referrer = host => Header('referrer', host.toLowerCase()); const Path = regExp => req => { const url = new URL(req.url); const path = url.pathname; const match = path.match(regExp) || []; return match[0] === path; }; /** * The Router handles determines which handler is matched given the * conditions present for each request. */ class Router { constructor() { this.routes = []; } handle(conditions, handler) { this.routes.push({ conditions, handler }); return this; } connect(url, handler) { return this.handle([Connect, Path(url)], handler); } delete(url, handler) { return this.handle([Delete, Path(url)], handler); } get(url, handler) { return this.handle([Get, Path(url)], handler); } head(url, handler) { return this.handle([Head, Path(url)], handler); } options(url, handler) { return this.handle([Options, Path(url)], handler); } patch(url, handler) { return this.handle([Patch, Path(url)], handler); } post(url, handler) { return this.handle([Post, Path(url)], handler); } put(url, handler) { return this.handle([Put, Path(url)], handler); } trace(url, handler) { return this.handle([Trace, Path(url)], handler); } all(handler) { return this.handle([], handler); } route(req) { const route = this.resolve(req); if (route) { return route.handler(req); } return new Response('resource not found', { status: 404, statusText: 'not found', headers: { 'content-type': 'text/plain' } }); } /** * resolve returns the matching route for a request that returns * true for all conditions (if any). */ resolve(req) { return this.routes.find(r => { if (!r.conditions || (Array.isArray(r) && !r.conditions.length)) { return true; } if (typeof r.conditions === 'function') { return r.conditions(req); } return r.conditions.every(c => c(req)); }); } } module.exports = Router; ================================================ FILE: src/test.js ================================================ const fetch = require('@dollarshaveclub/node-fetch'); const Request = fetch.Request; const Response = fetch.Response; const handleRequest = require('./index'); beforeAll(async () => { Object.assign(global, { Response }); }); describe('speedtest-down', () => { const getResponse = async numBytes => { const url = new URL( `https://someurl.com/down${ numBytes !== undefined ? `?bytes=${numBytes}` : '' }` ); return await handleRequest(new Request(url)); }; const getContent = async (...params) => await (await getResponse(...params)).text(); test('default bytes', async () => { const content = await getContent(); expect(content.length).toBeLessThan(100); }); describe('low request bytes', () => { [0, 1, 10, 50, 99].forEach(bytes => { test(`get ${bytes} bytes`, async () => { const content = await getContent(bytes); expect(content.length).toBeLessThan(100); }); }); }); describe('request bytes', () => { [100, 1e3, 1e6, 1e7].forEach(bytes => { test(`get ${bytes} bytes`, async () => { const content = await getContent(bytes); expect(content.length).toBe(bytes); }); }); }); test('max bytes', async () => { const content = await getContent(Infinity); expect(content.length).toBe(1e8); }); test('negative bytes', async () => { const content = await getContent(-100); expect(content.length).toBe(100); }); test('includes request time', async () => { const headers = (await getResponse()).headers; const reqTime = headers.get('cf-meta-request-time'); expect(reqTime).toBeDefined(); expect(+reqTime).toBeLessThanOrEqual(+new Date()); expect(+reqTime).toBeGreaterThan(+new Date() - 60 * 1000); }); }); describe('speedtest-up', () => { const getResponse = async ({ numBytes = 0, method = 'POST', ...other } = {}) => { const config = { method, ...other }; if (method === 'POST') { config.body = '0'.repeat(numBytes); config.headers = { 'content-length': numBytes }; } const req = new Request('https://someurl.com/up', config); return await handleRequest(req); }; const getContent = async (...params) => await (await getResponse(...params)).text(); test('get request', async () => { const content = await getContent({ method: 'GET' }); expect(content.length).toBeGreaterThan(0); }); test('empty post request', async () => { const content = await getContent({ numBytes: 0 }); expect(content.length).toBeGreaterThan(0); }); test('small post request', async () => { const content = await getContent({ numBytes: 10 }); expect(content.length).toBeGreaterThan(0); }); test('large post request', async () => { const content = await getContent({ numBytes: 1e8 }); expect(content.length).toBeGreaterThan(0); }); test('includes request time', async () => { const headers = (await getResponse()).headers; const reqTime = headers.get('cf-meta-request-time'); expect(reqTime).toBeDefined(); expect(+reqTime).toBeLessThanOrEqual(+new Date()); expect(+reqTime).toBeGreaterThan(+new Date() - 60 * 1000); }); }); ================================================ FILE: src/up.js ================================================ async function handleRequest(request) { const reqTime = new Date(); const res = new Response('ok'); res.headers.set('access-control-allow-origin', '*'); res.headers.set('timing-allow-origin', '*'); request.cf && request.cf.colo && res.headers.set('cf-meta-colo', request.cf.colo); res.headers.set('cf-meta-request-time', +reqTime); res.headers.set( 'access-control-expose-headers', 'cf-meta-colo, cf-meta-request-time' ); return res; } module.exports = handleRequest; ================================================ FILE: wrangler.toml ================================================ type = "webpack" zone_id = "" account_id = "" route = "" workers_dev = true