mirror of
https://github.com/movie-web/simple-proxy.git
synced 2025-09-13 07:43:25 +00:00
Add turnstile integration
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
# simple-proxy
|
||||
|
||||
Simple reverse proxy to bypass CORS, used by [movie-web](https://movie-web.app).
|
||||
|
||||
[](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
|
||||
|
@@ -15,6 +15,7 @@
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tsndr/cloudflare-worker-jwt": "^2.3.2",
|
||||
"h3": "^1.8.1",
|
||||
"nitropack": "latest"
|
||||
},
|
||||
|
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -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
|
||||
|
@@ -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);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
5
src/utils/ip.ts
Normal file
5
src/utils/ip.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { EventHandlerRequest, H3Event } from 'h3';
|
||||
|
||||
export function getIp(_event: H3Event<EventHandlerRequest>) {
|
||||
return 'not-a-real-ip'; // TODO cross platform IP
|
||||
}
|
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