74 Commits

Author SHA1 Message Date
William Oldham
5a09e0c602 Merge pull request #26 from movie-web/dev
Version 2.1.3: Push updates to support AWS
2024-01-06 20:54:30 +00:00
mrjvs
8852fca320 Merge branch 'master' into dev 2024-01-06 21:53:48 +01:00
William Oldham
1e147a793d Bump version 2024-01-06 20:52:02 +00:00
William Oldham
e88a4f3203 Bump dep versions and add error handling 2024-01-06 20:51:48 +00:00
William Oldham
7dc9d1809f Merge pull request #25 from movie-web/dev
Proxy v2.1.2
2024-01-06 17:48:18 +00:00
William Oldham
02b4dca218 Merge pull request #24 from movie-web/block-more-headers
Block more headers
2024-01-06 17:47:17 +00:00
mrjvs
5faca36cb4 Add ability to do debug logging with REQ_DEBUG=true 2024-01-06 18:44:06 +01:00
mrjvs
ad0ae4aaae Add version identifying on proxy 2024-01-06 18:37:40 +01:00
mrjvs
07a87b4571 Block more headers, where possible 2024-01-06 18:32:53 +01:00
William Oldham
e216a59cbb Merge pull request #23 from movie-web/dev
Version 2.1.1: Fix support for JWT on non-Cloudflare platforms
2024-01-06 14:38:42 +00:00
William Oldham
015f15d2e7 Merge branch 'master' into dev 2024-01-06 14:38:02 +00:00
William Oldham
3a1e8688cc Merge pull request #22 from movie-web/user-agent-support
User agent proxying support
2024-01-06 14:35:40 +00:00
mrjvs
d348892158 bump version 2024-01-04 21:06:50 +01:00
mrjvs
3d192e8bb8 Do proper proxying 2024-01-04 20:57:54 +01:00
mrjvs
882e26fa1b Upgrade h3 2024-01-04 20:16:53 +01:00
mrjvs
054ea6aa07 Support overwriting user agent 2024-01-04 19:54:24 +01:00
mrjvs
8c503269d1 Fixed AWS and NodeJS support 2024-01-04 19:54:14 +01:00
mrjvs
15b438be48 Create LICENSE 2023-12-23 23:25:03 +01:00
William Oldham
88b1852a91 Merge pull request #20 from movie-web/dev
Simple proxy v2.1.0
2023-12-20 15:39:43 +00:00
mrjvs
8c89f79441 Merge branch 'master' into dev 2023-12-20 16:38:58 +01:00
William Oldham
a03e1c1b59 Merge pull request #19 from movie-web/turnstile
Add turnstile integration
2023-12-20 15:38:11 +00:00
mrjvs
9e5d1a2993 Bump version number 2023-12-20 15:32:00 +01:00
mrjvs
0a553a8b84 Change error to a message 2023-12-20 14:44:21 +01:00
mrjvs
9ef1467ee1 Finish ip fetching 2023-12-20 14:37:34 +01:00
mrjvs
ed4d8826ce Add turnstile integration 2023-12-20 14:32:15 +01:00
William Oldham
3e63fe5b61 Merge pull request #18 from movie-web/binaryoverload-patch-1
Fix incorrect Docker command in release action
2023-11-26 22:54:39 +00:00
William Oldham
0500b7caa5 Update release.yml 2023-11-26 22:53:07 +00:00
William Oldham
193fcc06f7 Update release.yml 2023-11-26 22:52:29 +00:00
mrjvs
eb58298582 Merge pull request #17 from movie-web/dependabot/npm_and_yarn/undici-5.27.0
Bump undici from 5.24.0 to 5.27.0
2023-10-31 22:44:36 +01:00
dependabot[bot]
655b053fd6 Bump undici from 5.24.0 to 5.27.0
Bumps [undici](https://github.com/nodejs/undici) from 5.24.0 to 5.27.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.24.0...v5.27.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-29 20:20:42 +00:00
William Oldham
67a7c55a88 Merge pull request #16 from movie-web/dev
Simple proxy v2.0.1
2023-10-16 21:34:47 +01:00
mrjvs
37802661ad bump version 2023-10-16 22:25:46 +02:00
mrjvs
c0ce4c9e84 Fix forgotten awaits + prevent double reading 2023-10-16 22:25:37 +02:00
mrjvs
3e8d413a87 fix nodejs zip path 2023-10-16 20:15:39 +02:00
William Oldham
3678522e20 Merge pull request #15 from movie-web/dev
Simple proxy v2.0.0
2023-10-16 19:11:24 +01:00
mrjvs
54b40c1be1 Merge branch 'master' into dev 2023-10-16 20:09:04 +02:00
mrjvs
6d27577ca4 blacklist more headers 2023-10-16 20:03:57 +02:00
mrjvs
f890f59d43 remove cf internal headers before proxying 2023-10-16 20:01:29 +02:00
mrjvs
714b91ef8c set instead of append (append doubles header values) 2023-10-02 20:34:52 +02:00
William Oldham
96dc6d21d1 Merge pull request #13 from movie-web/nitro-todos
Final TODOS
2023-09-15 21:59:43 +01:00
mrjvs
38556eda4a update pnpm actions 2023-09-15 19:10:59 +02:00
mrjvs
bd45c86ef5 switch to pnpm and add import aliasing 2023-09-14 20:19:07 +02:00
mrjvs
2583a5126f Fix linter 2023-09-14 20:02:48 +02:00
mrjvs
951042e1f8 fix deploy button 2023-09-14 20:01:04 +02:00
mrjvs
56e84a2a3a better error handling 2023-09-14 19:55:50 +02:00
mrjvs
9ff25a4e61 remove todos 2023-09-14 19:41:08 +02:00
mrjvs
0e386dc21d Releases for all supported platforms 2023-09-14 19:40:47 +02:00
mrjvs
1b5a306879 cloudflare deploy button 2023-09-14 19:27:34 +02:00
William Oldham
d42c5d6270 Merge pull request #12 from zisra/dev
(refactor): Use Nitro web server
2023-09-13 22:48:06 +01:00
mrjvs
e6000b36f0 fix problems in ci 2023-09-13 23:11:15 +02:00
mrjvs
c2ae6432ae add CI stuff 2023-09-13 23:08:44 +02:00
mrjvs
95f0026f5a update readme 2023-09-13 22:58:05 +02:00
mrjvs
5a070b4a15 fix linter + make proxy work + remove temp files + fix typescript types 2023-09-13 22:54:28 +02:00
b18d07a6a5 refactor: use proxyRequest and handleCors 2023-09-10 22:36:08 -05:00
293a4c0b81 fix: ditch node-fetch 2023-09-09 23:02:43 -05:00
c0f50f0410 refactor: Bring back deleted changes 2023-09-09 23:00:56 -05:00
3936dd0765 fix: Add host header to proxy request 2023-09-09 04:22:06 -05:00
4d876b3298 fix(actions): update node version 2023-09-09 03:27:31 -05:00
f2f3f2dccd chore: remove unnecessary logs 2023-09-09 02:36:28 -05:00
af1331bcc2 fix: CORS and body 2023-09-09 01:47:34 -05:00
6e7df4e107 fix(actions): Revert update actions 2023-09-08 23:49:15 -05:00
598d2862f3 fix(actions): Use npm instead of yarn 2023-09-08 23:40:52 -05:00
1495f3bb5a fix(actions): Correct actions trigger 2023-09-08 23:36:29 -05:00
8f4c6eb857 fix(actions): Build worker using nitro 2023-09-08 23:34:36 -05:00
76fc23d88a fix(actions): Remove test 2023-09-08 23:29:48 -05:00
883078fdbc Initial setup 2023-09-08 22:59:05 -05:00
c5233b7088 Initiate repository 2023-09-06 16:34:21 -05:00
James Hawkins
23c090fc15 Merge pull request #10 from movie-web/dev
Add X-Final-Destination for 2embed
2023-05-10 10:56:21 +01:00
James Hawkins
f2d4e523a3 Merge branch 'master' into dev 2023-05-10 10:55:32 +01:00
James Hawkins
84e91bf422 Merge pull request #9 from Jordaar/dev
Add X-Final-Destination header to retrieve final redirected URL
2023-05-10 10:53:28 +01:00
JORDAAR
d4352e7189 Add X-Final-Destination header 2023-05-09 12:23:37 +05:30
James Hawkins
c03aaf8e3d Merge pull request #7 from movie-web/dev
Fix cookies
2023-01-24 10:32:00 +00:00
James Hawkins
256e6307f6 Merge pull request #6 from movie-web/fix-cookies
cookies bugfix
2023-01-24 07:08:14 +00:00
Jelle van Snik
091fc3e36e fix cookies 2023-01-24 00:00:13 +01:00
30 changed files with 5033 additions and 3413 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
node_modules
*.log*
.nitro
.cache
.output
.env
dist

3
.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
dist
.output
node-modules

50
.eslintrc.js Normal file
View File

@@ -0,0 +1,50 @@
module.exports = {
env: {
browser: true,
},
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
ignorePatterns: ["public/*", "dist/*", "/*.js", "/*.ts"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: "./",
},
settings: {
"import/resolver": {
typescript: {
project: "./tsconfig.json",
},
},
},
plugins: ["@typescript-eslint", "import", "prettier"],
rules: {
"no-underscore-dangle": "off",
"@typescript-eslint/no-explicit-any": "off",
"no-console": "off",
"@typescript-eslint/no-this-alias": "off",
"import/prefer-default-export": "off",
"@typescript-eslint/no-empty-function": "off",
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
"no-restricted-syntax": "off",
"import/no-unresolved": ["error", { ignore: ["^virtual:"] }],
"consistent-return": "off",
"no-continue": "off",
"no-eval": "off",
"no-await-in-loop": "off",
"no-nested-ternary": "off",
"prefer-destructuring": "off",
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"import/extensions": [
"error",
"ignorePackages",
{
ts: "never",
tsx: "never",
},
]
},
}

View File

@@ -1,18 +0,0 @@
{
"env": {
"worker": true,
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": "latest"
},
"root": true,
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
"plugins": [],
"ignorePatterns": ["dist"],
"rules": {
"prettier/prettier": "error",
"no-undef": "off"
}
}

View File

@@ -1,60 +0,0 @@
name: Build Release
on:
push:
branches:
- master
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Node.js
uses: actions/setup-node@v1
with:
node-version: 16
- name: Install NPM packages
run: npm install
- name: Build project
run: npm run build
- name: Upload production-ready build files
uses: actions/upload-artifact@v3
with:
name: worker.js
path: ./dist/worker.js
- name: Bump version and push tag
id: tag_version
uses: mathieudutour/github-tag-action@v6.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.tag_version.outputs.new_tag }}
release_name: Simple Proxy Worker
draft: false
prerelease: false
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/worker.js
asset_name: worker.js
asset_content_type: text/javascript

