Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cleanup #20

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
open-pull-requests-limit: 2
versioning-strategy: lockfile-only
schedule:
interval: "weekly"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
dist
node_modules
coverage

# config for `asdf` version manager.
.tool-versions
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@dbl-works:registry=https://npm.pkg.github.com
55 changes: 27 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,52 @@

Easily manage routing using Cloudflare Workers



## Usage

```typescript
import { startWorker } from '@dbl-works/cloudflare-router'

startWorker({
routes: {
'example.com': 's3://eu-central-1.assets.example.com',
```toml
# wrangler.toml
name = "my-worker"
routes = [
{
pattern = "*example.com/*",
zone_id = "5d7a8c0f96213b4e1a57b0c2f9478ec3"
},
edgeCacheTtl: 360 // seconds, Edge Cache TTL (Time to Live) specifies how long to cache a resource in the Cloudflare edge network
})
]
```



## Match rules

- Starting with `/` does a path only match
- Any other start will assume matching against `[domain][path]` as the value



## Basic Authentication

You can protect a deployment by defining basic auth in the config.

```typescript
startWorker({
deployments: [
{
accountId: '12345',
zoneId: 'abcdef',
routes: [
'*example.com/*',
],
auth: [
auth: [ // optional
{
type: 'basic',
username: 'test',
password: 'letmein',
password: 'lemmein',
},
{

}
],
},
],
routes: {
routes: { // define proxy routes
'production-example.com': 's3://eu-central-1.assets.example.com',
},
edgeCacheTtl: 360 // optional: Edge Cache TTL (Time to Live) specifies how long (in secs) to cache a resource in the Cloudflare edge network
})
```

## Match rules

- Starting with `/` does a path only match
- Any other start will assume matching against `[domain][path]` as the value

## Releases

- [Make sure you're logged in to npm.](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-with-a-personal-access-token)
- Switch to a branch named `chore/release/X.X.X` and make sure the changelog is up to date.
- In order to cut a release invoke `yarn release`. This will bump the version, update the changelog and push a new tag to the repo. The release will be automatically published to npm.
47 changes: 43 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,59 @@
{
"name": "cloudflare-router",
"version": "0.2.0",
"name": "@dbl-works/cloudflare-router",
"author": "dbl-works",
"version": "0.3.0",
"license": "MIT",
"main": "dist/index.js",
"description": "Cloudflare Router",
"homepage": "https://github.com/dbl-works/cloudflare-router#readme",
"bugs": {
"url": "https://github.com/dbl-works/cloudflare-router/issues"
},
"repository": "git+https://github.com/dbl-works/cloudflare-router.git",
"scripts": {
"build": "rm -rf dist && tsc",
"test": "yarn build && jest test/*.test.ts test/**/*.test.ts --coverage"
"test": "yarn build && jest test/*.test.ts test/**/*.test.ts --coverage",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"release": "release-it"
},
"eslintConfig": {
"root": true,
"extends": [
"typescript",
"prettier"
],
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "module"
},
"env": {
"node": true,
"es6": true
}
},
"release-it": {
"github": {
"release": true
}
},
"devDependencies": {
"@cloudflare/workers-types": "^3.0.0",
"@types/jest": "^27.0.2",
"@types/jest": "^29.5.5",
"@types/node-fetch": "^3.0.3",
"@typescript-eslint/eslint-plugin": "^5.32.0",
"@typescript-eslint/parser": "^5.32.0",
"eslint": "^8.50.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-typescript": "^3.0.0",
"jest": "^27.5.1",
"jest-environment-miniflare": "^2.14.1",
"jest-fetch-mock": "^3.0.3",
"release-it": "^16.2.1",
"ts-jest": "^27.0.7",
"ts-loader": "^9.2.6",
"typescript": "^4.4.4"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
3 changes: 2 additions & 1 deletion src/cloudflare-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import normalizeRequest from './utils/normalize-request'
import { withAuth } from './utils/with-auth'

export const startWorker = (config: Config) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addEventListener('fetch', (event: any) => {
withAuth(event, config, async () => {
const { request, cache }= normalizeRequest(event.request, config.routes)
const { request, cache } = normalizeRequest(event.request, config.routes)
const edgeCacheTtl = cache && config.edgeCacheTtl ? config.edgeCacheTtl : 0
return handleRequest(request, edgeCacheTtl)
})
Expand Down
11 changes: 2 additions & 9 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
interface Route {
remote: string
spa?: boolean
}

type RouteHandler = (request: Request) => Response

export type BasicAuthMethod = {
type: 'basic'
username: string
Expand All @@ -24,8 +17,8 @@ export interface Routes {
}

export interface Deployment {
accountId: string
zoneId: string
accountId?: string
zoneId?: string
Comment on lines +20 to +21
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are unused, leaving them here as optional properties, one might find it helpful to identify deployments in their worker code....

routes: string[]
auth?: AuthMethods[]
}
Expand Down
3 changes: 2 additions & 1 deletion src/utils/deployment-for-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Config, Deployment } from '../config'

export const deploymentForRequest = (request: Request, config: Config): Deployment | undefined => {
return config.deployments.find(deployment => {
return deployment.routes.find(route => {
return deployment.routes.some(route => {
// eslint-disable-next-line no-useless-escape
const sanitizedRoute = route.replace(/[^a-zA-Z0-9*\.\-\/]/g, '') // We only really want to allow the pattern *.example.com/*
const regexedRoute = sanitizedRoute.replace(/\./g, '\\.').replace(/\*/g, '(.*)')
const normalizedUrl = request.url.replace(/https?:\/\//, '')
Expand Down
32 changes: 14 additions & 18 deletions src/utils/handle-request.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
export default async function handleRequest(request: Request, edgeCacheTtl: number) {
export default async function handleRequest(request: Request, edgeCacheTtl: number): Promise<Response> {
// https://developers.cloudflare.com/workers/runtime-apis/request/#requestinitcfproperties
const cfOptions = edgeCacheTtl > 0 ? {
// Requests proxied through Cloudflare are cached even without Workers according to a zone’s default
// or configured behavior. Setting Cloudflare cache rules to further customised the behavior
cf: {
// NOTE: cf is Enterprise only feature
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samkahchiin I did not find in the documentation that this would be an enterprise-only feature, maybe it is publicly available now?

// This is equivalent to setting two Page Rules:
// - Edge Cache TTL and
// - Cache Level (to Cache Everything).
// Edge Cache TTL (Time to Live) specifies how long to cache a resource in the Cloudflare edge network
// The value must be zero or a positive number.
// A value of 0 indicates that the cache asset expires immediately.
// This option applies to GET and HEAD request methods only.
cacheTtlByStatus: { '200-299': edgeCacheTtl, '404': 1, '500-599': 0 },
// The Cloudflare CDN does not cache HTML by default without setting cacheEverything to true.
cacheEverything: true,
cacheKey: request.url
}
} : {}

//@ts-ignore
let response = await fetch(request, cfOptions);
const fetchResponse = await fetch(request, cfOptions as RequestInit);
const responseBodyBlob = await fetchResponse.blob();

// Reconstruct the Response object to make its headers mutable.
response = new Response(response.body, response);
response.headers.set('edge-cache-ttl', `${edgeCacheTtl}`)
const response = new Response(responseBodyBlob, {
status: fetchResponse.status,
statusText: fetchResponse.statusText,
headers: fetchResponse.headers,
});

if (edgeCacheTtl > 0) { // Only set the header if edgeCacheTtl is set
response.headers.set('edge-cache-ttl', `${edgeCacheTtl}`);
Copy link
Contributor Author

@marfoldi marfoldi Sep 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to set this to 0 if not defined?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole point of such a repo is to have sane defaults for all projects, i.e. ideally, we can always use this project without any options set at all, because we have great defaults for everything.

What would be a great default for caching?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I'm just wondering whether it wouldn't be unexpected that everything is getting cached let's say for a day. It would definitely be needed to add CircleCi --> Purge Cloudflare cache on deploy for every project as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed. Purging the cache after deploy should be in the standardized CircleCi config file :)

}

return response;
}

69 changes: 37 additions & 32 deletions src/utils/normalize-request.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,52 @@
import { Config } from '../config'
import { Config } from '../config';

const MEDIA_FILE_EXTENSIONS = 'css gif ico jpg js otf jpeg png svg ttf webp woff woff2 csv json'.split(' ')
const hasMediaFileExtension = (path: string): boolean => {
const ext = path.split('.').pop()?.toLowerCase()
return ext ? MEDIA_FILE_EXTENSIONS.includes(ext) : false
}

export default function normalizeRequest(request: Request, routes: Config['routes']): { request: Request, cache: boolean } {
const originalUrl = request.url
const originalUrlWithoutScheme = originalUrl.replace(/^https?:\/\//, '')
const path = originalUrlWithoutScheme.replace(/^.*?\//gi, '')
const MEDIA_FILE_EXTENSIONS = new Set([
'css',
'csv',
'gif',
'ico',
'jpeg',
'jpg',
'js',
'json',
'otf',
'png',
'svg',
'ttf',
'webp',
'woff',
'woff2',
]);
const hasMediaFileExtension = (path: string): boolean => MEDIA_FILE_EXTENSIONS.has(path.split('.').pop()?.toLowerCase() || '');

for (const [key, value] of Object.entries(routes)) {
let url = originalUrl
let newUrl = value
if (url.indexOf(key) !== -1) {

if (originalUrlWithoutScheme.startsWith(key) === false && key.startsWith('/') === false) {
break
}
export default function normalizeRequest(request: Request, routes: Config['routes']): { request: Request, cache: boolean } {
const url = new URL(request.url);
const originalUrlWithoutScheme = url.hostname + url.pathname;
const path = originalUrlWithoutScheme.replace(/^.*?\//gi, '');

const singlePageApp = newUrl.indexOf('s3://') === 0
const isMediaFile = hasMediaFileExtension(originalUrl)
for (const [route, replacement] of Object.entries(routes)) {
if (request.url.includes(route) && (originalUrlWithoutScheme.startsWith(route) || route.startsWith('/'))) {
let newUrl = replacement;
const singlePageApp = newUrl.startsWith('s3://');

const isMediaFile = hasMediaFileExtension(request.url)
if (singlePageApp) {
newUrl = newUrl.replace(new RegExp('s3://([^.]+).([^/]+)(/?)(.*)'), 'https://s3.$1.amazonaws.com/$2$3$4')
}

const lastChar = newUrl[newUrl.length - 1]
url = originalUrlWithoutScheme.replace(key, newUrl)
// console.log(path, { singlePageApp, isMediaFile })
let updatedUrl = originalUrlWithoutScheme.replace(route, newUrl)
if (singlePageApp && !isMediaFile) {
url = newUrl + '/index.html'
}
if (lastChar === '/') {
url = url + path
updatedUrl = newUrl + '/index.html'
}
if (url.indexOf('https://') !== 0) {
url = 'https://' + url
updatedUrl += newUrl.endsWith('/') ? updatedUrl + path : ''
if (!updatedUrl.startsWith('https://')) {
updatedUrl = 'https://' + updatedUrl
}
// Make sure we only cache requests from the stated routes
return { request: new Request(url), cache: true }

return { request: new Request(updatedUrl), cache: true };
}
}

return { request, cache: false }
return { request, cache: false };
}
2 changes: 2 additions & 0 deletions test/setup.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable */

import jestFetchMock from 'jest-fetch-mock'

jestFetchMock.enableMocks()
Expand Down
2 changes: 0 additions & 2 deletions test/utils/with-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ const MOCK_DEPLOYMENT_WITH_AUTH: Deployment = {
],
}
const MOCK_DEPLOYMENT_WITHOUT_AUTH: Deployment = {
accountId: '12345',
zoneId: '12345',
routes: [
'*example.com/*',
],
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"noImplicitAny": true,
"preserveConstEnums": true,
"outDir": "./dist",
"moduleResolution": "node"
"moduleResolution": "node",
},
"include": [
"index.ts",
Expand Down
Loading