Skip to content

Commit

Permalink
Merge pull request #20 from movie-web/dev
Browse files Browse the repository at this point in the history
Simple proxy v2.1.0
binaryoverload authored Dec 20, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 3e63fe5 + 8c89f79 commit 88b1852
Showing 6 changed files with 143 additions and 21 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# 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

---

### 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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "simple-proxy",
"private": true,
"version": "2.0.1",
"version": "2.1.0",
"scripts": {
"prepare": "nitropack prepare",
"dev": "nitropack dev",
@@ -15,6 +15,7 @@
"preinstall": "npx only-allow pnpm"
},
"dependencies": {
"@tsndr/cloudflare-worker-jwt": "^2.3.2",
"h3": "^1.8.1",
"nitropack": "latest"
},
37 changes: 21 additions & 16 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 18 additions & 2 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -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: {
message: 'Proxy is working as expected',
},
});

if (!(await isAllowedToMakeRequest(event)))
return await sendJson({
event,
status: 401,
data: {
error: 'destination query parameter invalid',
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);
},
});
});
10 changes: 10 additions & 0 deletions src/utils/ip.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 87 additions & 0 deletions src/utils/turnstile.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 88b1852

Please sign in to comment.