36
.github/workflows/cloudflare.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Deploy Worker
# this action is for the "deploy to cloudflare" button
# repository_dispatch is triggered by CF
# secrets should also be made by CF
on: ["repository_dispatch"]
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: latest
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install packages
run: pnpm install --frozen-lockfile
- name: Build Project
run: pnpm build:cloudflare
- name: Build & Deploy Worker
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

60
.github/workflows/linting.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Linting and Testing
on:
push:
branches:
- master
- dev
pull_request:
jobs:
linting:
name: Run Linters
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: latest
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install packages
run: pnpm install --frozen-lockfile
- name: Prepare for linting
run: pnpm prepare
- name: Run ESLint
run: pnpm lint
building:
name: Build project
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: latest
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install pnpm packages
run: pnpm install --frozen-lockfile
- name: Build Project
run: pnpm build

View File

@@ -1,42 +0,0 @@
name: Linting and Testing
on:
push:
branches:
- master
- dev
pull_request_target:
types: [opened, reopened, synchronize]
jobs:
linting:
name: Run Linters
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Node.js
uses: actions/setup-node@v1
with:
node-version: 16
- name: Install NPM packages
run: npm install
- name: Run ESLint Report
run: npm run lint:report
# continue on error, so it still reports it in the next step
continue-on-error: true
- name: Annotate Code Linting Results
uses: ataylorme/eslint-annotate-action@v2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
report-json: "eslint_report.json"
- name: Build Project
run: npm run build

