From 8a5a5c7d1e6ab733254be6a7e6f46a4f5fab45fb Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Sun, 4 Dec 2022 13:58:29 +0100 Subject: [PATCH] UserScript: add csp bypassing fetch (#284) --- browser/GMPolyfill.js | 107 +++++++++++++++++++++++++++++++++++++ browser/userscript.meta.js | 2 +- scripts/build/buildWeb.mjs | 7 ++- 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 browser/GMPolyfill.js diff --git a/browser/GMPolyfill.js b/browser/GMPolyfill.js new file mode 100644 index 00000000..3e0606d7 --- /dev/null +++ b/browser/GMPolyfill.js @@ -0,0 +1,107 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +function fetchOptions(url) { + return new Promise((resolve, reject) => { + const opt = { + method: "OPTIONS", + url: url, + }; + opt.onload = resp => resolve(resp.responseHeaders); + opt.ontimeout = () => reject("fetch timeout"); + opt.onerror = () => reject("fetch error"); + opt.onabort = () => reject("fetch abort"); + GM_xmlhttpRequest(opt); + }); +} + +function parseHeaders(headers) { + if (!headers) + return {}; + const result = {}; + const headersArr = headers.trim().split("\n"); + for (var i = 0; i < headersArr.length; i++) { + var row = headersArr[i]; + var index = row.indexOf(":") + , key = row.slice(0, index).trim().toLowerCase() + , value = row.slice(index + 1).trim(); + + if (result[key] === undefined) { + result[key] = value; + } else if (Array.isArray(result[key])) { + result[key].push(value); + } else { + result[key] = [result[key], value]; + } + } + return result; +} + +// returns true if CORS permits request +async function checkCors(url, method) { + const headers = parseHeaders(await fetchOptions(url)); + + const origin = headers["access-control-allow-origin"]; + if (origin !== "*" && origin !== window.location.origin) return false; + + const methods = headers["access-control-allow-methods"]?.split(/,\s/g); + if (methods && !methods.includes(method)) return false; + + return true; +} + +function blobTo(to, blob) { + if (to === "arrayBuffer" && blob.arrayBuffer) return blob.arrayBuffer(); + return new Promise((resolve, reject) => { + var fileReader = new FileReader(); + fileReader.onload = event => resolve(event.target.result); + if (to === "arrayBuffer") fileReader.readAsArrayBuffer(blob); + else if (to === "text") fileReader.readAsText(blob, "utf-8"); + else reject("unknown to"); + }); +} + +function GM_fetch(url, opt) { + return new Promise((resolve, reject) => { + checkCors(url, opt?.method || "GET") + .then(can => { + if (can) { + // https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest + const options = opt || {}; + options.url = url; + options.data = options.body; + options.responseType = "blob"; + options.onload = resp => { + var blob = resp.response; + resp.blob = () => Promise.resolve(blob); + resp.arrayBuffer = () => blobTo("arrayBuffer", blob); + resp.text = () => blobTo("text", blob); + resp.json = async () => JSON.parse(await blobTo("text", blob)); + resolve(resp); + }; + options.ontimeout = () => reject("fetch timeout"); + options.onerror = () => reject("fetch error"); + options.onabort = () => reject("fetch abort"); + GM_xmlhttpRequest(options); + } else { + reject("CORS issue"); + } + }); + }); +} +export const fetch = GM_fetch; diff --git a/browser/userscript.meta.js b/browser/userscript.meta.js index 81cf3e7b..5b2a39be 100644 --- a/browser/userscript.meta.js +++ b/browser/userscript.meta.js @@ -7,7 +7,7 @@ // @supportURL https://github.com/Vendicated/Vencord // @license GPL-3.0 // @match *://*.discord.com/* -// @grant none +// @grant GM_xmlhttpRequest // @run-at document-start // @compatible chrome Chrome + Tampermonkey or Violentmonkey // @compatible firefox Firefox Tampermonkey diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index 7508937a..c85d8aad 100755 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -60,13 +60,18 @@ await Promise.all( }), esbuild.build({ ...commonOptions, + inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])], + define: { + "window": "unsafeWindow", + ...(commonOptions?.define) + }, outfile: "dist/Vencord.user.js", banner: { js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${PackageJSON.version}.${new Date().getTime()}`) }, footer: { // UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local - js: "Object.defineProperty(window,'Vencord',{get:()=>Vencord});" + js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});" }, }) ]