diff --git a/.env b/.env new file mode 100644 index 00000000..97c4080d --- /dev/null +++ b/.env @@ -0,0 +1 @@ +REACT_APP_CORS_PROXY_URL=https://PROXY.workers.dev?destination= \ No newline at end of file diff --git a/README.md b/README.md index 78a3d049..1a9b95b4 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Small web app for watching movies easily. Check it out at **[movie.squeezebox.de ## Credits - Thanks to [@JipFr](https://github.com/JipFr) for initial work on [movie-cli](https://github.com/JipFr/movie-cli) - Thanks to [@mrjvs](https://github.com/mrjvs) for help porting to React, and for the beautiful design + - Thanks to [@JoshHeng](https://github.com/JoshHeng/) for the Cloudflare CORS Proxy and URL routing ## Installation To run this project locally for contributing or testing, run the following commands: @@ -18,5 +19,8 @@ yarn start ``` To build production files, simply run `yarn build`. +## Environment +* `REACT_APP_CORS_PROXY_URL` - The Cloudflare CORS Proxy, will be something like `https://PROXY.workers.dev?destination=` + ## Contributing Check out [this project's issues](https://github.com/JamesHawkinss/movie-web/issues) for inspiration for contribution. Pull requests are always welcome. \ No newline at end of file diff --git a/src/lib/scraper/gomostream.js b/src/lib/scraper/gomostream.js index 07b067e0..f93eb171 100644 --- a/src/lib/scraper/gomostream.js +++ b/src/lib/scraper/gomostream.js @@ -1,7 +1,6 @@ import { unpack } from '../util/unpacker'; -const CORS_URL = 'https://movie-web-proxy.herokuapp.com/'; -const BASE_URL = `${CORS_URL}https://gomo.to`; +const BASE_URL = `${process.env.REACT_APP_CORS_PROXY_URL}https://gomo.to`; const MOVIE_URL = `${BASE_URL}/movie` const DECODING_URL = `${BASE_URL}/decoding_v3.php` @@ -10,7 +9,7 @@ async function findContent(searchTerm, type) { if (type !== 'movie') return; const term = searchTerm.toLowerCase() - const imdbRes = await fetch(`${CORS_URL}https://v2.sg.media-imdb.com/suggestion/${term.slice(0, 1)}/${term}.json`).then(d => d.json()) + const imdbRes = await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}https://v2.sg.media-imdb.com/suggestion/${term.slice(0, 1)}/${term}.json`).then(d => d.json()) const results = []; imdbRes.d.forEach((e) => { @@ -68,7 +67,7 @@ async function getStreamUrl(slug, type, season, episode) { }).then((d) => d.json()); const embedUrl = src.find(url => url.includes('gomo.to')); - const site2 = await fetch(`${CORS_URL}${embedUrl}`).then((d) => d.text()); + const site2 = await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}${embedUrl}`).then((d) => d.text()); const parser = new DOMParser(); const site2Dom = parser.parseFromString(site2, "text/html"); diff --git a/src/lib/scraper/lookmovie.js b/src/lib/scraper/lookmovie.js index 9fafd3ce..5b28f120 100644 --- a/src/lib/scraper/lookmovie.js +++ b/src/lib/scraper/lookmovie.js @@ -1,8 +1,7 @@ import Fuse from 'fuse.js' import JSON5 from 'json5' -const CORS_URL = `https://movie-web-proxy.herokuapp.com`; -const BASE_URL = `${CORS_URL}/https://lookmovie.io`; +const BASE_URL = `${process.env.REACT_APP_CORS_PROXY_URL}https://lookmovie.io`; async function findContent(searchTerm, type) { const searchUrl = `${BASE_URL}/${type}s/search/?q=${encodeURIComponent(searchTerm)}`; diff --git a/src/views/Search.js b/src/views/Search.js index 836c9088..9dbb5021 100644 --- a/src/views/Search.js +++ b/src/views/Search.js @@ -111,8 +111,7 @@ export function SearchView() { React.useEffect(() => { async function fetchHealth() { - const HOME_URL = "https://movie-web-proxy.herokuapp.com" - await fetch(HOME_URL).catch(() => { + await fetch(process.env.REACT_APP_CORS_PROXY_URL).catch(() => { // Request failed; source likely offline setErrorStatus(`Our content provider is currently offline, apologies.`) }) diff --git a/worker.js b/worker.js new file mode 100644 index 00000000..fe1774fb --- /dev/null +++ b/worker.js @@ -0,0 +1,115 @@ +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", + "Access-Control-Max-Age": "86400", +} + +const allowedDomains = [ + 'https://v2.sg.media-imdb.com', + 'https://gomo.to', + 'https://lookmovie.io', + 'https://gomoplayer.com' +]; + +async function handleRequest(request, destinationUrl, iteration = 0) { + console.log(`PROXYING ${destinationUrl}${iteration ? ' ON ITERATION ' + iteration : ''}`); + + // Rewrite request to point to API url. This also makes the request mutable + // so we can add the correct Origin header to make the API server think + // that this request isn't cross-site. + request = new Request(destinationUrl, request); + request.headers.set("Origin", new URL(destinationUrl).origin); + + let response = await fetch(request); + + if ((response.status === 302 || response.status === 301) && response.headers.get('location')) { + if (iteration > 5) { + event.respondWith( + new Response('418 Too many redirects', { + status: 418 + }), + ); + } + + return await handleRequest(request, response.headers.get('location'), iteration + 1) + } + + // Recreate the response so we can modify the headers + response = new Response(response.body, response); + + // Set CORS headers + response.headers.set("Access-Control-Allow-Origin", '*'); + + // Append to/Add Vary header so browser will cache response correctly + response.headers.append("Vary", "Origin"); + + return response; +} + +function handleOptions(request) { + // Make sure the necessary headers are present + // for this to be a valid pre-flight request + let headers = request.headers; + + if (headers.get("Origin") !== null && headers.get("Access-Control-Request-Method") !== null && headers.get("Access-Control-Request-Headers") !== null ) { + return new Response(null, { + headers: { + ...corsHeaders, + // Allow all future content Request headers to go back to browser + // such as Authorization (Bearer) or X-Client-Name-Version + "Access-Control-Allow-Headers": request.headers.get("Access-Control-Request-Headers"), + }, + }); + } + else { + // Handle standard OPTIONS request + return new Response(null, { + headers: { + Allow: "GET, HEAD, POST, OPTIONS", + }, + }) + } +} + +addEventListener("fetch", event => { + const request = event.request + const url = new URL(request.url); + const destinationUrl = url.searchParams.get("destination"); + + console.log(`HTTP ${request.method} - ${request.url}` ); + + + if (request.method === "OPTIONS") { + // Handle CORS preflight requests + event.respondWith(handleOptions(request)); + } + else if (!destinationUrl) { + event.respondWith( + new Response('200 OK', { + status: 200, + headers: { + 'Allow': "GET, HEAD, POST, OPTIONS", + 'Access-Control-Allow-Origin': '*' + }, + }), + ); + } + else if (!allowedDomains.find(domain => destinationUrl.startsWith(domain))) { + event.respondWith( + new Response('404 Not Found', { + status: 404, + }), + ); + } + else if (request.method === "GET" || request.method === "HEAD" || request.method === "POST") { + // Handle request + event.respondWith(handleRequest(request, destinationUrl)); + } + else { + event.respondWith( + new Response('404 Not Found', { + status: 404, + }), + ); + } +}); \ No newline at end of file