55
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Docker Publish
on:
push:
branches:
- master
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v2
- name: Get version
id: package-version
uses: martinbeentjes/npm-get-version-action@main
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=auto
tags: |
type=semver,pattern={{version}},value=v${{ steps.package-version.outputs.current-version }}
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
with:
push: true
context: .
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.meta.outputs.tags }}

81
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: Release
on:
push:
branches:
- master
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: latest
- name: Get version
id: package-version
uses: martinbeentjes/npm-get-version-action@main
- name: Install packages
run: pnpm install --frozen-lockfile
- name: Build for cloudflare
run: pnpm build:cloudflare && cp ./.output/server/index.mjs ./cloudflare.worker.mjs
- name: Build for AWS
run: pnpm build:aws && cd .output/server && zip -r ../../lambda.zip .
- name: Build for Node
run: pnpm build:node && cd .output/server && zip -r ../../nodejs.zip .
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ steps.package-version.outputs.current-version }}
release_name: Bot v${{ steps.package-version.outputs.current-version }}
draft: false
prerelease: false
body: |
Instead of downloading a package, you can also run it in docker:
```sh
docker run ghcr.io/movie-web/simple-proxy:${{ steps.package-version.outputs.current-version }}
```
- name: Upload cloudflare build
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./cloudflare.worker.mjs
asset_name: simple-proxy-cloudflare.mjs
asset_content_type: text/javascript
- name: Upload AWS build
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./lambda.zip
asset_name: simple-proxy-aws-lambda.zip
asset_content_type: application/zip
- name: Upload Node build
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./nodejs.zip
asset_name: simple-proxy-nodejs.zip
asset_content_type: application/zip

