mirror of
https://github.com/movie-web/simple-proxy.git
synced 2025-09-13 09:43:26 +00:00
@@ -1,8 +1,7 @@
|
|||||||
# simple-proxy
|
# simple-proxy
|
||||||
|
|
||||||
Simple reverse proxy to bypass CORS, used by [movie-web](https://movie-web.app).
|
Simple reverse proxy to bypass CORS, used by [movie-web](https://movie-web.app).
|
||||||
|
Read the docs at https://docs.movie-web.app/proxy
|
||||||
[](https://deploy.workers.cloudflare.com/?url=https://github.com/movie-web/simple-proxy)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -10,6 +9,10 @@ Simple reverse proxy to bypass CORS, used by [movie-web](https://movie-web.app).
|
|||||||
- Deployable on many platforms - thanks to nitro
|
- Deployable on many platforms - thanks to nitro
|
||||||
- header rewrites - read and write protected headers
|
- header rewrites - read and write protected headers
|
||||||
- bypass CORS - always allows browser to send requests through it
|
- 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:
|
### supported platforms:
|
||||||
- cloudflare workers
|
- cloudflare workers
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "simple-proxy",
|
"name": "simple-proxy",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "nitropack prepare",
|
"prepare": "nitropack prepare",
|
||||||
"dev": "nitropack dev",
|
"dev": "nitropack dev",
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tsndr/cloudflare-worker-jwt": "^2.3.2",
|
||||||
"h3": "^1.8.1",
|
"h3": "^1.8.1",
|
||||||
"nitropack": "latest"
|
"nitropack": "latest"
|
||||||
},
|
},
|
||||||
|
37
pnpm-lock.yaml
generated
37
pnpm-lock.yaml
generated
@@ -5,6 +5,9 @@ settings:
|
|||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@tsndr/cloudflare-worker-jwt':
|
||||||
|
specifier: ^2.3.2
|
||||||
|
version: 2.3.2
|
||||||
h3:
|
h3:
|
||||||
specifier: ^1.8.1
|
specifier: ^1.8.1
|
||||||
version: 1.8.1
|
version: 1.8.1
|
||||||
@@ -283,6 +286,11 @@ packages:
|
|||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@fastify/busboy@2.0.0:
|
||||||
|
resolution: {integrity: sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@humanwhocodes/config-array@0.11.11:
|
/@humanwhocodes/config-array@0.11.11:
|
||||||
resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==}
|
resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==}
|
||||||
engines: {node: '>=10.10.0'}
|
engines: {node: '>=10.10.0'}
|
||||||
@@ -488,6 +496,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
micromatch: 4.0.5
|
micromatch: 4.0.5
|
||||||
|
napi-wasm: 1.1.0
|
||||||
dev: false
|
dev: false
|
||||||
bundledDependencies:
|
bundledDependencies:
|
||||||
- napi-wasm
|
- napi-wasm
|
||||||
@@ -695,6 +704,10 @@ packages:
|
|||||||
rollup: 3.29.1
|
rollup: 3.29.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@tsndr/cloudflare-worker-jwt@2.3.2:
|
||||||
|
resolution: {integrity: sha512-g1jSm5olPqKh15kadnj0666YPudibHYGyFyM0URLXSeY5MzNIGkfhFedLgKHq8NCDBMzLUMX7Oz8d+jmQXqBuw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/estree@1.0.1:
|
/@types/estree@1.0.1:
|
||||||
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
|
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -1127,13 +1140,6 @@ packages:
|
|||||||
run-applescript: 5.0.0
|
run-applescript: 5.0.0
|
||||||
dev: true
|
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:
|
/c12@1.4.2:
|
||||||
resolution: {integrity: sha512-3IP/MuamSVRVw8W8+CHWAz9gKN4gd+voF2zm/Ln6D25C2RhytEZ1ABbC8MjKr4BR9rhoV1JQ7jJA158LDiTkLg==}
|
resolution: {integrity: sha512-3IP/MuamSVRVw8W8+CHWAz9gKN4gd+voF2zm/Ln6D25C2RhytEZ1ABbC8MjKr4BR9rhoV1JQ7jJA158LDiTkLg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2781,6 +2787,10 @@ packages:
|
|||||||
/ms@2.1.3:
|
/ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
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:
|
/natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -3063,7 +3073,7 @@ packages:
|
|||||||
fast-glob: 3.3.1
|
fast-glob: 3.3.1
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
supports-color: 9.4.0
|
supports-color: 9.4.0
|
||||||
undici: 5.24.0
|
undici: 5.27.0
|
||||||
yargs-parser: 21.1.1
|
yargs-parser: 21.1.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -3513,11 +3523,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==}
|
resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/streamsearch@1.1.0:
|
|
||||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
|
||||||
engines: {node: '>=10.0.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/streamx@2.15.1:
|
/streamx@2.15.1:
|
||||||
resolution: {integrity: sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==}
|
resolution: {integrity: sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3795,11 +3800,11 @@ packages:
|
|||||||
unplugin: 1.4.0
|
unplugin: 1.4.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/undici@5.24.0:
|
/undici@5.27.0:
|
||||||
resolution: {integrity: sha512-OKlckxBjFl0oXxcj9FU6oB8fDAaiRUq+D8jrFWGmOfI/gIyjk/IeS75LMzgYKUaeHzLUcYvf9bbJGSrUwTfwwQ==}
|
resolution: {integrity: sha512-l3ydWhlhOJzMVOYkymLykcRRXqbUaQriERtR70B9LzNkZ4bX52Fc8wbTDneMiwo8T+AemZXvXaTx+9o5ROxrXg==}
|
||||||
engines: {node: '>=14.0'}
|
engines: {node: '>=14.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
busboy: 1.6.0
|
'@fastify/busboy': 2.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/unenv@1.7.4:
|
/unenv@1.7.4:
|
||||||
|
@@ -4,6 +4,11 @@ import {
|
|||||||
getAfterResponseHeaders,
|
getAfterResponseHeaders,
|
||||||
cleanupHeadersBeforeProxy,
|
cleanupHeadersBeforeProxy,
|
||||||
} from '@/utils/headers';
|
} from '@/utils/headers';
|
||||||
|
import {
|
||||||
|
createTokenIfNeeded,
|
||||||
|
isAllowedToMakeRequest,
|
||||||
|
setTokenHeader,
|
||||||
|
} from '@/utils/turnstile';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
// handle cors, if applicable
|
// handle cors, if applicable
|
||||||
@@ -14,14 +19,24 @@ export default defineEventHandler(async (event) => {
|
|||||||
if (!destination)
|
if (!destination)
|
||||||
return await sendJson({
|
return await sendJson({
|
||||||
event,
|
event,
|
||||||
status: 400,
|
status: 200,
|
||||||
data: {
|
data: {
|
||||||
error: 'destination query parameter invalid',
|
message: 'Proxy is working as expected',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!(await isAllowedToMakeRequest(event)))
|
||||||
|
return await sendJson({
|
||||||
|
event,
|
||||||
|
status: 401,
|
||||||
|
data: {
|
||||||
|
error: 'Invalid or missing token',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// read body
|
// read body
|
||||||
const body = await getBodyBuffer(event);
|
const body = await getBodyBuffer(event);
|
||||||
|
const token = await createTokenIfNeeded(event);
|
||||||
|
|
||||||
// proxy
|
// proxy
|
||||||
cleanupHeadersBeforeProxy(event);
|
cleanupHeadersBeforeProxy(event);
|
||||||
@@ -34,6 +49,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
onResponse(outputEvent, response) {
|
onResponse(outputEvent, response) {
|
||||||
const headers = getAfterResponseHeaders(response.headers, response.url);
|
const headers = getAfterResponseHeaders(response.headers, response.url);
|
||||||
setResponseHeaders(outputEvent, headers);
|
setResponseHeaders(outputEvent, headers);
|
||||||
|
if (token) setTokenHeader(event, token);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
10
src/utils/ip.ts
Normal file
10
src/utils/ip.ts
Normal 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;
|
||||||
|
}
|
87
src/utils/turnstile.ts
Normal file
87
src/utils/turnstile.ts
Normal file
@@ -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<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);
|
||||||
|
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;
|
||||||
|
}
|
Reference in New Issue
Block a user