├── .eslintrc ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .release-it.json ├── CHANGELOG.md ├── README.md ├── functions └── _middleware.ts ├── index.d.ts ├── package-lock.json ├── package.json ├── tasks ├── copy-core.ts ├── publish.ts └── setPrivatePackageFlag.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "parser": "@typescript-eslint/parser", 6 | "parserOptions": { 7 | "sourceType": "module", 8 | "project": "tsconfig.json" 9 | }, 10 | "plugins": ["@typescript-eslint", "jest"], 11 | "ignorePatterns": [ 12 | "package.json", 13 | "package-lock.json", 14 | "tsconfig.json", 15 | "tsconfig.release.json", 16 | "esbuild.js", 17 | "build" 18 | ], 19 | "extends": [ 20 | "eslint:recommended", 21 | "plugin:@typescript-eslint/eslint-recommended", 22 | "plugin:@typescript-eslint/recommended", 23 | "plugin:jest/recommended", 24 | "prettier" 25 | ], 26 | "globals": { 27 | "fetch": false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | publish: 6 | name: release main branch 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: 16 13 | check-latest: true 14 | registry-url: https://npm.pkg.github.com 15 | scope: "@flaregun-net" 16 | - name: Install dependencies 17 | # Skip post-install scripts here, as a malicious script could steal NODE_AUTH_TOKEN. 18 | run: | 19 | npm ci --ignore-scripts 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | - uses: actions/setup-node@v3 23 | with: 24 | node-version: 16 25 | check-latest: true 26 | registry-url: https://registry.npmjs.org 27 | scope: "@flaregun-net" 28 | - name: Set git user 29 | run: | 30 | git config user.name "${GITHUB_ACTOR}" 31 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 32 | - name: Publish 33 | run: | 34 | npm run build 35 | npm run transform:private:off 36 | npm run release 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env* 3 | logs 4 | *.log 5 | npm-debug.log* 6 | .npmrc 7 | .yarnrc 8 | node_modules/ 9 | coverage 10 | build/ 11 | .vscode 12 | .idea/ 13 | .npm 14 | .eslintcache 15 | .secrets 16 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm install 5 | npm run precommit 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !build/* 3 | !build/** 4 | !index.d.ts 5 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "before:init": ["npm run prepublishOnly"], 4 | "after:release": ["npm run postpublish"] 5 | }, 6 | "git": { 7 | "tag": true, 8 | "commit": true, 9 | "push": true, 10 | "requireUpstream": false, 11 | "requireCleanWorkingDir": false, 12 | "pushArgs": ["--follow-tags"], 13 | "releaseName": "v${version}", 14 | "commitMessage": "v${version}" 15 | }, 16 | "github": { 17 | "release": false, 18 | "autoGenerate": true, 19 | "releaseName": "v${version}", 20 | "tokenRef": "GITHUB_TOKEN" 21 | }, 22 | "npm": { 23 | "publish": true, 24 | "skipChecks": true, 25 | "ignoreVersion": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.0.217-next.4](https://github.com/flaregun-net/proxyflare-for-pages/compare/v0.0.217-next.3...v0.0.217-next.4) (2023-01-01) 7 | 8 | **Note:** Version bump only for package @flaregun-net/proxyflare-for-pages 9 | 10 | 11 | 12 | 13 | 14 | ## [0.0.217-next.2](https://github.com/flaregun-net/proxyflare-for-pages/compare/v0.0.217-next.1...v0.0.217-next.2) (2022-12-30) 15 | 16 | **Note:** Version bump only for package @flaregun-net/proxyflare-for-pages 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proxyflare for Cloudflare Pages 2 | 3 | [Visit our website](https://flaregun.net/docs/latest/proxyflare/plugin/getting-started) for the full documentation. 4 | 5 | ## Overview 6 | 7 | Proxyflare is a reverse proxy that makes it easy to move HTTP traffic around your domain and across the internet. 8 | 9 | This package provides Proxyflare as a [Cloudflare Pages](https://developers.cloudflare.com/pages) plugin. Any website deployed on Cloudflare Pages may use Proxyflare. 10 | 11 | Proxyflare is a middleware layer that matches incoming requests to `Routes` in your `Configuration`. 12 | 13 | Refer to the Cloudflare documentation for more information about [Pages Middleware](https://developers.cloudflare.com/pages/platform/functions/middleware/), [Pages Functions](https://developers.cloudflare.com/pages/platform/functions) and other awesome community [Plugins](https://developers.cloudflare.com/pages/platform/functions/plugins/) that can enhance your website. 14 | 15 | ## Use cases 16 | 17 | ### Proxy traffic to another service 18 | 19 | Proxy traffic from a part of your domain to another service on the same domain or elsewhere on the internet 20 | 21 | #### Examples 22 | 23 | 1. Move traffic from `https://yoursite.com/api/*` to `https://your-hosted-api.com` 24 | 1. Host a service on `https://torrents.yoursite.com/*` that points to `http://yoursite.com:41321` 25 | 26 | #### Notes 27 | 28 | - Proxyflare works over `http(s):` and `ws(s):` (websockets) 29 | - A proxied service must be available on the public internet 30 | - Both standard and custom ports are supported (e.g. `80`, `443`, `8787`, etc.) 31 | 32 | [Learn more](https://flaregun.net/docs/latest/proxyflare/plugin/tutorials/proxying-services) 33 | 34 | ### Mount a hosted website on your domain 35 | 36 | #### Examples 37 | 38 | 1. Mount your React-powered documentation hosted at `https://hosted-docs.com` on `https://yoursite.com/docs/*` 39 | 1. Mount a WordPress site hosted at `https://some-wordpress-blog.com` on `https://yoursite.com/blog/*` 40 | 41 | #### Notes 42 | 43 | - Mounted websites should configure the base url to match its mounted pathname 44 | - Static resources such as stylesheets must be carefully added to `Route["to.website.resources"]` 45 | 46 | [Learn more](https://flaregun.net/docs/latest/proxyflare/plugin/tutorials/proxying-websites) 47 | 48 | ### Redirect traffic from one place to another 49 | 50 | 1. Version an API (e.g. redirect `/v2/api`) 51 | 1. Redirect stale content URLs 52 | 53 | #### Notes 54 | 55 | - Redirects are wildcard-compatible 56 | - Any 300-level status code is supported 57 | 58 | [Learn more](https://flaregun.net/docs/latest/proxyflare/plugin/tutorials/redirecting-traffic) 59 | 60 | ### Serve static content through Proxyflare 61 | 62 | 1. Publish unique `robots.txt` and other website metadata files around your domain 63 | 64 | #### Notes 65 | 66 | - Custom response headers are supported to set `Content-Type` for text, JSON, or others 67 | - Text files should be no larger than 16KB 68 | 69 | [Learn more](https://flaregun.net/docs/latest/proxyflare/plugin/tutorials/serving-static-responses) 70 | 71 | ## Install 72 | 73 | Install `@flaregun-net/proxyflare-for-pages` and `@cloudflare/workers-types` using your preferred Node.js package manager 74 | 75 | ```bash 76 | npm install @flaregun-net/proxyflare-for-pages 77 | npm install -D @cloudflare/workers-types 78 | ``` 79 | 80 | ## Plug in 81 | 82 | ### Scaffold 83 | 84 | In your Cloudflare Pages project, create a `functions/_middleware.ts` file. The name of this file must be exactly as written because Cloudflare Pages uses the file name internally for routing. If your project already has a `functions/_middleware.ts` that exports a single `onRequest` object, convert it to a list of middleware for convenience. Middleware is called in the order listed. 85 | 86 | The `onRequest` middlewares should include the following configuration. Notice that we wrap Proxyflare in a `PagesFunction` in order to use environment variables with Proxyflare. [Learn more](https://flaregun.net/docs/latest/proxyflare/plugin/tutorials/using-environment-variables/) about environment variables and secrets. 87 | 88 | ```ts 89 | import proxyflare from "@flaregun-net/proxyflare-for-pages" 90 | 91 | const routes: Route[] = [] 92 | 93 | // `PagesFunction` is from @cloudflare/workers-types 94 | export const onRequest: PagesFunction[] = [ 95 | (context) => 96 | proxyflare({ 97 | config: { 98 | global: { debug: true }, 99 | routes, 100 | }, 101 | })(context), 102 | // other Pages plugins and middleware 103 | ] 104 | ``` 105 | 106 | This is a barebones Proxyflare configuration with `debug` enabled that will help with set up and configuration. Learn more about [debugging Proxyflare](https://flaregun.net/docs/latest/proxyflare/plugin/tutorials/debugging-proxyflare/). 107 | 108 | ### Configure 109 | 110 | Next, you'll need to write your first `Route`. Check out the [use cases](#use-cases) section to find `Route` ideas. If you don't have one yet, try this example: 111 | 112 | ```ts 113 | const routes: Route[] = [ 114 | { 115 | from: { 116 | pattern: "yourdomain.com/proxyflare-example", 117 | alsoMatchWWWSubdomain: true, 118 | }, 119 | to: { url: "https://example.com" }, 120 | }, 121 | ] 122 | ``` 123 | 124 | Replace `yoursite.com` with your domain name. 125 | 126 | ### Deploy 127 | 128 | Once you have a `Route` set up, deploy a new version of your Cloudflare Pages website, and keep an eye on the deployment. Once the deployment is successful, navigate to your domain. 129 | 130 | For the example `Route` above, you should see `https://example.com` rendered at `yourdomain.com/proxyflare-example`. If you don't see it, refer to the [debugging Proxyflare](https://flaregun.net/docs/latest/proxyflare/plugin/tutorials/debugging-proxyflare/) section or reach out for help in Discord. 131 | 132 | ## Next steps 133 | 134 | Now that you're up and running, check out the [Tutorials](https://flaregun.net/docs/latest/proxyflare/plugin/tutorials/proxying-services/) to learn more about what you can do with Proxyflare. 135 | -------------------------------------------------------------------------------- /functions/_middleware.ts: -------------------------------------------------------------------------------- 1 | import { makeBaseContainer } from "@flaregun-net/app-utils" 2 | import type { PluginArgs } from ".." 3 | import { router } from "../build/core" 4 | 5 | type ProxyflarePagesPluginFunction< 6 | Env = unknown, 7 | Params extends string = string, 8 | Data extends Record = Record, 9 | > = PagesPluginFunction 10 | 11 | export const onRequest: ProxyflarePagesPluginFunction = async (context) => { 12 | const { pluginArgs } = context 13 | const { config } = pluginArgs 14 | 15 | const baseContainer = makeBaseContainer( 16 | { 17 | request: context.request, 18 | waitUntil: context.waitUntil, 19 | passThroughOnException: context.next, 20 | next: context.next, 21 | }, 22 | { 23 | appName: "proxyflare", 24 | loggerEndpoint: null, 25 | }, 26 | ) 27 | 28 | try { 29 | return router(baseContainer, config) 30 | } catch (error) { 31 | return baseContainer.passThroughOnException() as unknown as ReturnType< 32 | typeof context.next 33 | > 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { CoreTypes } from "./build" 2 | 3 | export type PluginArgs = { 4 | config: CoreTypes.Configuration 5 | } 6 | 7 | export default function ProxyflarePagesPluginFunction( 8 | args: PluginArgs, 9 | ): PagesFunction 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flaregun-net/proxyflare-for-pages", 3 | "version": "0.1.0", 4 | "description": "A reverse proxy for your website on Cloudflare Pages", 5 | "license": "MIT", 6 | "main": "build/index.js", 7 | "types": "index.d.ts", 8 | "keywords": [ 9 | "proxyflare", 10 | "cloudflare", 11 | "cloudflare pages", 12 | "cloudflare pages plugin", 13 | "cloudflare reverse proxy", 14 | "cloudflare nginx", 15 | "nginx", 16 | "reverse proxy" 17 | ], 18 | "files": [ 19 | "index.d.ts", 20 | "build/*.d.ts", 21 | "build/types/*.d.ts", 22 | "build/index.js", 23 | "build/core.js" 24 | ], 25 | "scripts": { 26 | "precommit": "lint-staged", 27 | "prepare": "husky install && npm run build", 28 | "prepublishOnly": "npm run build", 29 | "postpublish": "npm run transform:private:on", 30 | "build": "npm-run-all transform:private:off build:*", 31 | "build:types": "ts-node ./tasks/copy-core.ts", 32 | "build:functions": "npx wrangler@latest pages functions build --plugin --outfile=build/index.js", 33 | "transform:private:off": "ts-node ./tasks/setPrivatePackageFlag.ts --off", 34 | "transform:private:on": "ts-node ./tasks/setPrivatePackageFlag.ts", 35 | "publish-prerelease": "npm run release", 36 | "release": "ts-node ./tasks/publish.ts" 37 | }, 38 | "peerDependencies": { 39 | "@cloudflare/workers-types": "3.14.0" 40 | }, 41 | "devDependencies": { 42 | "@cloudflare/workers-types": "3.14.0", 43 | "@flaregun-net/app-utils": "0.1.0", 44 | "@flaregun-net/proxyflare-core": "0.1.0", 45 | "@types/jest": "^29.2.2", 46 | "@types/node": "^17.0.35", 47 | "@typescript-eslint/eslint-plugin": "~5.25.0", 48 | "@typescript-eslint/parser": "~5.25.0", 49 | "cross-fetch": "^3.1.5", 50 | "del": "^6.1.1", 51 | "eslint": "~7.30.0", 52 | "eslint-config-prettier": "~8.3.0", 53 | "eslint-plugin-jest": "^26.1.0", 54 | "husky": "^7.0.0", 55 | "jest": "^29.3.0", 56 | "jest-environment-miniflare": "^2.5.0", 57 | "lint-staged": "^12.3.1", 58 | "miniflare": "^2.5.0", 59 | "npm-run-all": "^4.1.5", 60 | "prettier": "^2.6.2", 61 | "release-it": "^15.5.1", 62 | "semver": "^7.3.8", 63 | "ts-jest": "^29.0.3", 64 | "ts-node": "^10.8.0", 65 | "typescript": "~4.5.2" 66 | }, 67 | "jest": { 68 | "testEnvironment": "miniflare", 69 | "transform": { 70 | "^.+\\.tsx?$": "ts-jest" 71 | }, 72 | "globals": { 73 | "ts-jest": { 74 | "tsconfig": "tsconfig.json", 75 | "useESM": true 76 | } 77 | }, 78 | "moduleFileExtensions": [ 79 | "ts", 80 | "tsx", 81 | "js", 82 | "jsx" 83 | ], 84 | "testTimeout": 30000, 85 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(ts|js)x?$", 86 | "coverageDirectory": "coverage", 87 | "collectCoverageFrom": [ 88 | "src/**/*.{ts,tsx,js,jsx}", 89 | "!src/**/*.d.ts" 90 | ] 91 | }, 92 | "lint-staged": { 93 | "*.{js,ts,tsx}": [ 94 | "eslint", 95 | "prettier --write", 96 | "npm run transform:private:on" 97 | ] 98 | }, 99 | "repository": { 100 | "type": "git", 101 | "url": "git@github.com:flaregun-net/proxyflare-for-pages.git" 102 | }, 103 | "publishConfig": { 104 | "access": "public" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tasks/copy-core.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process" 2 | import del from "del" 3 | import { cp } from "fs/promises" 4 | import path from "path" 5 | 6 | const buildCore: () => Promise<{ coreRoot: string; coreFile: string }> = () => 7 | new Promise((resolve, reject) => { 8 | try { 9 | if (process.env.GITHUB_ACTIONS) { 10 | const child = spawn( 11 | "npm", 12 | ["install", "@flaregun-net/proxyflare-core"], 13 | { 14 | env: process.env, 15 | }, 16 | ) 17 | 18 | child.on("close", () => { 19 | resolve({ 20 | coreRoot: "./node_modules/@flaregun-net/proxyflare-core", 21 | coreFile: "./build/index.js", 22 | }) 23 | }) 24 | 25 | return 26 | } 27 | 28 | const coreRoot = path.join(process.cwd(), "../proxyflare-core") 29 | 30 | const child = spawn("npm", ["run", "build"], { 31 | cwd: coreRoot, 32 | env: { 33 | ...process.env, 34 | NODE_ENV: "production", 35 | }, 36 | }) 37 | 38 | child.on("close", () => { 39 | resolve({ 40 | coreRoot, 41 | coreFile: "./build/index.js", 42 | }) 43 | }) 44 | } catch (error) { 45 | reject(error) 46 | } 47 | }) 48 | 49 | ;(() => 50 | buildCore().then(({ coreRoot, coreFile }) => 51 | Promise.all( 52 | ["build/types", "build/index.d.ts", [coreFile, "build/core.js"]].map( 53 | (paths) => { 54 | const [from, to] = Array.isArray(paths) 55 | ? [paths[0], paths[1]] 56 | : [paths, paths] 57 | 58 | return cp(path.join(coreRoot, from), path.join(process.cwd(), to), { 59 | recursive: true, 60 | }) 61 | }, 62 | ), 63 | ).then(() => 64 | ["build/types/schema*.ts", "build/types/core.d.ts"].map((pathname) => 65 | del(path.join(process.cwd(), pathname)), 66 | ), 67 | ), 68 | ))() 69 | -------------------------------------------------------------------------------- /tasks/publish.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process" 2 | import fetch from "cross-fetch" 3 | import path from "path" 4 | import semverParse from "semver/functions/parse" 5 | 6 | const PROJECT_ROOT = path.resolve(__dirname, "../") 7 | 8 | const getVersions = ( 9 | packageName: string, 10 | githubToken: string, 11 | orgName = "flaregun-net", 12 | ): Promise<{ name: string }[]> => 13 | fetch( 14 | `https://api.github.com/orgs/${orgName}/packages/npm/${packageName}/versions`, 15 | { 16 | headers: { 17 | "X-GitHub-Api-Version": "2022-11-28", 18 | Accept: "application/vnd.github+json", 19 | Authorization: `Bearer ${githubToken}`, 20 | }, 21 | }, 22 | ) 23 | .then((a) => { 24 | if (a.status !== 200) { 25 | throw new Error(`request failed with code ${a.status}`) 26 | } 27 | 28 | return a 29 | }) 30 | .then((a) => a.json()) 31 | 32 | const getLastVersion = async () => { 33 | const versions = await getVersions( 34 | "proxyflare-core", 35 | process.env.GITHUB_TOKEN, 36 | ) 37 | if (!versions.length) { 38 | throw new Error("no versions found") 39 | } 40 | const latestVersion = versions[0].name 41 | 42 | const parsed = semverParse(latestVersion) 43 | 44 | return { isPrerelease: parsed.prerelease.length > 0, version: parsed.version } 45 | } 46 | 47 | ;(async () => { 48 | // get latest version of proxyflare-core 49 | const { version } = await getLastVersion() 50 | 51 | const args = [version, "--ci", `--git.tagName ${version}`, "-VV"] 52 | 53 | console.log(`Publishing with ${args}`) 54 | 55 | const command = spawn("release-it", args, { cwd: PROJECT_ROOT }) 56 | 57 | command.stdout.on("data", (data) => { 58 | console.log(data.toString()) 59 | }) 60 | 61 | command.stderr.on("data", (data) => { 62 | console.log(data.toString()) 63 | }) 64 | 65 | command.on("error", (error) => { 66 | console.log(`error: ${error.message}`) 67 | }) 68 | 69 | command.on("close", (code) => { 70 | console.log(`child process exited with code ${code}`) 71 | }) 72 | })() 73 | -------------------------------------------------------------------------------- /tasks/setPrivatePackageFlag.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import path from "path" 3 | 4 | const getProjectPackage = (projectDir = process.cwd()) => 5 | JSON.parse(fs.readFileSync(path.join(projectDir, "package.json")).toString()) 6 | 7 | const setProjectPackage = (contents, projectDir = process.cwd()) => 8 | fs.writeFileSync( 9 | path.join(projectDir, "package.json"), 10 | JSON.stringify(contents, null, 2) + "\n", 11 | ) 12 | 13 | const init = () => { 14 | const flag = process.argv[2] 15 | 16 | const contents = getProjectPackage() 17 | 18 | if (flag === "--off") { 19 | delete contents.private 20 | } else { 21 | contents.private = true 22 | } 23 | 24 | setProjectPackage(contents) 25 | } 26 | 27 | init() 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "lib": ["ES2020"], 6 | "types": ["node"], 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "esModuleInterop": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------