5
.gitignore vendored
View File

@@ -1,2 +1,7 @@
node_modules
*.log*
.nitro
.cache
.output
.env
dist

View File

@@ -1,6 +1,3 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig"
]
"recommendations": ["dbaeumer.vscode-eslint", "editorconfig.editorconfig"]
}

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM node:18-alpine as base
WORKDIR /app
# Build layer
FROM base as build
RUN npm i -g pnpm
COPY pnpm-lock.yaml package.json ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# Production layer
FROM base as production
EXPOSE 3000
ENV NODE_ENV=production
COPY --from=build /app/.output ./.output
CMD ["node", ".output/server/index.mjs"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 movie-web
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,3 +1,20 @@
# Cloudflare worker proxy
# simple-proxy
Simple http proxy in a cloudflare worker.
Simple reverse proxy to bypass CORS, used by [movie-web](https://movie-web.app).
Read the docs at https://docs.movie-web.app/proxy
---
### features:
- Deployable on many platforms - thanks to nitro
- header rewrites - read and write protected headers
- bypass CORS - always allows browser to send requests through it
- secure it with turnstile - prevent bots from using your proxy
> [!WARNING]
> Turnstile integration only works properly with cloudflare workers as platform
### supported platforms:
- cloudflare workers
- AWS lambda
- nodejs

14
nitro.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import { join } from "path";
import pkg from "./package.json";
//https://nitro.unjs.io/config
export default defineNitroConfig({
noPublicDir: true,
srcDir: "./src",
runtimeConfig: {
version: pkg.version
},
alias: {
"@": join(__dirname, "src")
}
});

3090
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,31 @@
{
"name": "simple-proxy",
"private": true,
"version": "1.0.0",
"version": "2.1.3",
"scripts": {
"build": "vite build",
"lint": "eslint --ext .js src/",
"lint:fix": "eslint --fix --ext .js src/",
"lint:report": "eslint --ext .js --output-file eslint_report.json --format json src/"
"prepare": "nitropack prepare",
"dev": "nitropack dev",
"build": "nitropack build",
"build:cloudflare": "NITRO_PRESET=cloudflare npm run build",
"build:aws": "NITRO_PRESET=aws_lambda npm run build",
"build:node": "NITRO_PRESET=node-server npm run build",
"start": "node .output/server/index.mjs",
"lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/",
"preinstall": "npx only-allow pnpm"
},
"dependencies": {
"h3": "^1.10.0",
"jose": "^5.2.0",
"nitropack": "^2.8.1"
},
"devDependencies": {
"eslint": "^8.30.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"vite": "^4.0.0",
"vite-plugin-eslint": "^1.8.1"
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.7.0",
"eslint": "^8.48.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.6.0",
"eslint-plugin-prettier": "^5.0.0"
}
}

4261
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,170 +0,0 @@
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
'Access-Control-Max-Age': '86400',
};
async function handleRequest(oRequest, destination, iteration = 0) {
console.log(
`PROXYING ${destination}${iteration ? ' ON ITERATION ' + iteration : ''}`,
);
// Create a new mutable request object for the destination
const request = new Request(destination, oRequest);
request.headers.set('Origin', new URL(destination).origin);
// TODO - Make cookie handling better. PHPSESSID overwrites all other cookie related headers
// Add custom X headers from client
// These headers are usually forbidden to be set by fetch
if (oRequest.headers.has('X-Cookie')) {
request.headers.set('Cookie', oRequest.headers.get('X-Cookie'));
request.headers.delete('X-Cookie');
}
if (request.headers.has('X-Referer')) {
request.headers.set('Referer', request.headers.get('X-Referer'));
request.headers.delete('X-Referer');
}
if (request.headers.has('X-Origin')) {
request.headers.set('Origin', request.headers.get('X-Origin'));
request.headers.delete('X-Origin');
}
// Set PHPSESSID cookie
if (request.headers.get('PHPSESSID')) {
request.headers.set(
'Cookie',
`PHPSESSID=${request.headers.get('PHPSESSID')}`,
);
}
// Set User Agent, if not exists
const useragent = request.headers.get('User-Agent');
if (!useragent) {
request.headers.set(
'User-Agent',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0',
);
}
// Fetch the new resource
const oResponse = await fetch(request.clone());
// If the server returned a redirect, follow it
if (
(oResponse.status === 302 || oResponse.status === 301) &&
oResponse.headers.get('location')
) {
// Server tried to redirect too many times
if (iteration > 5) {
return new Response('418 Too many redirects', {
status: 418,
});
}
// Handle and return the request for the redirected destination
return await handleRequest(
request,
oResponse.headers.get('location'),
iteration + 1,
);
}
// Create mutable response using the original response as init
const response = new Response(oResponse.body, oResponse);
// Set CORS headers
response.headers.set('Access-Control-Allow-Origin', '*');
response.headers.set('Access-Control-Expose-Headers', '*');
const cookiesToSet = response.headers.get('Set-Cookie');
// Transfer Set-Cookie to X-Set-Cookie
// Normally the Set-Cookie header is not accessible to fetch clients
if (cookiesToSet) {
response.headers.set('X-Set-Cookie', response.headers.get('Set-Cookie'));
}
// Set PHPSESSID cookie
if (
cookiesToSet &&
cookiesToSet.includes('PHPSESSID') &&
cookiesToSet.includes(';')
) {
let phpsessid = cookies.slice(cookies.search('PHPSESSID') + 10);
phpsessid = phpsessid.slice(0, phpsessid.search(';'));
response.headers.set('PHPSESSID', phpsessid);
}
// Append to/Add Vary header so browser will cache response correctly
response.headers.append('Vary', 'Origin');
return response;
}
function handleOptions(request) {
// Make sure the necessary headers are present
// for this to be a valid pre-flight request
const headers = request.headers;
let response = new Response(null, {
headers: {
Allow: 'GET, HEAD, POST, OPTIONS',
},
});
if (
headers.get('Origin') !== null &&
headers.get('Access-Control-Request-Method') !== null &&
headers.get('Access-Control-Request-Headers') !== null
) {
response = new Response(null, {
headers: {
...corsHeaders,
// Allow all future content Request headers to go back to browser
// such as Authorization (Bearer) or X-Client-Name-Version
'Access-Control-Allow-Headers': request.headers.get(
'Access-Control-Request-Headers',
),
},
});
}
return response;
}
addEventListener('fetch', (event) => {
const request = event.request;
const url = new URL(request.url);
const destination = url.searchParams.get('destination');
console.log(`HTTP ${request.method} - ${request.url}`);
let response = new Response('404 Not Found', {
status: 404,
});
if (request.method === 'OPTIONS') {
// Handle CORS preflight requests
response = handleOptions(request);
} else if (!destination) {
response = new Response('200 OK', {
status: 200,
headers: {
Allow: 'GET, HEAD, POST, OPTIONS',
'Access-Control-Allow-Origin': '*',
},
});
} else if (
request.method === 'GET' ||
request.method === 'HEAD' ||
request.method === 'POST'
) {
// Handle request
response = handleRequest(request, destination);
}
event.respondWith(response);
});

62
src/routes/index.ts Normal file
View File

@@ -0,0 +1,62 @@
import { getBodyBuffer } from '@/utils/body';
import {
getProxyHeaders,
getAfterResponseHeaders,
getBlacklistedHeaders,
} from '@/utils/headers';
import {
createTokenIfNeeded,
isAllowedToMakeRequest,
setTokenHeader,
} from '@/utils/turnstile';
export default defineEventHandler(async (event) => {
// handle cors, if applicable
if (isPreflightRequest(event)) return handleCors(event, {});
// parse destination URL
const destination = getQuery<{ destination?: string }>(event).destination;
if (!destination)
return await sendJson({
event,
status: 200,
data: {
message: `Proxy is working as expected (v${
useRuntimeConfig(event).version
})`,
},
});
if (!(await isAllowedToMakeRequest(event)))
return await sendJson({
event,
status: 401,
data: {
error: 'Invalid or missing token',
},
});
// read body
const body = await getBodyBuffer(event);
const token = await createTokenIfNeeded(event);
// proxy
try {
await specificProxyRequest(event, destination, {
blacklistedHeaders: getBlacklistedHeaders(),
fetchOptions: {
redirect: 'follow',
headers: getProxyHeaders(event.headers),
body,
},
onResponse(outputEvent, response) {
const headers = getAfterResponseHeaders(response.headers, response.url);
setResponseHeaders(outputEvent, headers);
if (token) setTokenHeader(event, token);
},
});
} catch (e) {
console.log('Error fetching', e);
throw e;
}
});

13
src/utils/body.ts Normal file
View File

@@ -0,0 +1,13 @@
import { H3Event } from 'h3';
export function hasBody(event: H3Event) {
const method = event.method.toUpperCase();
return ['PUT', 'POST', 'PATCH', 'DELETE'].includes(method);
}
export async function getBodyBuffer(
event: H3Event,
): Promise<Buffer | undefined> {
if (!hasBody(event)) return;
return await readRawBody(event, false);
}

72
src/utils/headers.ts Normal file
View File

@@ -0,0 +1,72 @@
const headerMap: Record<string, string> = {
'X-Cookie': 'Cookie',
'X-Referer': 'Referer',
'X-Origin': 'Origin',
'X-User-Agent': 'User-Agent',
'X-X-Real-Ip': 'X-Real-Ip',
};
const blacklistedHeaders = [
'cf-connecting-ip',
'cf-worker',
'cf-ray',
'cf-visitor',
'cf-ew-via',
'cdn-loop',
'x-amzn-trace-id',
'cf-ipcountry',
'x-forwarded-for',
'x-forwarded-host',
'x-forwarded-proto',
'forwarded',
'x-real-ip',
'content-length',
...Object.keys(headerMap),
];
function copyHeader(
headers: Headers,
outputHeaders: Headers,
inputKey: string,
outputKey: string,
) {
if (headers.has(inputKey))
outputHeaders.set(outputKey, headers.get(inputKey) ?? '');
}
export function getProxyHeaders(headers: Headers): Headers {
const output = new Headers();
// default user agent
output.set(
'User-Agent',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0',
);
Object.entries(headerMap).forEach((entry) => {
copyHeader(headers, output, entry[0], entry[1]);
});
return output;
}
export function getAfterResponseHeaders(
headers: Headers,
finalUrl: string,
): Record<string, string> {
const output: Record<string, string> = {};
if (headers.has('Set-Cookie'))
output['X-Set-Cookie'] = headers.get('Set-Cookie') ?? '';
return {
'Access-Control-Allow-Origin': '*',
'Access-Control-Expose-Headers': '*',
Vary: 'Origin',
'X-Final-Destination': finalUrl,
};
}
export function getBlacklistedHeaders() {
return blacklistedHeaders;
}

10
src/utils/ip.ts Normal file
View File

@@ -0,0 +1,10 @@
import { EventHandlerRequest, H3Event } from 'h3';
export function getIp(event: H3Event<EventHandlerRequest>) {
const value = getHeader(event, 'CF-Connecting-IP');
if (!value)
throw new Error(
'Ip header not found, turnstile only works on cloudflare workers',
);
return value;
}

92
src/utils/proxy.ts Normal file
View File

@@ -0,0 +1,92 @@
import {
H3Event,
Duplex,
ProxyOptions,
getProxyRequestHeaders,
RequestHeaders,
} from 'h3';
const PayloadMethods = new Set(['PATCH', 'POST', 'PUT', 'DELETE']);
export interface ExtraProxyOptions {
blacklistedHeaders?: string[];
}
function mergeHeaders(
defaults: HeadersInit,
...inputs: (HeadersInit | RequestHeaders | undefined)[]
) {
const _inputs = inputs.filter(Boolean) as HeadersInit[];
if (_inputs.length === 0) {
return defaults;
}
const merged = new Headers(defaults);
for (const input of _inputs) {
if (input.entries) {
for (const [key, value] of (input.entries as any)()) {
if (value !== undefined) {
merged.set(key, value);
}
}
} else {
for (const [key, value] of Object.entries(input)) {
if (value !== undefined) {
merged.set(key, value);
}
}
}
}
return merged;
}
export async function specificProxyRequest(
event: H3Event,
target: string,
opts: ProxyOptions & ExtraProxyOptions = {},
) {
let body;
let duplex: Duplex | undefined;
if (PayloadMethods.has(event.method)) {
if (opts.streamRequest) {
body = getRequestWebStream(event);
duplex = 'half';
} else {
body = await readRawBody(event, false).catch(() => undefined);
}
}
const method = opts.fetchOptions?.method || event.method;
const oldHeaders = getProxyRequestHeaders(event);
opts.blacklistedHeaders?.forEach((header) => {
const keys = Object.keys(oldHeaders).filter(
(v) => v.toLowerCase() === header.toLowerCase(),
);
keys.forEach((k) => delete oldHeaders[k]);
});
const fetchHeaders = mergeHeaders(
oldHeaders,
opts.fetchOptions?.headers,
opts.headers,
);
const headerObj = Object.fromEntries([...(fetchHeaders.entries as any)()]);
if (process.env.REQ_DEBUG === 'true') {
console.log({
type: 'request',
method,
url: target,
headers: headerObj,
});
}
return sendProxy(event, target, {
...opts,
fetchOptions: {
method,
body,
duplex,
...opts.fetchOptions,
headers: fetchHeaders,
},
});
}

10
src/utils/sending.ts Normal file
View File

@@ -0,0 +1,10 @@
import { H3Event, EventHandlerRequest } from 'h3';
export async function sendJson(ops: {
event: H3Event<EventHandlerRequest>;
data: Record<string, any>;
status?: number;
}) {
setResponseStatus(ops.event, ops.status ?? 200);
await send(ops.event, JSON.stringify(ops.data, null, 2), 'application/json');
}

90
src/utils/turnstile.ts Normal file
View File

@@ -0,0 +1,90 @@
import { H3Event, EventHandlerRequest } from 'h3';
import { SignJWT, jwtVerify } from 'jose';
import { getIp } from '@/utils/ip';
const turnstileSecret = process.env.TURNSTILE_SECRET ?? null;
const jwtSecret = process.env.JWT_SECRET ?? null;
const tokenHeader = 'X-Token';
const jwtPrefix = 'jwt|';
const turnstilePrefix = 'turnstile|';
export function isTurnstileEnabled() {
return !!turnstileSecret && !!jwtSecret;
}
export async function makeToken(ip: string) {
if (!jwtSecret) throw new Error('Cannot make token without a secret');
return await new SignJWT({ ip })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('10m')
.sign(new TextEncoder().encode(jwtSecret));
}
export function setTokenHeader(
event: H3Event<EventHandlerRequest>,
token: string,
) {
setHeader(event, tokenHeader, token);
}
export async function createTokenIfNeeded(
event: H3Event<EventHandlerRequest>,
): Promise<null | string> {
if (!isTurnstileEnabled()) return null;
if (!jwtSecret) return null;
const token = event.headers.get(tokenHeader);
if (!token) return null;
if (!token.startsWith(turnstilePrefix)) return null;
return await makeToken(getIp(event));
}
export async function isAllowedToMakeRequest(
event: H3Event<EventHandlerRequest>,
) {
if (!isTurnstileEnabled()) return true;
const token = event.headers.get(tokenHeader);
if (!token) return false;
if (!jwtSecret || !turnstileSecret) return false;
if (token.startsWith(jwtPrefix)) {
const jwtToken = token.slice(jwtPrefix.length);
let jwtPayload: { ip: string } | null = null;
try {
const jwtResult = await jwtVerify<{ ip: string }>(
jwtToken,
new TextEncoder().encode(jwtSecret),
{
algorithms: ['HS256'],
},
);
jwtPayload = jwtResult.payload;
} catch {}
if (!jwtPayload) return false;
if (getIp(event) !== jwtPayload.ip) return false;
return true;
}
if (token.startsWith(turnstilePrefix)) {
const turnstileToken = token.slice(turnstilePrefix.length);
const formData = new FormData();
formData.append('secret', turnstileSecret);
formData.append('response', turnstileToken);
formData.append('remoteip', getIp(event));
const result = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{
body: formData,
method: 'POST',
},
);
const outcome: { success: boolean } = await result.json();
return outcome.success;
}
return false;
}

23
tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"baseUrl": "./src",
"paths": {
"@/*": ["./*"]
}
},
"extends": "./.nitro/types/tsconfig.json"
}

View File

@@ -1,16 +0,0 @@
const path = require('path');
const { defineConfig } = require('vite');
const { default: eslint } = require('vite-plugin-eslint');
module.exports = defineConfig({
plugins: [eslint()],
build: {
minify: false,
lib: {
entry: path.resolve(__dirname, 'src/main.js'),
name: 'worker',
formats: ['es'],
fileName: () => `worker.js`,
},
},
});

4
wrangler.toml Normal file
View File

@@ -0,0 +1,4 @@
name = "simple-proxy"
main = "./.output/server/index.mjs"
workers_dev = true
compatibility_date = "2022-09-10"