From 655b053fd6e2759db464e1d35ccb225de78a2c2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 20:20:42 +0000 Subject: [PATCH 1/5] 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] --- pnpm-lock.yaml | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e2a20a..1b2c18d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -283,6 +283,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@fastify/busboy@2.0.0: + resolution: {integrity: sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==} + engines: {node: '>=14'} + dev: false + /@humanwhocodes/config-array@0.11.11: resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} engines: {node: '>=10.10.0'} @@ -488,6 +493,7 @@ packages: dependencies: is-glob: 4.0.3 micromatch: 4.0.5 + napi-wasm: 1.1.0 dev: false bundledDependencies: - napi-wasm @@ -1127,13 +1133,6 @@ packages: run-applescript: 5.0.0 dev: true - /busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - dependencies: - streamsearch: 1.1.0 - dev: false - /c12@1.4.2: resolution: {integrity: sha512-3IP/MuamSVRVw8W8+CHWAz9gKN4gd+voF2zm/Ln6D25C2RhytEZ1ABbC8MjKr4BR9rhoV1JQ7jJA158LDiTkLg==} dependencies: @@ -2781,6 +2780,10 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /napi-wasm@1.1.0: + resolution: {integrity: sha512-lHwIAJbmLSjF9VDRm9GoVOy9AGp3aIvkjv+Kvz9h16QR3uSVYH78PNQUnT2U4X53mhlnV2M7wrhibQ3GHicDmg==} + dev: false + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -3063,7 +3066,7 @@ packages: fast-glob: 3.3.1 js-yaml: 4.1.0 supports-color: 9.4.0 - undici: 5.24.0 + undici: 5.27.0 yargs-parser: 21.1.1 dev: false @@ -3513,11 +3516,6 @@ packages: resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==} dev: false - /streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - dev: false - /streamx@2.15.1: resolution: {integrity: sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==} dependencies: @@ -3795,11 +3793,11 @@ packages: unplugin: 1.4.0 dev: false - /undici@5.24.0: - resolution: {integrity: sha512-OKlckxBjFl0oXxcj9FU6oB8fDAaiRUq+D8jrFWGmOfI/gIyjk/IeS75LMzgYKUaeHzLUcYvf9bbJGSrUwTfwwQ==} + /undici@5.27.0: + resolution: {integrity: sha512-l3ydWhlhOJzMVOYkymLykcRRXqbUaQriERtR70B9LzNkZ4bX52Fc8wbTDneMiwo8T+AemZXvXaTx+9o5ROxrXg==} engines: {node: '>=14.0'} dependencies: - busboy: 1.6.0 + '@fastify/busboy': 2.0.0 dev: false /unenv@1.7.4: From ed4d8826ce5fcafe567de22067a3deae74f334ee Mon Sep 17 00:00:00 2001 From: mrjvs Date: Wed, 20 Dec 2023 14:32:15 +0100 Subject: [PATCH 2/5] Add turnstile integration --- README.md | 4 +- package.json | 1 + pnpm-lock.yaml | 7 ++++ src/routes/index.ts | 20 +++++++++- src/utils/ip.ts | 5 +++ src/utils/turnstile.ts | 87 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 src/utils/ip.ts create mode 100644 src/utils/turnstile.ts diff --git a/README.md b/README.md index 060c2b2..1c69650 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # simple-proxy Simple reverse proxy to bypass CORS, used by [movie-web](https://movie-web.app). - -[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/movie-web/simple-proxy) +Read the docs at https://docs.movie-web.app/proxy --- @@ -10,6 +9,7 @@ Simple reverse proxy to bypass CORS, used by [movie-web](https://movie-web.app). - 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 ### supported platforms: - cloudflare workers diff --git a/package.json b/package.json index 6bed52a..eb26a3d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "preinstall": "npx only-allow pnpm" }, "dependencies": { + "@tsndr/cloudflare-worker-jwt": "^2.3.2", "h3": "^1.8.1", "nitropack": "latest" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b2c18d..2c6251a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@tsndr/cloudflare-worker-jwt': + specifier: ^2.3.2 + version: 2.3.2 h3: specifier: ^1.8.1 version: 1.8.1 @@ -701,6 +704,10 @@ packages: rollup: 3.29.1 dev: false + /@tsndr/cloudflare-worker-jwt@2.3.2: + resolution: {integrity: sha512-g1jSm5olPqKh15kadnj0666YPudibHYGyFyM0URLXSeY5MzNIGkfhFedLgKHq8NCDBMzLUMX7Oz8d+jmQXqBuw==} + dev: false + /@types/estree@1.0.1: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} dev: false diff --git a/src/routes/index.ts b/src/routes/index.ts index 7fd6d54..07adbac 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -4,6 +4,11 @@ import { getAfterResponseHeaders, cleanupHeadersBeforeProxy, } from '@/utils/headers'; +import { + createTokenIfNeeded, + isAllowedToMakeRequest, + setTokenHeader, +} from '@/utils/turnstile'; export default defineEventHandler(async (event) => { // handle cors, if applicable @@ -14,14 +19,24 @@ export default defineEventHandler(async (event) => { if (!destination) return await sendJson({ event, - status: 400, + status: 200, data: { - error: 'destination query parameter invalid', + error: 'Proxy is working as expected', + }, + }); + + 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 cleanupHeadersBeforeProxy(event); @@ -34,6 +49,7 @@ export default defineEventHandler(async (event) => { onResponse(outputEvent, response) { const headers = getAfterResponseHeaders(response.headers, response.url); setResponseHeaders(outputEvent, headers); + if (token) setTokenHeader(event, token); }, }); }); diff --git a/src/utils/ip.ts b/src/utils/ip.ts new file mode 100644 index 0000000..017b868 --- /dev/null +++ b/src/utils/ip.ts @@ -0,0 +1,5 @@ +import { EventHandlerRequest, H3Event } from 'h3'; + +export function getIp(_event: H3Event) { + return 'not-a-real-ip'; // TODO cross platform IP +} diff --git a/src/utils/turnstile.ts b/src/utils/turnstile.ts new file mode 100644 index 0000000..2e4f22e --- /dev/null +++ b/src/utils/turnstile.ts @@ -0,0 +1,87 @@ +import { H3Event, EventHandlerRequest } from 'h3'; +import jsonwebtoken from '@tsndr/cloudflare-worker-jwt'; +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 jsonwebtoken.sign( + { + ip, + exp: Math.floor(Date.now() / 1000) + 60 * 10, // 10 Minutes + }, + jwtSecret, + ); +} + +export function setTokenHeader( + event: H3Event, + token: string, +) { + setHeader(event, tokenHeader, token); +} + +export async function createTokenIfNeeded( + event: H3Event, +): Promise { + 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, +) { + 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); + const isValid = await jsonwebtoken.verify(jwtToken, jwtSecret, { + algorithm: 'HS256', + }); + if (!isValid) return false; + const jwtBody = jsonwebtoken.decode<{ ip: string }>(jwtToken); + if (!jwtBody.payload) return false; + if (getIp(event) !== jwtBody.payload.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; +} From 9ef1467ee1948e8dc181ce3146cf5d45a1a0bf07 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Wed, 20 Dec 2023 14:37:34 +0100 Subject: [PATCH 3/5] Finish ip fetching --- README.md | 3 +++ src/utils/ip.ts | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1c69650..5df460f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ Read the docs at https://docs.movie-web.app/proxy - 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 diff --git a/src/utils/ip.ts b/src/utils/ip.ts index 017b868..65d48b4 100644 --- a/src/utils/ip.ts +++ b/src/utils/ip.ts @@ -1,5 +1,10 @@ import { EventHandlerRequest, H3Event } from 'h3'; -export function getIp(_event: H3Event) { - return 'not-a-real-ip'; // TODO cross platform IP +export function getIp(event: H3Event) { + const value = getHeader(event, 'CF-Connecting-IP'); + if (!value) + throw new Error( + 'Ip header not found, turnstile only works on cloudflare workers', + ); + return value; } From 0a553a8b844a5d6fd92d9df569e7a4a8078cb2ab Mon Sep 17 00:00:00 2001 From: mrjvs Date: Wed, 20 Dec 2023 14:44:21 +0100 Subject: [PATCH 4/5] Change error to a message --- src/routes/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/index.ts b/src/routes/index.ts index 07adbac..f3e70d4 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -21,7 +21,7 @@ export default defineEventHandler(async (event) => { event, status: 200, data: { - error: 'Proxy is working as expected', + message: 'Proxy is working as expected', }, }); From 9e5d1a29936890b0f4fe9d68a601618cee2496c6 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Wed, 20 Dec 2023 15:32:00 +0100 Subject: [PATCH 5/5] Bump version number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eb26a3d..c9eac5f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "simple-proxy", "private": true, - "version": "2.0.1", + "version": "2.1.0", "scripts": { "prepare": "nitropack prepare", "dev": "nitropack dev",