Fixed sabr on electron
This commit is contained in:
Generated
+4
-30
@@ -23,7 +23,7 @@
|
||||
"bgutils-js": "^3.2.0",
|
||||
"capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz",
|
||||
"fuse.js": "^7.0.0",
|
||||
"googlevideo": "^2.0.0",
|
||||
"googlevideo": "^3.0.0",
|
||||
"he": "^1.2.0",
|
||||
"human-number": "^2.0.4",
|
||||
"iso-3166": "^4.3.0",
|
||||
@@ -54,7 +54,6 @@
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vite-pwa/sveltekit": "^0.6.6",
|
||||
"buffer": "^6.0.3",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.45.1",
|
||||
@@ -4620,31 +4619,6 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
@@ -7370,9 +7344,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/googlevideo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/googlevideo/-/googlevideo-2.0.0.tgz",
|
||||
"integrity": "sha512-OVlNWZ07TPIelaEII6mH9od+Cxljl7P4AzhEYVNN5d4FhFT9L5otpcLtgvraTE9u69KfVVw+L4pVeczArcD33w==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/googlevideo/-/googlevideo-3.0.0.tgz",
|
||||
"integrity": "sha512-8LGFjbZFG4RCsyhMbjX2+IlU5Suj94FeQr1JandPSsKfAMrqL08pOoQsBYl/5P+bqIFm0wQEVpKIhm5wpiekwg==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vite-pwa/sveltekit": "^0.6.6",
|
||||
"buffer": "^6.0.3",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.45.1",
|
||||
@@ -55,7 +54,7 @@
|
||||
"bgutils-js": "^3.2.0",
|
||||
"capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz",
|
||||
"fuse.js": "^7.0.0",
|
||||
"googlevideo": "^2.0.0",
|
||||
"googlevideo": "^3.0.0",
|
||||
"he": "^1.2.0",
|
||||
"human-number": "^2.0.4",
|
||||
"iso-3166": "^4.3.0",
|
||||
@@ -72,4 +71,4 @@
|
||||
"terser": "^5.34.1",
|
||||
"youtubei.js": "^13.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,56 @@
|
||||
import { goto } from "$app/navigation";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { goto } from '$app/navigation';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
const corsProxyUrl: string = 'http://localhost:3000/';
|
||||
|
||||
function needsProxying(target: string): boolean {
|
||||
if (!target.startsWith('http')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export const androidFetch = async (
|
||||
requestInput: string | URL | Request,
|
||||
requestOptions?: RequestInit
|
||||
): Promise<Response> => {
|
||||
const uri = requestInput instanceof Request ? requestInput.url : requestInput.toString();
|
||||
|
||||
if (needsProxying(uri)) {
|
||||
if (requestInput instanceof Request) {
|
||||
requestInput = new Request(corsProxyUrl + uri, {
|
||||
method: requestInput.method,
|
||||
headers: requestInput.headers,
|
||||
body: requestInput.body,
|
||||
mode: requestInput.mode,
|
||||
credentials: requestInput.credentials,
|
||||
cache: requestInput.cache,
|
||||
redirect: requestInput.redirect,
|
||||
referrer: requestInput.referrer,
|
||||
integrity: requestInput.integrity,
|
||||
keepalive: requestInput.keepalive,
|
||||
...(requestInput.body ? { duplex: 'half' } : {})
|
||||
});
|
||||
} else {
|
||||
requestInput = corsProxyUrl + uri;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the original fetch with the proxied URL and options
|
||||
return originalFetch(requestInput, requestOptions);
|
||||
};
|
||||
|
||||
if (Capacitor.getPlatform() === 'android') {
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = androidFetch;
|
||||
|
||||
function needsProxying(target: string): boolean {
|
||||
if (!target.startsWith('http')) return false;
|
||||
return true;
|
||||
}
|
||||
const originalXhrOpen = XMLHttpRequest.prototype.open;
|
||||
|
||||
const corsProxyUrl: string = 'http://localhost:3000/';
|
||||
XMLHttpRequest.prototype.open = function (...args: any[]): void {
|
||||
if (needsProxying(args[1])) {
|
||||
args[1] = corsProxyUrl + args[1];
|
||||
}
|
||||
/* @ts-ignore */
|
||||
return originalXhrOpen.apply(this, args);
|
||||
};
|
||||
|
||||
window.fetch = async (requestInput: string | URL | Request, requestOptions?: RequestInit): Promise<Response> => {
|
||||
const uri = requestInput instanceof Request ? requestInput.url : requestInput.toString();
|
||||
|
||||
if (needsProxying(uri)) {
|
||||
if (requestInput instanceof Request) {
|
||||
requestInput = new Request(corsProxyUrl + uri, {
|
||||
method: requestInput.method,
|
||||
headers: requestInput.headers,
|
||||
body: requestInput.body,
|
||||
mode: requestInput.mode,
|
||||
credentials: requestInput.credentials,
|
||||
cache: requestInput.cache,
|
||||
redirect: requestInput.redirect,
|
||||
referrer: requestInput.referrer,
|
||||
integrity: requestInput.integrity,
|
||||
keepalive: requestInput.keepalive,
|
||||
...(requestInput.body ? { duplex: "half" } : {})
|
||||
});
|
||||
} else {
|
||||
requestInput = corsProxyUrl + uri;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the original fetch with the proxied URL and options
|
||||
return originalFetch(requestInput, requestOptions);
|
||||
};
|
||||
|
||||
const originalXhrOpen = XMLHttpRequest.prototype.open;
|
||||
|
||||
XMLHttpRequest.prototype.open = function (...args: any[]): void {
|
||||
if (needsProxying(args[1])) {
|
||||
args[1] = corsProxyUrl + args[1];
|
||||
}
|
||||
/* @ts-ignore */
|
||||
return originalXhrOpen.apply(this, args);
|
||||
};
|
||||
|
||||
setTimeout(() => goto('/', { replaceState: true }), 100);
|
||||
setTimeout(() => goto('/', { replaceState: true }), 100);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
} from '../store';
|
||||
import { getDynamicTheme, setStatusBarColor } from '../theme';
|
||||
import { HttpFetchPlugin, type SabrStreamingContext } from '$lib/sabr/shakaHttpPlugin';
|
||||
import { Constants, Innertube, type Misc } from 'youtubei.js';
|
||||
import { Constants, type Misc } from 'youtubei.js';
|
||||
import {
|
||||
fromFormat,
|
||||
fromFormatInitializationMetadata,
|
||||
@@ -174,6 +174,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
HttpFetchPlugin.cacheManager.clearCache();
|
||||
|
||||
player = new shaka.Player();
|
||||
playerElement = document.getElementById('player') as HTMLMediaElement;
|
||||
|
||||
@@ -500,9 +502,8 @@
|
||||
}
|
||||
}
|
||||
} else if (type == shaka.net.NetworkingEngine.RequestType.LICENSE) {
|
||||
const innertube = await Innertube.create({ fetch: window.fetch });
|
||||
const wrapped = {} as Record<string, any>;
|
||||
wrapped.context = innertube.session.context;
|
||||
wrapped.context = data.video.ytjs?.innertube.session.context;
|
||||
wrapped.cpn = data.video.ytjs?.clientPlaybackNonce;
|
||||
wrapped.drmParams = decodeURIComponent(drmParams || '');
|
||||
wrapped.drmSystem = 'DRM_SYSTEM_WIDEVINE';
|
||||
|
||||
@@ -13,7 +13,6 @@ import { fromFormat } from '$lib/sabr/formatKeyUtils';
|
||||
import { interfaceRegionStore, poTokenCacheStore } from '$lib/store';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { USER_AGENT } from 'bgutils-js';
|
||||
import { Buffer } from 'buffer';
|
||||
import { get } from 'svelte/store';
|
||||
import { Innertube, UniversalCache, Utils, YT, YTNodes } from 'youtubei.js';
|
||||
|
||||
@@ -92,13 +91,11 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
let dashUri: string | undefined;
|
||||
|
||||
if (video.streaming_data) {
|
||||
video.streaming_data.adaptive_formats = video.streaming_data.adaptive_formats.map((format) => {
|
||||
video.streaming_data.adaptive_formats.forEach((format) => {
|
||||
const formatKey = fromFormat(format) || '';
|
||||
format.url = `https://sabr?___key=${formatKey}`;
|
||||
format.signature_cipher = undefined;
|
||||
format.decipher = () => format.url || '';
|
||||
|
||||
return format;
|
||||
});
|
||||
|
||||
if (video.basic_info.is_live) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GoogleVideo, Protos, concatenateChunks, Part, PART } from 'googlevideo';
|
||||
import { GoogleVideo, Protos, concatenateChunks, PART } from 'googlevideo';
|
||||
import type { Part } from 'googlevideo';
|
||||
import shaka from 'shaka-player/dist/shaka-player.ui';
|
||||
|
||||
import { cacheSegment } from './cacheHelper';
|
||||
|
||||
@@ -5,6 +5,8 @@ import type { Protos } from 'googlevideo';
|
||||
import { retrieveCachedSegment } from './cacheHelper';
|
||||
import { SabrUmpParser } from './sabrUmpParser';
|
||||
import { CacheManager } from './cacheManager';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { androidFetch } from '$lib/android/http/androidRequests';
|
||||
|
||||
export interface SabrStreamingContext {
|
||||
byteRange?: { start: number; end: number };
|
||||
@@ -26,7 +28,7 @@ export interface SabrStreamingContext {
|
||||
}
|
||||
|
||||
export class HttpFetchPlugin {
|
||||
private static fetch_ = window.fetch;
|
||||
private static fetch_ = Capacitor.getPlatform() === 'android' ? androidFetch : window.fetch;
|
||||
private static AbortController_ = window.AbortController;
|
||||
private static Headers_ = window.Headers;
|
||||
public static cacheManager = new CacheManager();
|
||||
|
||||
@@ -5,129 +5,151 @@ const HOST = 'localhost';
|
||||
const PORT = 3000;
|
||||
const MAX_REDIRECTS = 10;
|
||||
|
||||
const CORS_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-user-agent, Accept-Language, Range, Referer'
|
||||
const CORS_ORIGIN = 'https://www.youtube.com'
|
||||
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36(KHTML, like Gecko)'
|
||||
const CORS_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(', ');
|
||||
const CORS_ORIGIN = 'https://www.youtube.com';
|
||||
const USER_AGENT =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36(KHTML, like Gecko)';
|
||||
|
||||
function setCorsHeaders(res) {
|
||||
res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN);
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
|
||||
res.setHeader('Access-Control-Max-Age', '86400')
|
||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN);
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
|
||||
res.setHeader('Access-Control-Max-Age', '86400');
|
||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
}
|
||||
|
||||
function fetchWithRedirects(targetUrl, options, redirectCount = 0) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const httpClient = targetUrl.protocol.startsWith('https') ? https : http;
|
||||
return new Promise((resolve, reject) => {
|
||||
const httpClient = targetUrl.protocol.startsWith('https') ? https : http;
|
||||
|
||||
const req = httpClient.request(targetUrl, options, (res) => {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
if (redirectCount >= MAX_REDIRECTS) {
|
||||
req.end();
|
||||
return reject(new Error('Too many redirects'));
|
||||
}
|
||||
const req = httpClient.request(targetUrl, options, (res) => {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
if (redirectCount >= MAX_REDIRECTS) {
|
||||
req.end();
|
||||
return reject(new Error('Too many redirects'));
|
||||
}
|
||||
|
||||
try {
|
||||
// Attempt to create the redirect URL
|
||||
const redirectUrl = new URL(res.headers.location, targetUrl);
|
||||
return resolve(fetchWithRedirects(redirectUrl, options, redirectCount + 1));
|
||||
} catch (error) {
|
||||
req.end();
|
||||
return reject(new Error(`Invalid URL in redirect: ${error.message}`));
|
||||
}
|
||||
}
|
||||
resolve(res); // Resolve with the final response if not a redirect
|
||||
});
|
||||
try {
|
||||
// Attempt to create the redirect URL
|
||||
const redirectUrl = new URL(res.headers.location, targetUrl);
|
||||
return resolve(fetchWithRedirects(redirectUrl, options, redirectCount + 1));
|
||||
} catch (error) {
|
||||
req.end();
|
||||
return reject(new Error(`Invalid URL in redirect: ${error.message}`));
|
||||
}
|
||||
}
|
||||
resolve(res); // Resolve with the final response if not a redirect
|
||||
});
|
||||
|
||||
if (options.body && (req.method === 'POST' || req.method === 'PUT')) {
|
||||
req.write(options.body);
|
||||
}
|
||||
if (options.body && (req.method === 'POST' || req.method === 'PUT')) {
|
||||
req.write(options.body);
|
||||
}
|
||||
|
||||
req.setTimeout(10000, () => reject(new Error('Request timeout')), req.end());
|
||||
req.on('error', (error) => reject(error), req.end());
|
||||
req.end();
|
||||
});
|
||||
req.setTimeout(10000, () => reject(new Error('Request timeout')), req.end());
|
||||
req.on('error', (error) => reject(error), req.end());
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
setCorsHeaders(res);
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
return res.end();
|
||||
}
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
return res.end();
|
||||
}
|
||||
|
||||
if (!req.url || req.url === '/') {
|
||||
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
||||
return res.end('No URL provided to fetch.');
|
||||
}
|
||||
if (!req.url || req.url === '/') {
|
||||
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
||||
return res.end('No URL provided to fetch.');
|
||||
}
|
||||
|
||||
let targetUrl = req.url.slice(1); // Remove leading '/'
|
||||
let parsedTarget;
|
||||
try {
|
||||
// Ensure protocol (http) is added if missing
|
||||
if (!targetUrl.startsWith('http')) {
|
||||
targetUrl = 'http://' + targetUrl;
|
||||
}
|
||||
parsedTarget = new URL(targetUrl);
|
||||
} catch (error) {
|
||||
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
||||
return res.end(`Invalid URL: ${error.message}`);
|
||||
}
|
||||
let targetUrl = req.url.slice(1); // Remove leading '/'
|
||||
let parsedTarget;
|
||||
try {
|
||||
// Ensure protocol (http) is added if missing
|
||||
if (!targetUrl.startsWith('http')) {
|
||||
targetUrl = 'http://' + targetUrl;
|
||||
}
|
||||
parsedTarget = new URL(targetUrl);
|
||||
} catch (error) {
|
||||
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
||||
return res.end(`Invalid URL: ${error.message}`);
|
||||
}
|
||||
|
||||
let body = '';
|
||||
req.on('data', chunk => {
|
||||
body += chunk;
|
||||
});
|
||||
let body = '';
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
|
||||
req.on('end', async () => {
|
||||
const options = {
|
||||
method: req.method,
|
||||
headers: Object.fromEntries(
|
||||
Object.entries(req.headers).filter(([key]) => ![
|
||||
'host', 'origin', 'referer', 'x-forwarded-for', 'x-requested-with'
|
||||
].includes(key.toLowerCase()))
|
||||
)
|
||||
};
|
||||
req.on('end', async () => {
|
||||
const options = {
|
||||
method: req.method,
|
||||
headers: Object.fromEntries(
|
||||
Object.entries(req.headers).filter(
|
||||
([key]) =>
|
||||
!['host', 'origin', 'referer', 'x-forwarded-for', 'x-requested-with'].includes(
|
||||
key.toLowerCase()
|
||||
)
|
||||
)
|
||||
)
|
||||
};
|
||||
|
||||
options.headers.host = parsedTarget.host;
|
||||
options.headers.origin = parsedTarget.origin;
|
||||
options.headers['user-agent'] = USER_AGENT;
|
||||
options.headers.host = parsedTarget.host;
|
||||
options.headers.origin = parsedTarget.origin;
|
||||
options.headers['user-agent'] = USER_AGENT;
|
||||
|
||||
// For POST and PUT methods, pass the body to the outgoing request
|
||||
if (req.method === 'POST' || req.method === 'PUT') {
|
||||
options.headers['Content-Length'] = Buffer.byteLength(body);
|
||||
options.body = body;
|
||||
}
|
||||
// For POST and PUT methods, pass the body to the outgoing request
|
||||
if (req.method === 'POST' || req.method === 'PUT') {
|
||||
options.headers['Content-Length'] = Buffer.byteLength(body);
|
||||
options.body = body;
|
||||
}
|
||||
|
||||
try {
|
||||
const proxyRes = await fetchWithRedirects(parsedTarget, options);
|
||||
try {
|
||||
const proxyRes = await fetchWithRedirects(parsedTarget, options);
|
||||
|
||||
req.on('close', () => {
|
||||
console.log('Request canceled by the client.');
|
||||
proxyRes.destroy();
|
||||
});
|
||||
req.on('close', () => {
|
||||
console.log('Request canceled by the client.');
|
||||
proxyRes.destroy();
|
||||
});
|
||||
|
||||
res.writeHead(proxyRes.statusCode, {
|
||||
...proxyRes.headers,
|
||||
'Access-Control-Allow-Origin': CORS_ORIGIN,
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': CORS_HEADERS,
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
});
|
||||
res.writeHead(proxyRes.statusCode, {
|
||||
...proxyRes.headers,
|
||||
'Access-Control-Allow-Origin': CORS_ORIGIN,
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': CORS_HEADERS,
|
||||
'Access-Control-Allow-Credentials': 'true'
|
||||
});
|
||||
|
||||
// Pipe response data back to the client
|
||||
proxyRes.pipe(res);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Proxy error:', error);
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
res.end(`Error: ${error.message}`);
|
||||
}
|
||||
});
|
||||
// Pipe response data back to the client
|
||||
proxyRes.pipe(res);
|
||||
} catch (error) {
|
||||
console.error('Proxy error:', error);
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
res.end(`Error: ${error.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Server is running on http://${HOST}:${PORT}`);
|
||||
console.log(`Server is running on http://${HOST}:${PORT}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user