├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── bunnycdn_edge_script.js ├── m3u8proxy(cf_worker).js ├── package.json ├── src ├── index.html ├── index.js └── lib │ ├── createServer.js │ ├── getHandler.js │ ├── isValidHostName.js │ ├── parseURL.js │ ├── proxyM3U8.js │ ├── proxyRequest.js │ ├── proxyTS.js │ ├── server.js │ └── withCORS.js └── vercel.json /.env.example: -------------------------------------------------------------------------------- 1 | HOST=host #optional 2 | PORT=port_number #optional 3 | PUBLIC_URL=url_of_the_deployed_m3u8_server #mandatory 4 | ALLOWED_ORIGINS=https://site1.com,https://site2.com #mandatory 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | .env 4 | dist 5 | package-lock.json 6 | .idea 7 | yarn.lock 8 | pnpm-lock.yaml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sayan Das 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # M3U8 Proxy Server 2 | 3 | Welcome to the M3U8 Proxy Server! Tired of dealing with those pesky CORS errors when trying to stream your M3U8 files? Fear not, because our proxy server is here to save the day! 4 | 5 | ## 🎯 What This Project Does 6 | 7 | This Node.js application acts as a middleman between your requests and the M3U8 files you're trying to access. Just drop your M3U8 link into the input box, and we'll take care of the rest. Our server proxies the M3U8 file and provides you with a new link to use—free from CORS troubles. 8 | 9 | ## Deployment 10 | 11 | ### Vercel 12 | 13 | Host your own instance of M3U8-Proxy on vercel 14 | 15 | [![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/itzzzme/m3u8proxy) 16 | ### Render 17 | 18 | Host your own instance of M3U8-Proxy on Render. 19 | 20 | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/itzzzme/m3u8proxy) 21 | 22 | ### Cloudflare-worker 23 | - Create an example starter worker in your cloudflare account 24 | - Edit the code of the worker after deploying it 25 | - Override the code of your starter worker with the code given in `m3u8proxy(cf_worker).js` 26 | - Deploy your worker 27 | 28 | ## 🚀 How It Works 29 | 30 | 1. **Enter Your M3U8 Link**: Simply input your M3U8 URL into the provided input box. 31 | 2. **Get a Proxied Link**: Our server fetches the M3U8 file and proxies it for you. 32 | 3. **Use the Proxied Link**: Replace your original link with the new proxied link in your platform. 33 | 34 | ## ⚙️ Envs 35 | 36 | More info can be found in [`.env.example`](https://github.com/itzzzme/m3u8proxy/blob/main/.env.example) file 37 | 38 | - `HOST`: host of your proxy server `optional` 39 | - `PORT`: port number (any) `optional` 40 | - `PUBLIC_URL`: link of your website `mandatory` 41 | - `ALLOWED_ORIGINS`: origins you want to allow `mandatory` 42 | 43 | 44 | ## 🕵️‍♂️ How to Use 45 | 46 | If you want to use the proxy directly, append your M3U8 link to the following URL: 47 | 48 | [https:///m3u8-proxy?url={url}](https:///m3u8-proxy?url={url}) 49 | 50 | For example, if your M3U8 link is `https://example.com/stream.m3u8`, you would use: 51 | 52 | [https:///m3u8-proxy?url=https://example.com/stream.m3u8](https:///m3u8-proxy?url=https://example.com/stream.m3u8) 53 | 54 | ## 🎥 Video Demo 55 | 56 | You can watch a demo of the proxy server in action using the MP4 video file below: 57 | 58 | https://github.com/user-attachments/assets/ae8e74a6-176d-42a4-8590-3b7a2b7182ff 59 | 60 | 61 | 62 | ## 📹 Example Code Snippet 63 | 64 | Here’s how you can use Video.js to play the proxied URL: 65 | 66 | ```html 67 | 68 | 69 | 70 | 71 | 72 | Proxied M3U8 Player 73 | 74 | 75 | 76 |
77 | 80 |
81 | 82 | 83 | 84 | 87 | 88 | 89 | 90 | ``` 91 | 92 | ## 😂 A Note About CORS 93 | 94 | CORS, oh CORS—you're like that friend who always shows up uninvited. But don’t worry, our proxy server is the bouncer that keeps you and your streams happy and stress-free. No more unwanted guests in the form of CORS errors! 95 | 96 | ## 📜 License 97 | 98 | This project is licensed under the MIT License. 99 | 100 | ## 🤝 Contributing 101 | 102 | Feel free to open issues or pull requests if you have suggestions or improvements! 103 | 104 |
105 | 106 |

Made by itzzzme 107 | 🫰

108 | 109 | -------------------------------------------------------------------------------- /bunnycdn_edge_script.js: -------------------------------------------------------------------------------- 1 | import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.11.2"; 2 | 3 | /** 4 | * @param {Request} request - 5 | * @return {Response} 6 | */ 7 | 8 | // Default server URL provided manually 9 | const web_server_url = ""; // Replace this with your public URL 10 | const REFERER = ""; 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 | 128 | 129 | 130 | 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 | } --------------------------------------------------------------------------------