Implemented backend proxy

This commit is contained in:
WardPearce
2026-02-13 18:31:59 +13:00
parent cbbd253d36
commit 8c32343dae
3 changed files with 123 additions and 11 deletions
@@ -1,8 +1,12 @@
import { timeout } from '$lib/misc';
import { isUnrestrictedPlatform, timeout } from '$lib/misc';
import { Capacitor } from '@capacitor/core';
const originalFetch = window.fetch;
const corsProxyUrl: string = 'http://localhost:3000/';
const corsProxyUrl =
Capacitor.getPlatform() === 'android'
? 'http://localhost:3000/'
: `${location.origin}/api/proxy/`;
let mobileNodeLoaded = false;
function needsProxying(target: string): boolean {
@@ -10,11 +14,11 @@ function needsProxying(target: string): boolean {
return true;
}
export const androidFetch = async (
export const fetchProxied = async (
requestInput: string | URL | Request,
requestOptions?: RequestInit
): Promise<Response> => {
if (!mobileNodeLoaded) {
if (!mobileNodeLoaded && Capacitor.getPlatform() === 'android') {
let mobileNodeResp: Response | undefined;
while (!mobileNodeResp || !mobileNodeResp.ok) {
@@ -29,11 +33,17 @@ export const androidFetch = async (
mobileNodeLoaded = true;
}
const uri = requestInput instanceof Request ? requestInput.url : requestInput.toString();
const nonProxiedUrl =
requestInput instanceof Request ? requestInput.url : requestInput.toString();
if (needsProxying(nonProxiedUrl)) {
const proxiedURLEncoded =
Capacitor.getPlatform() === 'android' ? nonProxiedUrl : encodeURIComponent(nonProxiedUrl);
const proxiedUrl = corsProxyUrl + proxiedURLEncoded;
if (needsProxying(uri)) {
if (requestInput instanceof Request) {
requestInput = new Request(corsProxyUrl + uri, {
requestInput = new Request(proxiedUrl, {
method: requestInput.method,
headers: requestInput.headers,
body: requestInput.body,
@@ -47,7 +57,7 @@ export const androidFetch = async (
...(requestInput.body ? { duplex: 'half' } : {})
});
} else {
requestInput = corsProxyUrl + uri;
requestInput = proxiedUrl;
}
}
@@ -55,8 +65,8 @@ export const androidFetch = async (
return originalFetch(requestInput, requestOptions);
};
if (Capacitor.getPlatform() === 'android') {
window.fetch = androidFetch;
if (isUnrestrictedPlatform() && Capacitor.getPlatform() !== 'electron') {
window.fetch = fetchProxied;
const originalXhrOpen = XMLHttpRequest.prototype.open;
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { isAndroidTvStore } from '$lib/store';
import { App } from '@capacitor/app';
import '$lib/android/http/androidRequests';
import '$lib/fetchProxy';
let { children } = $props();
@@ -0,0 +1,102 @@
const ALLOWED_HEADERS = [
'Origin',
'X-Requested-With',
'Content-Type',
'Accept',
'Authorization',
'x-goog-visitor-id',
'x-goog-api-key',
'x-origin',
'x-youtube-client-version',
'x-youtube-client-name',
'x-goog-api-format-version',
'x-goog-authuser',
'x-user-agent',
'Accept-Language',
'X-Goog-FieldMask',
'Range',
'Referer',
'Cookie'
].join(', ');
function copyHeader(header: string, targetHeaders: Headers, sourceHeaders: Headers) {
if (sourceHeaders.has(header)) {
targetHeaders.set(header, sourceHeaders.get(header)!);
}
}
async function proxyRequest(request: Request, urlToProxy: string): Promise<Response> {
let urlToProxyObj: URL;
try {
urlToProxyObj = new URL(decodeURIComponent(urlToProxy));
} catch {
return new Response('Invalid URL', { status: 400 });
}
const requestHeaders = request.headers;
requestHeaders.set('host', urlToProxyObj.host);
requestHeaders.set('origin', urlToProxyObj.origin);
for (const key of [
'referer',
'x-forwarded-for',
'x-requested-with',
'sec-ch-ua-mobile',
'sec-ch-ua',
'sec-ch-ua-platform'
]) {
requestHeaders.delete(key);
}
const fetchRes = await fetch(urlToProxy.toString(), {
method: request.method,
headers: requestHeaders,
body: request.body,
credentials: 'same-origin',
...(request.body ? { duplex: 'half' } : {})
});
const responseHeaders = new Headers();
copyHeader('content-length', requestHeaders, fetchRes.headers);
copyHeader('content-type', requestHeaders, fetchRes.headers);
copyHeader('content-disposition', requestHeaders, fetchRes.headers);
copyHeader('accept-ranges', requestHeaders, fetchRes.headers);
copyHeader('content-range', requestHeaders, fetchRes.headers);
return new Response(fetchRes.body, {
status: fetchRes.status,
headers: responseHeaders
});
}
export async function GET({ request, params }) {
return await proxyRequest(request, params.urlToProxy);
}
export async function PATCH({ request, params }) {
return await proxyRequest(request, params.urlToProxy);
}
export async function DELETE({ request, params }) {
return await proxyRequest(request, params.urlToProxy);
}
export async function PUT({ request, params }) {
return await proxyRequest(request, params.urlToProxy);
}
export async function POST({ request, params }) {
return await proxyRequest(request, params.urlToProxy);
}
export async function OPTIONS({ request }) {
return new Response('', {
status: 200,
headers: {
'Access-Control-Allow-Origin': request.headers.get('origin') || '',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': ALLOWED_HEADERS,
'Access-Control-Max-Age': '86400',
'Access-Control-Allow-Credentials': 'true'
}
});
}