[
  {
    "path": ".github/workflows/semgrep.yml",
    "content": "\non:\n  pull_request: {}\n  workflow_dispatch: {}\n  push: \n    branches:\n      - main\n      - master\n  schedule:\n    - cron: '0 0 * * *'\nname: Semgrep config\njobs:\n  semgrep:\n    name: semgrep/ci\n    runs-on: ubuntu-20.04\n    env:\n      SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}\n      SEMGREP_URL: https://cloudflare.semgrep.dev\n      SEMGREP_APP_URL: https://cloudflare.semgrep.dev\n      SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version\n    container:\n      image: returntocorp/semgrep\n    steps:\n      - uses: actions/checkout@v3\n      - run: semgrep ci\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/*\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"trailingComma\": \"none\",\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": "README.md",
    "content": "## Speed Test\n\nWorker for measuring download / upload connection speed from the client side, using the [Performance Timing API](https://w3c.github.io/perf-timing-primer/).\n\n### Installation\n\n[`index.js`](https://github.com/cloudflare/worker-speedtest-template/blob/master/router.js) is the content of the Workers script.\n\n_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.\n\n#### Wrangler\n\nYou 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:\n\n```\nwrangler generate myApp https://github.com/cloudflare/worker-speedtest-template\n```\n\nBefore 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/).\n\nOnce you are ready, you can publish your code by running the following command:\n\n```\nwrangler publish\n```\n\n#### Serverless\n\nTo deploy using serverless add a [`serverless.yml`](https://serverless.com/framework/docs/providers/cloudflare/) file.\n\n### API Reference\n\nThis worker exposes two endpoints designed to support the measuring of bandwidth and latency from the client side.\n\n#### Download\n\n**GET** `/down` Request binary content of a certain size\n\n| Param | Description                            | Required | Default |\n| ----- | -------------------------------------- | :------: | :-----: |\n| bytes | The size of the response body in bytes |    no    |    0    |\n\nExample: `/down?bytes=10000`\n\n#### Upload\n\n**POST** `/up` Receive content posted to the server\n\nThe content is discarded by the endpoint. A response is sent once all the content has been received from the client.\n\nNo query string parameters.\n"
  },
  {
    "path": "index.js",
    "content": "const handleRequest = require('./src');\n\naddEventListener('fetch', event => {\n  event.respondWith(handleRequest(event.request));\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"speedtest-worker\",\n  \"private\": true,\n  \"version\": \"1.0.0\",\n  \"description\": \"Worker for testing connection speed\",\n  \"author\": \"Vasco Asturiano <vasco@cloudflare.com>\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"wrangler build\",\n    \"test\": \"NODE_ENV=test jest --verbose --no-cache\",\n    \"test-dev\": \"NODE_ENV=test jest --watch --no-cache\",\n    \"format\": \"prettier --write '**/*.{js,css,json,md}'\"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"yarn format\",\n      \"pre-push\": \"yarn test && yarn format\"\n    }\n  },\n  \"dependencies\": {},\n  \"devDependencies\": {\n    \"@babel/plugin-transform-runtime\": \"^7.4.4\",\n    \"@babel/preset-env\": \"^7.4.5\",\n    \"@babel/runtime\": \"^7.4.5\",\n    \"@dollarshaveclub/cloudworker\": \"^0.0.11\",\n    \"husky\": \"^2.4.1\",\n    \"jest\": \"^24.8.0\",\n    \"prettier\": \"^1.18.2\",\n    \"request\": \"^2.88.0\"\n  }\n}\n"
  },
  {
    "path": "src/down.js",
    "content": "const DEFAULT_NUM_BYTES = 0;\nconst MAX_BYTES = 1e8;\n\nconst getQs = url => {\n  const sp = url.split('?');\n  if (sp.length < 2) {\n    return {}; // no qs\n  }\n  const qs = sp[1];\n\n  return Object.assign(\n    {},\n    ...qs.split('&').map(s => {\n      const sp = s.split('=');\n      if (sp.length !== 2) {\n        return {};\n      }\n\n      return { [sp[0]]: sp[1] };\n    })\n  );\n};\n\nconst genContent = (numBytes = 0) => '0'.repeat(Math.max(0, numBytes));\n\nasync function handleRequest(request) {\n  const reqTime = new Date();\n\n  const qs = getQs(request.url);\n\n  const numBytes = qs.hasOwnProperty('bytes')\n    ? Math.min(MAX_BYTES, Math.abs(+qs.bytes))\n    : DEFAULT_NUM_BYTES;\n\n  const res = new Response(genContent(numBytes));\n\n  res.headers.set('access-control-allow-origin', '*');\n  res.headers.set('timing-allow-origin', '*');\n  res.headers.set('cache-control', 'no-store');\n  res.headers.set('content-type', 'application/octet-stream');\n\n  request.cf &&\n    request.cf.colo &&\n    res.headers.set('cf-meta-colo', request.cf.colo);\n\n  res.headers.set('cf-meta-request-time', +reqTime);\n\n  res.headers.set(\n    'access-control-expose-headers',\n    'cf-meta-colo, cf-meta-request-time'\n  );\n\n  return res;\n}\n\nmodule.exports = handleRequest;\n"
  },
  {
    "path": "src/index.js",
    "content": "const Router = require('./router');\n\nconst downHandler = require('./down');\nconst upHandler = require('./up');\n\nasync function handleRequest(request) {\n  const r = new Router();\n\n  r.get('.*/down', downHandler);\n  r.post('.*/up', upHandler);\n\n  return await r.route(request);\n}\n\nmodule.exports = handleRequest;\n"
  },
  {
    "path": "src/router.js",
    "content": "/**\n * Helper functions that when passed a request will return a\n * boolean indicating if the request uses that HTTP method,\n * header, host or referrer.\n */\nconst Method = method => req =>\n  req.method.toLowerCase() === method.toLowerCase();\nconst Connect = Method('connect');\nconst Delete = Method('delete');\nconst Get = Method('get');\nconst Head = Method('head');\nconst Options = Method('options');\nconst Patch = Method('patch');\nconst Post = Method('post');\nconst Put = Method('put');\nconst Trace = Method('trace');\n\nconst Header = (header, val) => req => req.headers.get(header) === val;\nconst Host = host => Header('host', host.toLowerCase());\nconst Referrer = host => Header('referrer', host.toLowerCase());\n\nconst Path = regExp => req => {\n  const url = new URL(req.url);\n  const path = url.pathname;\n  const match = path.match(regExp) || [];\n  return match[0] === path;\n};\n\n/**\n * The Router handles determines which handler is matched given the\n * conditions present for each request.\n */\nclass Router {\n  constructor() {\n    this.routes = [];\n  }\n\n  handle(conditions, handler) {\n    this.routes.push({\n      conditions,\n      handler\n    });\n    return this;\n  }\n\n  connect(url, handler) {\n    return this.handle([Connect, Path(url)], handler);\n  }\n\n  delete(url, handler) {\n    return this.handle([Delete, Path(url)], handler);\n  }\n\n  get(url, handler) {\n    return this.handle([Get, Path(url)], handler);\n  }\n\n  head(url, handler) {\n    return this.handle([Head, Path(url)], handler);\n  }\n\n  options(url, handler) {\n    return this.handle([Options, Path(url)], handler);\n  }\n\n  patch(url, handler) {\n    return this.handle([Patch, Path(url)], handler);\n  }\n\n  post(url, handler) {\n    return this.handle([Post, Path(url)], handler);\n  }\n\n  put(url, handler) {\n    return this.handle([Put, Path(url)], handler);\n  }\n\n  trace(url, handler) {\n    return this.handle([Trace, Path(url)], handler);\n  }\n\n  all(handler) {\n    return this.handle([], handler);\n  }\n\n  route(req) {\n    const route = this.resolve(req);\n\n    if (route) {\n      return route.handler(req);\n    }\n\n    return new Response('resource not found', {\n      status: 404,\n      statusText: 'not found',\n      headers: {\n        'content-type': 'text/plain'\n      }\n    });\n  }\n\n  /**\n   * resolve returns the matching route for a request that returns\n   * true for all conditions (if any).\n   */\n  resolve(req) {\n    return this.routes.find(r => {\n      if (!r.conditions || (Array.isArray(r) && !r.conditions.length)) {\n        return true;\n      }\n\n      if (typeof r.conditions === 'function') {\n        return r.conditions(req);\n      }\n\n      return r.conditions.every(c => c(req));\n    });\n  }\n}\n\nmodule.exports = Router;\n"
  },
  {
    "path": "src/test.js",
    "content": "const fetch = require('@dollarshaveclub/node-fetch');\nconst Request = fetch.Request;\nconst Response = fetch.Response;\n\nconst handleRequest = require('./index');\n\nbeforeAll(async () => {\n  Object.assign(global, { Response });\n});\n\ndescribe('speedtest-down', () => {\n  const getResponse = async numBytes => {\n    const url = new URL(\n      `https://someurl.com/down${\n        numBytes !== undefined ? `?bytes=${numBytes}` : ''\n      }`\n    );\n    return await handleRequest(new Request(url));\n  };\n  const getContent = async (...params) =>\n    await (await getResponse(...params)).text();\n\n  test('default bytes', async () => {\n    const content = await getContent();\n    expect(content.length).toBeLessThan(100);\n  });\n\n  describe('low request bytes', () => {\n    [0, 1, 10, 50, 99].forEach(bytes => {\n      test(`get ${bytes} bytes`, async () => {\n        const content = await getContent(bytes);\n        expect(content.length).toBeLessThan(100);\n      });\n    });\n  });\n\n  describe('request bytes', () => {\n    [100, 1e3, 1e6, 1e7].forEach(bytes => {\n      test(`get ${bytes} bytes`, async () => {\n        const content = await getContent(bytes);\n        expect(content.length).toBe(bytes);\n      });\n    });\n  });\n\n  test('max bytes', async () => {\n    const content = await getContent(Infinity);\n    expect(content.length).toBe(1e8);\n  });\n\n  test('negative bytes', async () => {\n    const content = await getContent(-100);\n    expect(content.length).toBe(100);\n  });\n\n  test('includes request time', async () => {\n    const headers = (await getResponse()).headers;\n    const reqTime = headers.get('cf-meta-request-time');\n\n    expect(reqTime).toBeDefined();\n    expect(+reqTime).toBeLessThanOrEqual(+new Date());\n    expect(+reqTime).toBeGreaterThan(+new Date() - 60 * 1000);\n  });\n});\n\ndescribe('speedtest-up', () => {\n  const getResponse = async ({\n    numBytes = 0,\n    method = 'POST',\n    ...other\n  } = {}) => {\n    const config = { method, ...other };\n    if (method === 'POST') {\n      config.body = '0'.repeat(numBytes);\n      config.headers = { 'content-length': numBytes };\n    }\n\n    const req = new Request('https://someurl.com/up', config);\n    return await handleRequest(req);\n  };\n  const getContent = async (...params) =>\n    await (await getResponse(...params)).text();\n\n  test('get request', async () => {\n    const content = await getContent({ method: 'GET' });\n    expect(content.length).toBeGreaterThan(0);\n  });\n\n  test('empty post request', async () => {\n    const content = await getContent({ numBytes: 0 });\n    expect(content.length).toBeGreaterThan(0);\n  });\n\n  test('small post request', async () => {\n    const content = await getContent({ numBytes: 10 });\n    expect(content.length).toBeGreaterThan(0);\n  });\n\n  test('large post request', async () => {\n    const content = await getContent({ numBytes: 1e8 });\n    expect(content.length).toBeGreaterThan(0);\n  });\n\n  test('includes request time', async () => {\n    const headers = (await getResponse()).headers;\n    const reqTime = headers.get('cf-meta-request-time');\n\n    expect(reqTime).toBeDefined();\n    expect(+reqTime).toBeLessThanOrEqual(+new Date());\n    expect(+reqTime).toBeGreaterThan(+new Date() - 60 * 1000);\n  });\n});\n"
  },
  {
    "path": "src/up.js",
    "content": "async function handleRequest(request) {\n  const reqTime = new Date();\n\n  const res = new Response('ok');\n\n  res.headers.set('access-control-allow-origin', '*');\n  res.headers.set('timing-allow-origin', '*');\n\n  request.cf &&\n    request.cf.colo &&\n    res.headers.set('cf-meta-colo', request.cf.colo);\n\n  res.headers.set('cf-meta-request-time', +reqTime);\n\n  res.headers.set(\n    'access-control-expose-headers',\n    'cf-meta-colo, cf-meta-request-time'\n  );\n\n  return res;\n}\n\nmodule.exports = handleRequest;\n"
  },
  {
    "path": "wrangler.toml",
    "content": "type = \"webpack\"\nzone_id = \"\"\naccount_id = \"\"\nroute = \"\"\nworkers_dev = true\n"
  }
]