";
11 |
12 | BunnySDK.net.http.serve(async (request) => {
13 | const url = new URL(request.url);
14 | const headers = Object.fromEntries(request.headers.entries());
15 |
16 | if (url.pathname === "/m3u8-proxy") {
17 | return proxyM3U8(url, headers);
18 | } else if (url.pathname === "/ts-proxy") {
19 | return proxyTs(url, headers, request);
20 | }
21 |
22 | if (url.pathname === "/") {
23 | return new Response("Welcome to the Proxy Service", { status: 200 });
24 | }
25 |
26 | return new Response("Not Found", { status: 404 });
27 | });
28 |
29 | async function proxyM3U8(url, headers) {
30 | const targetUrl = url.searchParams.get("url");
31 | const targetHeaders = JSON.parse(url.searchParams.get("headers") || "{}");
32 |
33 | if (!targetUrl) {
34 | return new Response("URL is required", { status: 400 });
35 | }
36 |
37 | try {
38 | // Fetch the M3U8 file
39 | const response = await fetch(targetUrl, {
40 | headers: { Referer: REFERER },
41 | });
42 | if (!response.ok) {
43 | return new Response("Failed to fetch the M3U8 file", {
44 | status: response.status,
45 | });
46 | }
47 |
48 | let m3u8 = await response.text();
49 | m3u8 = m3u8
50 | .split("\n")
51 | .filter((line) => !line.startsWith("#EXT-X-MEDIA:TYPE=AUDIO"))
52 | .join("\n");
53 |
54 | const newLines = m3u8.split("\n").map((line) => {
55 | if (line.startsWith("#")) {
56 | if (line.startsWith("#EXT-X-KEY:")) {
57 | const regex = /https?:\/\/[^\""\s]+/g;
58 | const keyUrl = regex.exec(line)?.[0] ?? "";
59 | const newUrl = `/ts-proxy?url=${encodeURIComponent(
60 | keyUrl
61 | )}&headers=${encodeURIComponent(JSON.stringify(headers))}`;
62 | return line.replace(keyUrl, newUrl);
63 | }
64 | return line;
65 | } else {
66 | let uri;
67 | if (line.endsWith(".m3u8")) {
68 | // Handle M3U8 links
69 | uri = new URL(line, targetUrl);
70 | return `${web_server_url}/m3u8-proxy?url=${encodeURIComponent(
71 | uri.href
72 | )}&headers=${encodeURIComponent(JSON.stringify(headers))}`;
73 | } else if (!line.endsWith(".ts")) {
74 | // Handle TS segments
75 | uri = new URL(line, targetUrl);
76 | return `${web_server_url}/ts-proxy?url=${encodeURIComponent(
77 | uri.href
78 | )}&headers=${encodeURIComponent(JSON.stringify(headers))}`;
79 | } else {
80 | // Handle Other segments
81 | uri = new URL(line, targetUrl);
82 | return `${web_server_url}/ts-proxy?url=${encodeURIComponent(
83 | uri.href
84 | )}&headers=${encodeURIComponent(JSON.stringify(headers))}`;
85 | }
86 | }
87 | });
88 |
89 | return new Response(newLines.join("\n"), {
90 | headers: {
91 | "Content-Type": "application/vnd.apple.mpegurl",
92 | "Access-Control-Allow-Origin": "*",
93 | "Access-Control-Allow-Headers": "*",
94 | "Access-Control-Allow-Methods": "*",
95 | },
96 | });
97 | } catch (error) {
98 | return new Response(error.message, { status: 500 });
99 | }
100 | }
101 |
102 | async function proxyTs(url, headers, request) {
103 | const targetUrl = url.searchParams.get("url");
104 | const targetHeaders = JSON.parse(url.searchParams.get("headers") || "{}");
105 |
106 | if (!targetUrl) {
107 | return new Response("URL is required", { status: 400 });
108 | }
109 |
110 | try {
111 | const response = await fetch(targetUrl, {
112 | method: request.method,
113 | headers: { Referer: REFERER },
114 | });
115 |
116 | if (!response.ok) {
117 | return new Response("Failed to fetch TS segment", {
118 | status: response.status,
119 | });
120 | }
121 |
122 | return new Response(response.body, {
123 | status: response.status,
124 | headers: {
125 | "Content-Type": "video/mp2t",
126 | "Access-Control-Allow-Origin": "*",
127 | "Access-Control-Allow-Headers": "*",
128 | "Access-Control-Allow-Methods": "*",
129 | },
130 | });
131 | } catch (error) {
132 | return new Response(error.message, { status: 500 });
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/m3u8proxy(cf_worker).js:
--------------------------------------------------------------------------------
1 | addEventListener("fetch", (event) => {
2 | event.respondWith(handleRequest(event.request));
3 | });
4 |
5 | async function handleRequest(request) {
6 | const url = new URL(request.url);
7 |
8 | if (url.pathname === "/m3u8-proxy") {
9 | return handleM3U8Proxy(request);
10 | } else if (url.pathname === "/ts-proxy") {
11 | return handleTsProxy(request);
12 | }
13 |
14 | return new Response("Not Found", { status: 404 });
15 | }
16 |
17 | const options = {
18 | originBlacklist: [],
19 | originWhitelist: ["*"],
20 | };
21 |
22 | const isOriginAllowed = (origin, options) => {
23 | if (options.originWhitelist.includes("*")) {
24 | return true;
25 | }
26 | if (
27 | options.originWhitelist.length &&
28 | !options.originWhitelist.includes(origin)
29 | ) {
30 | return false;
31 | }
32 | if (
33 | options.originBlacklist.length &&
34 | options.originBlacklist.includes(origin)
35 | ) {
36 | return false;
37 | }
38 | return true;
39 | };
40 |
41 | async function handleM3U8Proxy(request) {
42 | const { searchParams } = new URL(request.url);
43 | const targetUrl = searchParams.get("url");
44 | const headers = JSON.parse(searchParams.get("headers") || "{}");
45 | const origin = request.headers.get("Origin") || "";
46 |
47 | if (!isOriginAllowed(origin, options)) {
48 | return new Response(`The origin "${origin}" is not allowed.`, {
49 | status: 403,
50 | });
51 | }
52 | if (!targetUrl) {
53 | return new Response("URL is required", { status: 400 });
54 | }
55 |
56 | try {
57 | const response = await fetch(targetUrl, { headers });
58 | if (!response.ok) {
59 | return new Response("Failed to fetch the m3u8 file", {
60 | status: response.status,
61 | });
62 | }
63 |
64 | let m3u8 = await response.text();
65 | m3u8 = m3u8
66 | .split("\n")
67 | .filter((line) => !line.startsWith("#EXT-X-MEDIA:TYPE=AUDIO"))
68 | .join("\n");
69 |
70 | const lines = m3u8.split("\n");
71 | const newLines = [];
72 |
73 | lines.forEach((line) => {
74 | if (line.startsWith("#")) {
75 | if (line.startsWith("#EXT-X-KEY:")) {
76 | const regex = /https?:\/\/[^\""\s]+/g;
77 | const keyUrl = regex.exec(line)?.[0] ?? "";
78 | const newUrl = `/ts-proxy?url=${encodeURIComponent(
79 | keyUrl
80 | )}&headers=${encodeURIComponent(JSON.stringify(headers))}`;
81 | newLines.push(line.replace(keyUrl, newUrl));
82 | } else {
83 | newLines.push(line);
84 | }
85 | } else {
86 | const uri = new URL(line, targetUrl);
87 | newLines.push(
88 | `/ts-proxy?url=${encodeURIComponent(
89 | uri.href
90 | )}&headers=${encodeURIComponent(JSON.stringify(headers))}`
91 | );
92 | }
93 | });
94 |
95 | return new Response(newLines.join("\n"), {
96 | headers: {
97 | "Content-Type": "application/vnd.apple.mpegurl",
98 | "Access-Control-Allow-Origin": "*",
99 | "Access-Control-Allow-Headers": "*",
100 | "Access-Control-Allow-Methods": "*",
101 | },
102 | });
103 | } catch (error) {
104 | return new Response(error.message, { status: 500 });
105 | }
106 | }
107 |
108 | async function handleTsProxy(request) {
109 | const { searchParams } = new URL(request.url);
110 | const targetUrl = searchParams.get("url");
111 | const headers = JSON.parse(searchParams.get("headers") || "{}");
112 | const origin = request.headers.get("Origin") || "";
113 |
114 | if (!isOriginAllowed(origin, options)) {
115 | return new Response(`The origin "${origin}" is not allowed.`, {
116 | status: 403,
117 | });
118 | }
119 | if (!targetUrl) {
120 | return new Response("URL is required", { status: 400 });
121 | }
122 |
123 | try {
124 | const response = await fetch(targetUrl, {
125 | method: request.method,
126 | headers: {
127 | "User-Agent":
128 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36",
129 | ...headers,
130 | },
131 | });
132 |
133 | if (!response.ok) {
134 | return new Response("Failed to fetch segment", {
135 | status: response.status,
136 | });
137 | }
138 |
139 | return new Response(response.body, {
140 | status: response.status,
141 | headers: {
142 | "Content-Type": "video/mp2t",
143 | "Access-Control-Allow-Origin": "*",
144 | "Access-Control-Allow-Headers": "*",
145 | "Access-Control-Allow-Methods": "*",
146 | },
147 | });
148 | } catch (error) {
149 | return new Response(error.message, { status: 500 });
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "m3u8proxy",
3 | "version": "1.0.0",
4 | "description": "",
5 | "devDependencies": {
6 | "@types/node": "^18.11.13",
7 | "@typescript-eslint/eslint-plugin": "^5.59.2",
8 | "@typescript-eslint/parser": "^5.59.7",
9 | "copyfiles": "^2.4.1",
10 | "nodemon": "^3.1.4",
11 | "prettier": "2.8.8",
12 | "ts-node": "^10.9.1",
13 | "typescript": "^4.9.4"
14 | },
15 | "scripts": {
16 | "start": "node ./src/index.js",
17 | "dev": "nodemon ./src/index.js",
18 | "test": "echo \"Error: no test specified\" && exit 1"
19 | },
20 | "dependencies": {
21 | "axios": "^0.27.2",
22 | "colors": "^1.4.0",
23 | "dotenv": "^16.0.3",
24 | "http-proxy": "^1.18.1",
25 | "proxy-from-env": "^1.1.0"
26 | },
27 | "type": "module",
28 | "keywords": [],
29 | "author": "",
30 | "license": "MIT"
31 | }
32 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | M3U8 Proxy
7 |
8 |
9 |
124 |
125 |
126 |
127 | URL
128 |
129 | PLAY
130 | CLEAR
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
157 |
158 |
159 |
204 |
205 |
206 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import server from "./lib/server.js";
2 |
3 | server();
--------------------------------------------------------------------------------
/src/lib/createServer.js:
--------------------------------------------------------------------------------
1 | import getHandler from "./getHandler.js";
2 | import httpProxy from "http-proxy";
3 | import http from "node:http";
4 |
5 | export default function createServer(options) {
6 | options = options || {};
7 |
8 | const httpProxyOptions = {
9 | xfwd: true,
10 | secure: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0",
11 | };
12 |
13 | if (options.httpProxyOptions) {
14 | Object.keys(options.httpProxyOptions).forEach(function (option) {
15 | httpProxyOptions[option] = options.httpProxyOptions[option];
16 | });
17 | }
18 |
19 | const proxyServer = httpProxy.createProxyServer(httpProxyOptions);
20 | const requestHandler = getHandler(options, proxyServer);
21 | let server;
22 |
23 | const handleCors = (req, res) => {
24 | res.setHeader("Access-Control-Allow-Origin", "*");
25 | res.setHeader(
26 | "Access-Control-Allow-Methods",
27 | "GET, POST, PUT, DELETE, OPTIONS"
28 | );
29 | res.setHeader(
30 | "Access-Control-Allow-Headers",
31 | "Content-Type, Authorization"
32 | );
33 | res.setHeader("Access-Control-Allow-Credentials", "true");
34 |
35 | if (req.method === "OPTIONS") {
36 | res.writeHead(204);
37 | res.end();
38 | return true;
39 | }
40 | return false;
41 | };
42 |
43 | const isOriginAllowed = (origin, options) => {
44 | if (options.originWhitelist.includes("*")) {
45 | return true;
46 | }
47 | if (
48 | options.originWhitelist.length &&
49 | !options.originWhitelist.includes(origin)
50 | ) {
51 | return false;
52 | }
53 | if (
54 | options.originBlacklist.length &&
55 | options.originBlacklist.includes(origin)
56 | ) {
57 | return false;
58 | }
59 | return true;
60 | };
61 |
62 | if (options.httpsOptions) {
63 | server = https.createServer(options.httpsOptions, (req, res) => {
64 | const origin = req.headers.origin || "";
65 | if (!isOriginAllowed(origin, options)) {
66 | res.writeHead(403, "Forbidden");
67 | res.end(
68 | `The origin "${origin}" was blacklisted by the operator of this proxy.`
69 | );
70 | return;
71 | }
72 | if (handleCors(req, res)) return;
73 | requestHandler(req, res);
74 | });
75 | } else {
76 | server = http.createServer((req, res) => {
77 | const origin = req.headers.origin || "";
78 | if (!isOriginAllowed(origin, options)) {
79 | res.writeHead(403, "Forbidden");
80 | res.end(
81 | `The origin "${origin}" was blacklisted by the operator of this proxy.`
82 | );
83 | return;
84 | }
85 | if (handleCors(req, res)) return;
86 | requestHandler(req, res);
87 | });
88 | }
89 |
90 | proxyServer.on("error", function (err, req, res) {
91 | console.error("Proxy error:", err);
92 | if (res.headersSent) {
93 | if (!res.writableEnded) {
94 | res.end();
95 | }
96 | return;
97 | }
98 |
99 | const headerNames = res.getHeaderNames
100 | ? res.getHeaderNames()
101 | : Object.keys(res._headers || {});
102 | headerNames.forEach(function (name) {
103 | res.removeHeader(name);
104 | });
105 |
106 | res.writeHead(404, { "Access-Control-Allow-Origin": "*" });
107 | res.end("Not found because of proxy error: " + err);
108 | });
109 |
110 | return server;
111 | }
112 |
--------------------------------------------------------------------------------
/src/lib/getHandler.js:
--------------------------------------------------------------------------------
1 | import { isValidHostName } from "./isValidHostName.js";
2 | import { getProxyForUrl } from "proxy-from-env";
3 | import { readFileSync } from "node:fs";
4 | import { join } from "node:path";
5 | import { fileURLToPath } from "url";
6 | import { dirname } from "path";
7 | import withCORS from "./withCORS.js";
8 | import parseURL from "./parseURL.js";
9 | import proxyM3U8 from "./proxyM3U8.js";
10 | import { proxyTs } from "./proxyTS.js";
11 |
12 | export default function getHandler(options, proxy) {
13 | const corsAnywhere = {
14 | handleInitialRequest: null,
15 | getProxyForUrl: getProxyForUrl,
16 | maxRedirects: 5,
17 | originBlacklist: [],
18 | originWhitelist: [],
19 | checkRateLimit: null,
20 | redirectSameOrigin: false,
21 | requireHeader: null,
22 | removeHeaders: [],
23 | setHeaders: {},
24 | corsMaxAge: 0,
25 | };
26 |
27 | Object.keys(corsAnywhere).forEach(function (option) {
28 | if (Object.prototype.hasOwnProperty.call(options, option)) {
29 | corsAnywhere[option] = options[option];
30 | }
31 | });
32 |
33 | if (corsAnywhere.requireHeader) {
34 | if (typeof corsAnywhere.requireHeader === "string") {
35 | corsAnywhere.requireHeader = [corsAnywhere.requireHeader.toLowerCase()];
36 | } else if (
37 | !Array.isArray(corsAnywhere.requireHeader) ||
38 | corsAnywhere.requireHeader.length === 0
39 | ) {
40 | corsAnywhere.requireHeader = null;
41 | } else {
42 | corsAnywhere.requireHeader = corsAnywhere.requireHeader.map(function (
43 | headerName
44 | ) {
45 | return headerName.toLowerCase();
46 | });
47 | }
48 | }
49 | const hasRequiredHeaders = function (headers) {
50 | return (
51 | !corsAnywhere.requireHeader ||
52 | corsAnywhere.requireHeader.some(function (headerName) {
53 | return Object.hasOwnProperty.call(headers, headerName);
54 | })
55 | );
56 | };
57 |
58 | return function (req, res) {
59 | req.corsAnywhereRequestState = {
60 | getProxyForUrl: corsAnywhere.getProxyForUrl,
61 | maxRedirects: corsAnywhere.maxRedirects,
62 | corsMaxAge: corsAnywhere.corsMaxAge,
63 | };
64 |
65 | const cors_headers = withCORS({}, req);
66 | if (req.method === "OPTIONS") {
67 | res.writeHead(200, cors_headers);
68 | res.end();
69 | return;
70 | }
71 |
72 | const location = parseURL(req.url.slice(1));
73 |
74 | if (
75 | corsAnywhere.handleInitialRequest &&
76 | corsAnywhere.handleInitialRequest(req, res, location)
77 | ) {
78 | return;
79 | }
80 |
81 | if (!location) {
82 | if (/^\/https?:\/[^/]/i.test(req.url)) {
83 | res.writeHead(400, "Missing slash", cors_headers);
84 | res.end(
85 | "The URL is invalid: two slashes are needed after the http(s):."
86 | );
87 | return;
88 | }
89 | const __filename = fileURLToPath(import.meta.url);
90 | const __dirname = dirname(__filename);
91 |
92 | res.end(readFileSync(join(__dirname, "../index.html")));
93 | return;
94 | }
95 |
96 | if (location.host === "iscorsneeded") {
97 | res.writeHead(200, { "Content-Type": "text/plain" });
98 | res.end("no");
99 | return;
100 | }
101 |
102 | if ((Number(location.port) ?? 0) > 65535) {
103 | res.writeHead(400, "Invalid port", cors_headers);
104 | res.end("Port number too large: " + location.port);
105 | return;
106 | }
107 |
108 | if (!/^\/https?:/.test(req.url) && !isValidHostName(location.hostname)) {
109 | const uri = new URL(req.url ?? web_server_url, "http://localhost:3000");
110 | if (uri.pathname === "/m3u8-proxy") {
111 | let headers = {};
112 | try {
113 | headers = JSON.parse(uri.searchParams.get("headers") ?? "{}");
114 | } catch (e) {
115 | res.writeHead(500);
116 | res.end(e.message);
117 | return;
118 | }
119 | const url = uri.searchParams.get("url");
120 | return proxyM3U8(url ?? "", headers, res);
121 | } else if (uri.pathname === "/ts-proxy") {
122 | let headers = {};
123 | try {
124 | headers = JSON.parse(uri.searchParams.get("headers") ?? "{}");
125 | } catch (e) {
126 | res.writeHead(500);
127 | res.end(e.message);
128 | return;
129 | }
130 | const url = uri.searchParams.get("url");
131 | return proxyTs(url ?? "", headers, req, res);
132 | } else if (uri.pathname === "/") {
133 | return res.end(readFileSync(join(__dirname, "../index.html")));
134 | } else {
135 | res.writeHead(404, "Invalid host", cors_headers);
136 | res.end("Invalid host: " + location.hostname);
137 | return;
138 | }
139 | }
140 |
141 | if (!hasRequiredHeaders(req.headers)) {
142 | res.writeHead(400, "Header required", cors_headers);
143 | res.end(
144 | "Missing required request header. Must specify one of: " +
145 | corsAnywhere.requireHeader
146 | );
147 | return;
148 | }
149 |
150 | const origin = req.headers.origin || "";
151 | if (corsAnywhere.originBlacklist.indexOf(origin) >= 0) {
152 | res.writeHead(403, "Forbidden", cors_headers);
153 | res.end(
154 | 'The origin "' +
155 | origin +
156 | '" was blacklisted by the operator of this proxy.'
157 | );
158 | return;
159 | }
160 |
161 | if (
162 | corsAnywhere.originWhitelist.length &&
163 | corsAnywhere.originWhitelist.indexOf(origin) === -1
164 | ) {
165 | res.writeHead(403, "Forbidden", cors_headers);
166 | res.end(
167 | 'The origin "' +
168 | origin +
169 | '" was not whitelisted by the operator of this proxy.'
170 | );
171 | return;
172 | }
173 |
174 | const rateLimitMessage =
175 | corsAnywhere.checkRateLimit && corsAnywhere.checkRateLimit(origin);
176 | if (rateLimitMessage) {
177 | res.writeHead(429, "Too Many Requests", cors_headers);
178 | res.end(
179 | 'The origin "' +
180 | origin +
181 | '" has sent too many requests.\n' +
182 | rateLimitMessage
183 | );
184 | return;
185 | }
186 |
187 | if (
188 | corsAnywhere.redirectSameOrigin &&
189 | origin &&
190 | location.href[origin.length] === "/" &&
191 | location.href.lastIndexOf(origin, 0) === 0
192 | ) {
193 | cors_headers.vary = "origin";
194 | cors_headers["cache-control"] = "private";
195 | cors_headers.location = location.href;
196 | res.writeHead(301, "Please use a direct request", cors_headers);
197 | res.end();
198 | return;
199 | }
200 |
201 | const isRequestedOverHttps =
202 | req.connection.encrypted ||
203 | /^\s*https/.test(req.headers["x-forwarded-proto"]);
204 | const proxyBaseUrl =
205 | (isRequestedOverHttps ? "https://" : "http://") + req.headers.host;
206 |
207 | corsAnywhere.removeHeaders.forEach(function (header) {
208 | delete req.headers[header];
209 | });
210 |
211 | Object.keys(corsAnywhere.setHeaders).forEach(function (header) {
212 | req.headers[header] = corsAnywhere.setHeaders[header];
213 | });
214 |
215 | req.corsAnywhereRequestState.location = location;
216 | req.corsAnywhereRequestState.proxyBaseUrl = proxyBaseUrl;
217 |
218 | proxyRequest(req, res, proxy);
219 | };
220 | }
221 |
--------------------------------------------------------------------------------
/src/lib/isValidHostName.js:
--------------------------------------------------------------------------------
1 | import net from "node:net";
2 |
3 | export function isValidHostName(hostname) {
4 | const regexp =
5 | /\.(?:AAA|AARP|ABARTH|ABB|ABBOTT|ABBVIE|ABC|ABLE|ABOGADO|ABUDHABI|AC|ACADEMY|ACCENTURE|ACCOUNTANT|ACCOUNTANTS|ACO|ACTOR|AD|ADAC|ADS|ADULT|AE|AEG|AERO|AETNA|AF|AFAMILYCOMPANY|AFL|AFRICA|AG|AGAKHAN|AGENCY|AI|AIG|AIRBUS|AIRFORCE|AIRTEL|AKDN|AL|ALFAROMEO|ALIBABA|ALIPAY|ALLFINANZ|ALLSTATE|ALLY|ALSACE|ALSTOM|AM|AMAZON|AMERICANEXPRESS|AMERICANFAMILY|AMEX|AMFAM|AMICA|AMSTERDAM|ANALYTICS|ANDROID|ANQUAN|ANZ|AO|AOL|APARTMENTS|APP|APPLE|AQ|AQUARELLE|AR|ARAB|ARAMCO|ARCHI|ARMY|ARPA|ART|ARTE|AS|ASDA|ASIA|ASSOCIATES|AT|ATHLETA|ATTORNEY|AU|AUCTION|AUDI|AUDIBLE|AUDIO|AUSPOST|AUTHOR|AUTO|AUTOS|AVIANCA|AW|AWS|AX|AXA|AZ|AZURE|BA|BABY|BAIDU|BANAMEX|BANANAREPUBLIC|BAND|BANK|BAR|BARCELONA|BARCLAYCARD|BARCLAYS|BAREFOOT|BARGAINS|BASEBALL|BASKETBALL|BAUHAUS|BAYERN|BB|BBC|BBT|BBVA|BCG|BCN|BD|BE|BEATS|BEAUTY|BEER|BENTLEY|BERLIN|BEST|BESTBUY|BET|BF|BG|BH|BHARTI|BI|BIBLE|BID|BIKE|BING|BINGO|BIO|BIZ|BJ|BLACK|BLACKFRIDAY|BLOCKBUSTER|BLOG|BLOOMBERG|BLUE|BM|BMS|BMW|BN|BNPPARIBAS|BO|BOATS|BOEHRINGER|BOFA|BOM|BOND|BOO|BOOK|BOOKING|BOSCH|BOSTIK|BOSTON|BOT|BOUTIQUE|BOX|BR|BRADESCO|BRIDGESTONE|BROADWAY|BROKER|BROTHER|BRUSSELS|BS|BT|BUDAPEST|BUGATTI|BUILD|BUILDERS|BUSINESS|BUY|BUZZ|BV|BW|BY|BZ|BZH|CA|CAB|CAFE|CAL|CALL|CALVINKLEIN|CAM|CAMERA|CAMP|CANCERRESEARCH|CANON|CAPETOWN|CAPITAL|CAPITALONE|CAR|CARAVAN|CARDS|CARE|CAREER|CAREERS|CARS|CASA|CASE|CASH|CASINO|CAT|CATERING|CATHOLIC|CBA|CBN|CBRE|CBS|CC|CD|CENTER|CEO|CERN|CF|CFA|CFD|CG|CH|CHANEL|CHANNEL|CHARITY|CHASE|CHAT|CHEAP|CHINTAI|CHRISTMAS|CHROME|CHURCH|CI|CIPRIANI|CIRCLE|CISCO|CITADEL|CITI|CITIC|CITY|CITYEATS|CK|CL|CLAIMS|CLEANING|CLICK|CLINIC|CLINIQUE|CLOTHING|CLOUD|CLUB|CLUBMED|CM|CN|CO|COACH|CODES|COFFEE|COLLEGE|COLOGNE|COM|COMCAST|COMMBANK|COMMUNITY|COMPANY|COMPARE|COMPUTER|COMSEC|CONDOS|CONSTRUCTION|CONSULTING|CONTACT|CONTRACTORS|COOKING|COOKINGCHANNEL|COOL|COOP|CORSICA|COUNTRY|COUPON|COUPONS|COURSES|CPA|CR|CREDIT|CREDITCARD|CREDITUNION|CRICKET|CROWN|CRS|CRUISE|CRUISES|CSC|CU|CUISINELLA|CV|CW|CX|CY|CYMRU|CYOU|CZ|DABUR|DAD|DANCE|DATA|DATE|DATING|DATSUN|DAY|DCLK|DDS|DE|DEAL|DEALER|DEALS|DEGREE|DELIVERY|DELL|DELOITTE|DELTA|DEMOCRAT|DENTAL|DENTIST|DESI|DESIGN|DEV|DHL|DIAMONDS|DIET|DIGITAL|DIRECT|DIRECTORY|DISCOUNT|DISCOVER|DISH|DIY|DJ|DK|DM|DNP|DO|DOCS|DOCTOR|DOG|DOMAINS|DOT|DOWNLOAD|DRIVE|DTV|DUBAI|DUCK|DUNLOP|DUPONT|DURBAN|DVAG|DVR|DZ|EARTH|EAT|EC|ECO|EDEKA|EDU|EDUCATION|EE|EG|EMAIL|EMERCK|ENERGY|ENGINEER|ENGINEERING|ENTERPRISES|EPSON|EQUIPMENT|ER|ERICSSON|ERNI|ES|ESQ|ESTATE|ET|ETISALAT|EU|EUROVISION|EUS|EVENTS|EXCHANGE|EXPERT|EXPOSED|EXPRESS|EXTRASPACE|FAGE|FAIL|FAIRWINDS|FAITH|FAMILY|FAN|FANS|FARM|FARMERS|FASHION|FAST|FEDEX|FEEDBACK|FERRARI|FERRERO|FI|FIAT|FIDELITY|FIDO|FILM|FINAL|FINANCE|FINANCIAL|FIRE|FIRESTONE|FIRMDALE|FISH|FISHING|FIT|FITNESS|FJ|FK|FLICKR|FLIGHTS|FLIR|FLORIST|FLOWERS|FLY|FM|FO|FOO|FOOD|FOODNETWORK|FOOTBALL|FORD|FOREX|FORSALE|FORUM|FOUNDATION|FOX|FR|FREE|FRESENIUS|FRL|FROGANS|FRONTDOOR|FRONTIER|FTR|FUJITSU|FUJIXEROX|FUN|FUND|FURNITURE|FUTBOL|FYI|GA|GAL|GALLERY|GALLO|GALLUP|GAME|GAMES|GAP|GARDEN|GAY|GB|GBIZ|GD|GDN|GE|GEA|GENT|GENTING|GEORGE|GF|GG|GGEE|GH|GI|GIFT|GIFTS|GIVES|GIVING|GL|GLADE|GLASS|GLE|GLOBAL|GLOBO|GM|GMAIL|GMBH|GMO|GMX|GN|GODADDY|GOLD|GOLDPOINT|GOLF|GOO|GOODYEAR|GOOG|GOOGLE|GOP|GOT|GOV|GP|GQ|GR|GRAINGER|GRAPHICS|GRATIS|GREEN|GRIPE|GROCERY|GROUP|GS|GT|GU|GUARDIAN|GUCCI|GUGE|GUIDE|GUITARS|GURU|GW|GY|HAIR|HAMBURG|HANGOUT|HAUS|HBO|HDFC|HDFCBANK|HEALTH|HEALTHCARE|HELP|HELSINKI|HERE|HERMES|HGTV|HIPHOP|HISAMITSU|HITACHI|HIV|HK|HKT|HM|HN|HOCKEY|HOLDINGS|HOLIDAY|HOMEDEPOT|HOMEGOODS|HOMES|HOMESENSE|HONDA|HORSE|HOSPITAL|HOST|HOSTING|HOT|HOTELES|HOTELS|HOTMAIL|HOUSE|HOW|HR|HSBC|HT|HU|HUGHES|HYATT|HYUNDAI|IBM|ICBC|ICE|ICU|ID|IE|IEEE|IFM|IKANO|IL|IM|IMAMAT|IMDB|IMMO|IMMOBILIEN|IN|INC|INDUSTRIES|INFINITI|INFO|ING|INK|INSTITUTE|INSURANCE|INSURE|INT|INTERNATIONAL|INTUIT|INVESTMENTS|IO|IPIRANGA|IQ|IR|IRISH|IS|ISMAILI|IST|ISTANBUL|IT|ITAU|ITV|IVECO|JAGUAR|JAVA|JCB|JE|JEEP|JETZT|JEWELRY|JIO|JLL|JM|JMP|JNJ|JO|JOBS|JOBURG|JOT|JOY|JP|JPMORGAN|JPRS|JUEGOS|JUNIPER|KAUFEN|KDDI|KE|KERRYHOTELS|KERRYLOGISTICS|KERRYPROPERTIES|KFH|KG|KH|KI|KIA|KIM|KINDER|KINDLE|KITCHEN|KIWI|KM|KN|KOELN|KOMATSU|KOSHER|KP|KPMG|KPN|KR|KRD|KRED|KUOKGROUP|KW|KY|KYOTO|KZ|LA|LACAIXA|LAMBORGHINI|LAMER|LANCASTER|LANCIA|LAND|LANDROVER|LANXESS|LASALLE|LAT|LATINO|LATROBE|LAW|LAWYER|LB|LC|LDS|LEASE|LECLERC|LEFRAK|LEGAL|LEGO|LEXUS|LGBT|LI|LIDL|LIFE|LIFEINSURANCE|LIFESTYLE|LIGHTING|LIKE|LILLY|LIMITED|LIMO|LINCOLN|LINDE|LINK|LIPSY|LIVE|LIVING|LIXIL|LK|LLC|LLP|LOAN|LOANS|LOCKER|LOCUS|LOFT|LOL|LONDON|LOTTE|LOTTO|LOVE|LPL|LPLFINANCIAL|LR|LS|LT|LTD|LTDA|LU|LUNDBECK|LUXE|LUXURY|LV|LY|MA|MACYS|MADRID|MAIF|MAISON|MAKEUP|MAN|MANAGEMENT|MANGO|MAP|MARKET|MARKETING|MARKETS|MARRIOTT|MARSHALLS|MASERATI|MATTEL|MBA|MC|MCKINSEY|MD|ME|MED|MEDIA|MEET|MELBOURNE|MEME|MEMORIAL|MEN|MENU|MERCKMSD|MG|MH|MIAMI|MICROSOFT|MIL|MINI|MINT|MIT|MITSUBISHI|MK|ML|MLB|MLS|MM|MMA|MN|MO|MOBI|MOBILE|MODA|MOE|MOI|MOM|MONASH|MONEY|MONSTER|MORMON|MORTGAGE|MOSCOW|MOTO|MOTORCYCLES|MOV|MOVIE|MP|MQ|MR|MS|MSD|MT|MTN|MTR|MU|MUSEUM|MUTUAL|MV|MW|MX|MY|MZ|NA|NAB|NAGOYA|NAME|NATIONWIDE|NATURA|NAVY|NBA|NC|NE|NEC|NET|NETBANK|NETFLIX|NETWORK|NEUSTAR|NEW|NEWS|NEXT|NEXTDIRECT|NEXUS|NF|NFL|NG|NGO|NHK|NI|NICO|NIKE|NIKON|NINJA|NISSAN|NISSAY|NL|NO|NOKIA|NORTHWESTERNMUTUAL|NORTON|NOW|NOWRUZ|NOWTV|NP|NR|NRA|NRW|NTT|NU|NYC|NZ|OBI|OBSERVER|OFF|OFFICE|OKINAWA|OLAYAN|OLAYANGROUP|OLDNAVY|OLLO|OM|OMEGA|ONE|ONG|ONL|ONLINE|ONYOURSIDE|OOO|OPEN|ORACLE|ORANGE|ORG|ORGANIC|ORIGINS|OSAKA|OTSUKA|OTT|OVH|PA|PAGE|PANASONIC|PARIS|PARS|PARTNERS|PARTS|PARTY|PASSAGENS|PAY|PCCW|PE|PET|PF|PFIZER|PG|PH|PHARMACY|PHD|PHILIPS|PHONE|PHOTO|PHOTOGRAPHY|PHOTOS|PHYSIO|PICS|PICTET|PICTURES|PID|PIN|PING|PINK|PIONEER|PIZZA|PK|PL|PLACE|PLAY|PLAYSTATION|PLUMBING|PLUS|PM|PN|PNC|POHL|POKER|POLITIE|PORN|POST|PR|PRAMERICA|PRAXI|PRESS|PRIME|PRO|PROD|PRODUCTIONS|PROF|PROGRESSIVE|PROMO|PROPERTIES|PROPERTY|PROTECTION|PRU|PRUDENTIAL|PS|PT|PUB|PW|PWC|PY|QA|QPON|QUEBEC|QUEST|QVC|RACING|RADIO|RAID|RE|READ|REALESTATE|REALTOR|REALTY|RECIPES|RED|REDSTONE|REDUMBRELLA|REHAB|REISE|REISEN|REIT|RELIANCE|REN|RENT|RENTALS|REPAIR|REPORT|REPUBLICAN|REST|RESTAURANT|REVIEW|REVIEWS|REXROTH|RICH|RICHARDLI|RICOH|RIL|RIO|RIP|RMIT|RO|ROCHER|ROCKS|RODEO|ROGERS|ROOM|RS|RSVP|RU|RUGBY|RUHR|RUN|RW|RWE|RYUKYU|SA|SAARLAND|SAFE|SAFETY|SAKURA|SALE|SALON|SAMSCLUB|SAMSUNG|SANDVIK|SANDVIKCOROMANT|SANOFI|SAP|SARL|SAS|SAVE|SAXO|SB|SBI|SBS|SC|SCA|SCB|SCHAEFFLER|SCHMIDT|SCHOLARSHIPS|SCHOOL|SCHULE|SCHWARZ|SCIENCE|SCJOHNSON|SCOT|SD|SE|SEARCH|SEAT|SECURE|SECURITY|SEEK|SELECT|SENER|SERVICES|SES|SEVEN|SEW|SEX|SEXY|SFR|SG|SH|SHANGRILA|SHARP|SHAW|SHELL|SHIA|SHIKSHA|SHOES|SHOP|SHOPPING|SHOUJI|SHOW|SHOWTIME|SI|SILK|SINA|SINGLES|SITE|SJ|SK|SKI|SKIN|SKY|SKYPE|SL|SLING|SM|SMART|SMILE|SN|SNCF|SO|SOCCER|SOCIAL|SOFTBANK|SOFTWARE|SOHU|SOLAR|SOLUTIONS|SONG|SONY|SOY|SPA|SPACE|SPORT|SPOT|SPREADBETTING|SR|SRL|SS|ST|STADA|STAPLES|STAR|STATEBANK|STATEFARM|STC|STCGROUP|STOCKHOLM|STORAGE|STORE|STREAM|STUDIO|STUDY|STYLE|SU|SUCKS|SUPPLIES|SUPPLY|SUPPORT|SURF|SURGERY|SUZUKI|SV|SWATCH|SWIFTCOVER|SWISS|SX|SY|SYDNEY|SYSTEMS|SZ|TAB|TAIPEI|TALK|TAOBAO|TARGET|TATAMOTORS|TATAR|TATTOO|TAX|TAXI|TC|TCI|TD|TDK|TEAM|TECH|TECHNOLOGY|TEL|TEMASEK|TENNIS|TEVA|TF|TG|TH|THD|THEATER|THEATRE|TIAA|TICKETS|TIENDA|TIFFANY|TIPS|TIRES|TIROL|TJ|TJMAXX|TJX|TK|TKMAXX|TL|TM|TMALL|TN|TO|TODAY|TOKYO|TOOLS|TOP|TORAY|TOSHIBA|TOTAL|TOURS|TOWN|TOYOTA|TOYS|TR|TRADE|TRADING|TRAINING|TRAVEL|TRAVELCHANNEL|TRAVELERS|TRAVELERSINSURANCE|TRUST|TRV|TT|TUBE|TUI|TUNES|TUSHU|TV|TVS|TW|TZ|UA|UBANK|UBS|UG|UK|UNICOM|UNIVERSITY|UNO|UOL|UPS|US|UY|UZ|VA|VACATIONS|VANA|VANGUARD|VC|VE|VEGAS|VENTURES|VERISIGN|VERSICHERUNG|VET|VG|VI|VIAJES|VIDEO|VIG|VIKING|VILLAS|VIN|VIP|VIRGIN|VISA|VISION|VIVA|VIVO|VLAANDEREN|VN|VODKA|VOLKSWAGEN|VOLVO|VOTE|VOTING|VOTO|VOYAGE|VU|VUELOS|WALES|WALMART|WALTER|WANG|WANGGOU|WATCH|WATCHES|WEATHER|WEATHERCHANNEL|WEBCAM|WEBER|WEBSITE|WED|WEDDING|WEIBO|WEIR|WF|WHOSWHO|WIEN|WIKI|WILLIAMHILL|WIN|WINDOWS|WINE|WINNERS|WME|WOLTERSKLUWER|WOODSIDE|WORK|WORKS|WORLD|WOW|WS|WTC|WTF|XBOX|XEROX|XFINITY|XIHUAN|XIN|XN--11B4C3D|XN--1CK2E1B|XN--1QQW23A|XN--2SCRJ9C|XN--30RR7Y|XN--3BST00M|XN--3DS443G|XN--3E0B707E|XN--3HCRJ9C|XN--3OQ18VL8PN36A|XN--3PXU8K|XN--42C2D9A|XN--45BR5CYL|XN--45BRJ9C|XN--45Q11C|XN--4DBRK0CE|XN--4GBRIM|XN--54B7FTA0CC|XN--55QW42G|XN--55QX5D|XN--5SU34J936BGSG|XN--5TZM5G|XN--6FRZ82G|XN--6QQ986B3XL|XN--80ADXHKS|XN--80AO21A|XN--80AQECDR1A|XN--80ASEHDB|XN--80ASWG|XN--8Y0A063A|XN--90A3AC|XN--90AE|XN--90AIS|XN--9DBQ2A|XN--9ET52U|XN--9KRT00A|XN--B4W605FERD|XN--BCK1B9A5DRE4C|XN--C1AVG|XN--C2BR7G|XN--CCK2B3B|XN--CCKWCXETD|XN--CG4BKI|XN--CLCHC0EA0B2G2A9GCD|XN--CZR694B|XN--CZRS0T|XN--CZRU2D|XN--D1ACJ3B|XN--D1ALF|XN--E1A4C|XN--ECKVDTC9D|XN--EFVY88H|XN--FCT429K|XN--FHBEI|XN--FIQ228C5HS|XN--FIQ64B|XN--FIQS8S|XN--FIQZ9S|XN--FJQ720A|XN--FLW351E|XN--FPCRJ9C3D|XN--FZC2C9E2C|XN--FZYS8D69UVGM|XN--G2XX48C|XN--GCKR3F0F|XN--GECRJ9C|XN--GK3AT1E|XN--H2BREG3EVE|XN--H2BRJ9C|XN--H2BRJ9C8C|XN--HXT814E|XN--I1B6B1A6A2E|XN--IMR513N|XN--IO0A7I|XN--J1AEF|XN--J1AMH|XN--J6W193G|XN--JLQ480N2RG|XN--JLQ61U9W7B|XN--JVR189M|XN--KCRX77D1X4A|XN--KPRW13D|XN--KPRY57D|XN--KPUT3I|XN--L1ACC|XN--LGBBAT1AD8J|XN--MGB9AWBF|XN--MGBA3A3EJT|XN--MGBA3A4F16A|XN--MGBA7C0BBN0A|XN--MGBAAKC7DVF|XN--MGBAAM7A8H|XN--MGBAB2BD|XN--MGBAH1A3HJKRD|XN--MGBAI9AZGQP6J|XN--MGBAYH7GPA|XN--MGBBH1A|XN--MGBBH1A71E|XN--MGBC0A9AZCG|XN--MGBCA7DZDO|XN--MGBCPQ6GPA1A|XN--MGBERP4A5D4AR|XN--MGBGU82A|XN--MGBI4ECEXP|XN--MGBPL2FH|XN--MGBT3DHD|XN--MGBTX2B|XN--MGBX4CD0AB|XN--MIX891F|XN--MK1BU44C|XN--MXTQ1M|XN--NGBC5AZD|XN--NGBE9E0A|XN--NGBRX|XN--NODE|XN--NQV7F|XN--NQV7FS00EMA|XN--NYQY26A|XN--O3CW4H|XN--OGBPF8FL|XN--OTU796D|XN--P1ACF|XN--P1AI|XN--PGBS0DH|XN--PSSY2U|XN--Q7CE6A|XN--Q9JYB4C|XN--QCKA1PMC|XN--QXA6A|XN--QXAM|XN--RHQV96G|XN--ROVU88B|XN--RVC1E0AM3E|XN--S9BRJ9C|XN--SES554G|XN--T60B56A|XN--TCKWE|XN--TIQ49XQYJ|XN--UNUP4Y|XN--VERMGENSBERATER-CTB|XN--VERMGENSBERATUNG-PWB|XN--VHQUV|XN--VUQ861B|XN--W4R85EL8FHU5DNRA|XN--W4RS40L|XN--WGBH1C|XN--WGBL6A|XN--XHQ521B|XN--XKC2AL3HYE2A|XN--XKC2DL3A5EE0H|XN--Y9A3AQ|XN--YFRO4I67O|XN--YGBI2AMMX|XN--ZFR164B|XXX|XYZ|YACHTS|YAHOO|YAMAXUN|YANDEX|YE|YODOBASHI|YOGA|YOKOHAMA|YOU|YOUTUBE|YT|YUN|ZA|ZAPPOS|ZARA|ZERO|ZIP|ZM|ZONE|ZUERICH|ZW)$/i;
6 | return !!(
7 | regexp.test(hostname) ||
8 | net.isIPv4(hostname) ||
9 | net.isIPv6(hostname)
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/lib/parseURL.js:
--------------------------------------------------------------------------------
1 | import url from "node:url";
2 |
3 | export default function parseURL(req_url) {
4 | const match = req_url.match(
5 | /^(?:(https?:)?\/\/)?(([^\/?]+?)(?::(\d{0,5})(?=[\/?]|$))?)([\/?][\S\s]*|$)/i
6 | );
7 | if (!match) {
8 | return null;
9 | }
10 | if (!match[1]) {
11 | if (/^https?:/i.test(req_url)) {
12 | return null;
13 | }
14 | if (req_url.lastIndexOf("//", 0) === -1) {
15 | req_url = "//" + req_url;
16 | }
17 | req_url = (match[4] === "443" ? "https:" : "http:") + req_url;
18 | }
19 | const parsed = url.parse(req_url);
20 | if (!parsed.hostname) {
21 | return null;
22 | }
23 | return parsed;
24 | }
25 |
--------------------------------------------------------------------------------
/src/lib/proxyM3U8.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import dotenv from "dotenv";
3 |
4 | dotenv.config();
5 |
6 | const host = process.env.HOST || "127.0.0.1";
7 | const port = process.env.PORT || 8080;
8 | const web_server_url = process.env.PUBLIC_URL || `http://${host}:${port}`;
9 |
10 | export default async function proxyM3U8(url, headers, res) {
11 | const req = await axios(url, {
12 | headers: headers,
13 | }).catch((err) => {
14 | res.writeHead(500);
15 | res.end(err.message);
16 | return null;
17 | });
18 | if (!req) {
19 | return;
20 | }
21 | const m3u8 = req.data
22 | .split("\n")
23 | //now it supports also proxying multi-audio streams
24 | // .filter((line) => !line.startsWith("#EXT-X-MEDIA:TYPE=AUDIO"))
25 | .join("\n");
26 | if (m3u8.includes("RESOLUTION=")) {
27 | const lines = m3u8.split("\n");
28 | const newLines = [];
29 | for (const line of lines) {
30 | if (line.startsWith("#")) {
31 | if (line.startsWith("#EXT-X-KEY:")) {
32 | const regex = /https?:\/\/[^\""\s]+/g;
33 | const url = `${web_server_url}${
34 | "/ts-proxy?url=" +
35 | encodeURIComponent(regex.exec(line)?.[0] ?? "") +
36 | "&headers=" +
37 | encodeURIComponent(JSON.stringify(headers))
38 | }`;
39 | newLines.push(line.replace(regex, url));
40 | } else if (line.startsWith("#EXT-X-MEDIA:TYPE=AUDIO")) {
41 | const regex = /https?:\/\/[^\""\s]+/g;
42 | const url = `${web_server_url}${
43 | "/m3u8-proxy?url=" +
44 | encodeURIComponent(regex.exec(line)?.[0] ?? "") +
45 | "&headers=" +
46 | encodeURIComponent(JSON.stringify(headers))
47 | }`;
48 | newLines.push(line.replace(regex, url));
49 | } else {
50 | newLines.push(line);
51 | }
52 | } else {
53 | const uri = new URL(line, url);
54 | newLines.push(
55 | `${
56 | web_server_url +
57 | "/m3u8-proxy?url=" +
58 | encodeURIComponent(uri.href) +
59 | "&headers=" +
60 | encodeURIComponent(JSON.stringify(headers))
61 | }`
62 | );
63 | }
64 | }
65 |
66 | [
67 | "Access-Control-Allow-Origin",
68 | "Access-Control-Allow-Methods",
69 | "Access-Control-Allow-Headers",
70 | "Access-Control-Max-Age",
71 | "Access-Control-Allow-Credentials",
72 | "Access-Control-Expose-Headers",
73 | "Access-Control-Request-Method",
74 | "Access-Control-Request-Headers",
75 | "Origin",
76 | "Vary",
77 | "Referer",
78 | "Server",
79 | "x-cache",
80 | "via",
81 | "x-amz-cf-pop",
82 | "x-amz-cf-id",
83 | ].map((header) => res.removeHeader(header));
84 |
85 | res.setHeader("Content-Type", "application/vnd.apple.mpegurl");
86 | res.setHeader("Access-Control-Allow-Origin", "*");
87 | res.setHeader("Access-Control-Allow-Headers", "*");
88 | res.setHeader("Access-Control-Allow-Methods", "*");
89 |
90 | res.end(newLines.join("\n"));
91 | return;
92 | } else {
93 | const lines = m3u8.split("\n");
94 | const newLines = [];
95 | for (const line of lines) {
96 | if (line.startsWith("#")) {
97 | if (line.startsWith("#EXT-X-KEY:")) {
98 | const regex = /https?:\/\/[^\""\s]+/g;
99 | const url = `${web_server_url}${
100 | "/ts-proxy?url=" +
101 | encodeURIComponent(regex.exec(line)?.[0] ?? "") +
102 | "&headers=" +
103 | encodeURIComponent(JSON.stringify(headers))
104 | }`;
105 | newLines.push(line.replace(regex, url));
106 | } else {
107 | newLines.push(line);
108 | }
109 | } else {
110 | const uri = new URL(line, url);
111 |
112 | newLines.push(
113 | `${web_server_url}${
114 | "/ts-proxy?url=" +
115 | encodeURIComponent(uri.href) +
116 | "&headers=" +
117 | encodeURIComponent(JSON.stringify(headers))
118 | }`
119 | );
120 | }
121 | }
122 |
123 | [
124 | "Access-Control-Allow-Origin",
125 | "Access-Control-Allow-Methods",
126 | "Access-Control-Allow-Headers",
127 | "Access-Control-Max-Age",
128 | "Access-Control-Allow-Credentials",
129 | "Access-Control-Expose-Headers",
130 | "Access-Control-Request-Method",
131 | "Access-Control-Request-Headers",
132 | "Origin",
133 | "Vary",
134 | "Referer",
135 | "Server",
136 | "x-cache",
137 | "via",
138 | "x-amz-cf-pop",
139 | "x-amz-cf-id",
140 | ].map((header) => res.removeHeader(header));
141 |
142 | res.setHeader("Content-Type", "application/vnd.apple.mpegurl");
143 | res.setHeader("Access-Control-Allow-Origin", "*");
144 | res.setHeader("Access-Control-Allow-Headers", "*");
145 | res.setHeader("Access-Control-Allow-Methods", "*");
146 |
147 | res.end(newLines.join("\n"));
148 | return;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/lib/proxyRequest.js:
--------------------------------------------------------------------------------
1 | import url from "node:url";
2 | import parseURL from "./parseURL.js";
3 | import withCORS from "./withCORS.js";
4 |
5 |
6 | export default function onProxyResponse(proxy, proxyReq, proxyRes, req, res) {
7 | const requestState = req.corsAnywhereRequestState;
8 |
9 | const statusCode = proxyRes.statusCode;
10 |
11 | if (!requestState.redirectCount_) {
12 | res.setHeader("x-request-url", requestState.location.href);
13 | }
14 | if (
15 | statusCode === 301 ||
16 | statusCode === 302 ||
17 | statusCode === 303 ||
18 | statusCode === 307 ||
19 | statusCode === 308
20 | ) {
21 | let locationHeader = proxyRes.headers.location;
22 | let parsedLocation;
23 | if (locationHeader) {
24 | locationHeader = url.resolve(requestState.location.href, locationHeader);
25 | parsedLocation = parseURL(locationHeader);
26 | }
27 | if (parsedLocation) {
28 | if (statusCode === 301 || statusCode === 302 || statusCode === 303) {
29 | requestState.redirectCount_ = requestState.redirectCount_ + 1 || 1;
30 | if (requestState.redirectCount_ <= requestState.maxRedirects) {
31 | res.setHeader(
32 | "X-CORS-Redirect-" + requestState.redirectCount_,
33 | statusCode + " " + locationHeader
34 | );
35 |
36 | req.method = "GET";
37 | req.headers["content-length"] = "0";
38 | delete req.headers["content-type"];
39 | requestState.location = parsedLocation;
40 | req.removeAllListeners();
41 | proxyReq.removeAllListeners("error");
42 | proxyReq.once("error", function catchAndIgnoreError() {});
43 | proxyReq.abort();
44 | proxyRequest(req, res, proxy);
45 | return false;
46 | }
47 | }
48 | proxyRes.headers.location =
49 | requestState.proxyBaseUrl + "/" + locationHeader;
50 | }
51 | }
52 |
53 | delete proxyRes.headers["set-cookie"];
54 | delete proxyRes.headers["set-cookie2"];
55 |
56 | proxyRes.headers["x-final-url"] = requestState.location.href;
57 | withCORS(proxyRes.headers, req);
58 | return true;
59 | }
60 |
61 | function proxyRequest(req, res, proxy) {
62 | const location = req.corsAnywhereRequestState.location;
63 | req.url = location.path;
64 |
65 | const proxyOptions = {
66 | changeOrigin: false,
67 | prependPath: false,
68 | target: location,
69 | headers: {
70 | host: location.host,
71 | },
72 | buffer: {
73 | pipe: function (proxyReq) {
74 | const proxyReqOn = proxyReq.on;
75 | proxyReq.on = function (eventName, listener) {
76 | if (eventName !== "response") {
77 | return proxyReqOn.call(this, eventName, listener);
78 | }
79 | return proxyReqOn.call(this, "response", function (proxyRes) {
80 | if (onProxyResponse(proxy, proxyReq, proxyRes, req, res)) {
81 | try {
82 | listener(proxyRes);
83 | } catch (err) {
84 | proxyReq.emit("error", err);
85 | }
86 | }
87 | });
88 | };
89 | return req.pipe(proxyReq);
90 | },
91 | },
92 | };
93 |
94 | const proxyThroughUrl = req.corsAnywhereRequestState.getProxyForUrl(
95 | location.href
96 | );
97 | if (proxyThroughUrl) {
98 | proxyOptions.target = proxyThroughUrl;
99 | proxyOptions.toProxy = true;
100 | req.url = location.href;
101 | }
102 | try {
103 | proxy.web(req, res, proxyOptions);
104 | } catch (err) {
105 | console.error(err);
106 | console.log(proxy);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/lib/proxyTS.js:
--------------------------------------------------------------------------------
1 | import https from "node:https";
2 | import http from "node:http";
3 |
4 | export async function proxyTs(url, headers, req, res) {
5 | let forceHTTPS = false;
6 |
7 | if (url.startsWith("https://")) {
8 | forceHTTPS = true;
9 | }
10 |
11 | const uri = new URL(url);
12 | const options = {
13 | hostname: uri.hostname,
14 | port: uri.port,
15 | path: uri.pathname + uri.search,
16 | method: req.method,
17 | headers: {
18 | "User-Agent":
19 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36",
20 | ...headers,
21 | },
22 | };
23 | res.setHeader("Access-Control-Allow-Origin", "*");
24 | res.setHeader("Access-Control-Allow-Headers", "*");
25 | res.setHeader("Access-Control-Allow-Methods", "*");
26 |
27 | try {
28 | if (forceHTTPS) {
29 | const proxy = https.request(options, (r) => {
30 | r.headers["content-type"] = "video/mp2t";
31 | res.writeHead(r.statusCode ?? 200, r.headers);
32 |
33 | r.pipe(res, {
34 | end: true,
35 | });
36 | });
37 |
38 | req.pipe(proxy, {
39 | end: true,
40 | });
41 | } else {
42 | const proxy = http.request(options, (r) => {
43 | r.headers["content-type"] = "video/mp2t";
44 | res.writeHead(r.statusCode ?? 200, r.headers);
45 |
46 | r.pipe(res, {
47 | end: true,
48 | });
49 | });
50 | req.pipe(proxy, {
51 | end: true,
52 | });
53 | }
54 | } catch (e) {
55 | res.writeHead(500);
56 | res.end(e.message);
57 | return null;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/lib/server.js:
--------------------------------------------------------------------------------
1 | import dotenv from "dotenv";
2 | import createServer from "./createServer.js";
3 | import colors from "colors";
4 |
5 | dotenv.config();
6 |
7 | const host = process.env.HOST || "127.0.0.1";
8 | const port = process.env.PORT || 8080;
9 | const web_server_url = process.env.PUBLIC_URL || `http://${host}:${port}`;
10 |
11 | export default function server() {
12 | createServer({
13 | originBlacklist: ["*"],
14 | originWhitelist: process.env.ALLOWED_ORIGINS
15 | ? process.env.ALLOWED_ORIGINS.split(",")
16 | : [],
17 | requireHeader: [],
18 | removeHeaders: [
19 | "cookie",
20 | "cookie2",
21 | "x-request-start",
22 | "x-request-id",
23 | "via",
24 | "connect-time",
25 | "total-route-time",
26 | ],
27 | redirectSameOrigin: true,
28 | httpProxyOptions: {
29 | xfwd: false,
30 | },
31 | }).listen(port, Number(host), function () {
32 | console.log(
33 | colors.green("Server running on ") + colors.blue(`${web_server_url}`)
34 | );
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/src/lib/withCORS.js:
--------------------------------------------------------------------------------
1 | export default function withCORS(headers, request) {
2 | headers["access-control-allow-origin"] = "*";
3 | const corsMaxAge = request.corsAnywhereRequestState.corsMaxAge;
4 | if (request.method === "OPTIONS" && corsMaxAge) {
5 | headers["access-control-max-age"] = corsMaxAge;
6 | }
7 | if (request.headers["access-control-request-method"]) {
8 | headers["access-control-allow-methods"] =
9 | request.headers["access-control-request-method"];
10 | delete request.headers["access-control-request-method"];
11 | }
12 | if (request.headers["access-control-request-headers"]) {
13 | headers["access-control-allow-headers"] =
14 | request.headers["access-control-request-headers"];
15 | delete request.headers["access-control-request-headers"];
16 | }
17 | headers["access-control-expose-headers"] = Object.keys(headers).join(",");
18 | return headers;
19 | }
20 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "builds": [
4 | {
5 | "src": "/src/index.js",
6 | "use": "@vercel/node"
7 | }
8 | ],
9 | "routes": [
10 | {
11 | "src": "/(.*)",
12 | "dest": "/src/index.js",
13 | "headers": {
14 | "Access-Control-Allow-Origin": "*"
15 | }
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------