mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-20 03:47:01 -04:00
Allow users to manually whitelist Domains for use in themes (#3476)
This commit is contained in:
parent
7f2c4a3566
commit
ed5ed4b80a
12 changed files with 313 additions and 46 deletions
|
@ -115,4 +115,5 @@ window.VencordNative = {
|
||||||
},
|
},
|
||||||
|
|
||||||
pluginHelpers: {} as any,
|
pluginHelpers: {} as any,
|
||||||
|
csp: {} as any,
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Settings } from "@api/Settings";
|
import type { Settings } from "@api/Settings";
|
||||||
|
import { CspRequestResult } from "@main/csp/manager";
|
||||||
import { PluginIpcMappings } from "@main/ipcPlugins";
|
import { PluginIpcMappings } from "@main/ipcPlugins";
|
||||||
import type { UserThemeHeader } from "@main/themes";
|
import type { UserThemeHeader } from "@main/themes";
|
||||||
import { IpcEvents } from "@shared/IpcEvents";
|
import { IpcEvents } from "@shared/IpcEvents";
|
||||||
|
@ -73,5 +74,17 @@ export default {
|
||||||
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url)
|
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
csp: {
|
||||||
|
/**
|
||||||
|
* Note: Only supports full explicit matches, not wildcards.
|
||||||
|
*
|
||||||
|
* If `*.example.com` is allowed, `isDomainAllowed("https://sub.example.com")` will return false.
|
||||||
|
*/
|
||||||
|
isDomainAllowed: (url: string, directives: string[]) => invoke<boolean>(IpcEvents.CSP_IS_DOMAIN_ALLOWED, url, directives),
|
||||||
|
removeOverride: (url: string) => invoke<boolean>(IpcEvents.CSP_REMOVE_OVERRIDE, url),
|
||||||
|
requestAddOverride: (url: string, directives: string[], callerName: string) =>
|
||||||
|
invoke<CspRequestResult>(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, url, directives, callerName),
|
||||||
|
},
|
||||||
|
|
||||||
pluginHelpers: PluginHelpers
|
pluginHelpers: PluginHelpers
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { Settings, useSettings } from "@api/Settings";
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||||
import { Grid } from "@components/Grid";
|
import { Grid } from "@components/Grid";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
import { authorizeCloud, checkCloudUrlCsp, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
|
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
|
||||||
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
|
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
|
||||||
|
@ -38,6 +38,8 @@ function validateUrl(url: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function eraseAllData() {
|
async function eraseAllData() {
|
||||||
|
if (!await checkCloudUrlCsp()) return;
|
||||||
|
|
||||||
const res = await fetch(new URL("/v1/", getCloudUrl()), {
|
const res = await fetch(new URL("/v1/", getCloudUrl()), {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: { Authorization: await getCloudAuth() }
|
headers: { Authorization: await getCloudAuth() }
|
||||||
|
|
|
@ -24,15 +24,15 @@ import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIc
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||||
import type { UserThemeHeader } from "@main/themes";
|
import type { UserThemeHeader } from "@main/themes";
|
||||||
import { useCspErrors } from "@utils/cspViolations";
|
import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations";
|
||||||
import { openInviteModal } from "@utils/discord";
|
import { openInviteModal } from "@utils/discord";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { showItemInFolder } from "@utils/native";
|
import { relaunch, showItemInFolder } from "@utils/native";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter, useForceUpdater } from "@utils/react";
|
||||||
import { getStylusWebStoreUrl } from "@utils/web";
|
import { getStylusWebStoreUrl } from "@utils/web";
|
||||||
import { findLazy } from "@webpack";
|
import { findLazy } from "@webpack";
|
||||||
import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
import { Alerts, Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
||||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
@ -365,22 +365,73 @@ function ThemesTab() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CspErrorCard() {
|
export function CspErrorCard() {
|
||||||
|
if (IS_WEB) return null;
|
||||||
|
|
||||||
const errors = useCspErrors();
|
const errors = useCspErrors();
|
||||||
|
const forceUpdate = useForceUpdater();
|
||||||
|
|
||||||
if (!errors.length) return null;
|
if (!errors.length) return null;
|
||||||
|
|
||||||
|
const isImgurHtmlDomain = (url: string) => url.startsWith("https://imgur.com/");
|
||||||
|
|
||||||
|
const allowUrl = async (url: string) => {
|
||||||
|
const { origin: baseUrl, hostname } = new URL(url);
|
||||||
|
|
||||||
|
const result = await VencordNative.csp.requestAddOverride(baseUrl, ["connect-src", "img-src", "style-src", "font-src"], "Vencord Themes");
|
||||||
|
if (result !== "ok") return;
|
||||||
|
|
||||||
|
CspBlockedUrls.forEach(url => {
|
||||||
|
if (new URL(url).hostname === hostname) {
|
||||||
|
CspBlockedUrls.delete(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
forceUpdate();
|
||||||
|
|
||||||
|
Alerts.show({
|
||||||
|
title: "Restart Required",
|
||||||
|
body: "A restart is required to apply this change",
|
||||||
|
confirmText: "Restart now",
|
||||||
|
cancelText: "Later!",
|
||||||
|
onConfirm: relaunch
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasImgurHtmlDomain = errors.some(isImgurHtmlDomain);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorCard className="vc-settings-card">
|
<ErrorCard className="vc-settings-card">
|
||||||
<Forms.FormTitle tag="h5">Blocked Resources</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Blocked Resources</Forms.FormTitle>
|
||||||
<Forms.FormText>Some images, styles, or fonts were blocked because they come from disallowed domains.</Forms.FormText>
|
<Forms.FormText>Some images, styles, or fonts were blocked because they come from disallowed domains.</Forms.FormText>
|
||||||
<Forms.FormText>Make sure that your themes and custom css only load resources from whitelisted websites, such as GitHub, Imgur and Google Fonts.</Forms.FormText>
|
<Forms.FormText>It is highly recommended to move them to GitHub or Imgur. But you may also allow domains if you fully trust them.</Forms.FormText>
|
||||||
|
<Forms.FormText>
|
||||||
|
After allowing a domain, you have to fully close (from tray / task manager) and restart {IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} to apply the change.
|
||||||
|
</Forms.FormText>
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.top16, Margins.bottom8)}>Blocked URLs</Forms.FormTitle>
|
<Forms.FormTitle tag="h5" className={classes(Margins.top16, Margins.bottom8)}>Blocked URLs</Forms.FormTitle>
|
||||||
<Flex flexDirection="column" style={{ gap: "0.25em" }}>
|
<div className="vc-settings-csp-list">
|
||||||
{errors.map(url => (
|
{errors.map((url, i) => (
|
||||||
<Link href={url} key={url}>{url}</Link>
|
<div key={url}>
|
||||||
|
{i !== 0 && <Forms.FormDivider className={Margins.bottom8} />}
|
||||||
|
<div className="vc-settings-csp-row">
|
||||||
|
<Link href={url}>{url}</Link>
|
||||||
|
<Button color={Button.Colors.PRIMARY} onClick={() => allowUrl(url)} disabled={isImgurHtmlDomain(url)}>
|
||||||
|
Allow
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</div>
|
||||||
|
|
||||||
|
{hasImgurHtmlDomain && (
|
||||||
|
<>
|
||||||
|
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom16)} />
|
||||||
|
<Forms.FormText>
|
||||||
|
Imgur links should be direct links in the form of <code>https://i.imgur.com/...</code>
|
||||||
|
</Forms.FormText>
|
||||||
|
<Forms.FormText>To obtain a direct link, right-click the image and select "Copy image address".</Forms.FormText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ErrorCard>
|
</ErrorCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,3 +27,26 @@
|
||||||
.vc-settings-theme-author::before {
|
.vc-settings-theme-author::before {
|
||||||
content: "by ";
|
content: "by ";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vc-settings-csp-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-settings-csp-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
|
||||||
|
& a {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
--custom-button-button-md-height: 26px;
|
||||||
|
}
|
||||||
|
|
|
@ -4,59 +4,63 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { NativeSettings } from "@main/settings";
|
||||||
import { session } from "electron";
|
import { session } from "electron";
|
||||||
|
|
||||||
type PolicyMap = Record<string, string[]>;
|
type PolicyMap = Record<string, string[]>;
|
||||||
|
|
||||||
export const ConnectSrc = ["connect-src"];
|
export const ConnectSrc = ["connect-src"];
|
||||||
export const MediaSrc = [...ConnectSrc, "img-src", "media-src"];
|
export const ImageSrc = [...ConnectSrc, "img-src"];
|
||||||
export const CssSrc = ["style-src", "font-src"];
|
export const CssSrc = ["style-src", "font-src"];
|
||||||
export const MediaAndCssSrc = [...MediaSrc, ...CssSrc];
|
export const ImageAndCssSrc = [...ImageSrc, ...CssSrc];
|
||||||
export const MediaScriptsAndCssSrc = [...MediaAndCssSrc, "script-src", "worker-src"];
|
export const ImageScriptsAndCssSrc = [...ImageAndCssSrc, "script-src", "worker-src"];
|
||||||
|
|
||||||
// Plugins can whitelist their own domains by importing this object in their native.ts
|
// Plugins can whitelist their own domains by importing this object in their native.ts
|
||||||
// script and just adding to it. But generally, you should just edit this file instead
|
// script and just adding to it. But generally, you should just edit this file instead
|
||||||
|
|
||||||
export const CspPolicies: PolicyMap = {
|
export const CspPolicies: PolicyMap = {
|
||||||
"*.github.io": MediaAndCssSrc, // GitHub pages, used by most themes
|
"localhost": ImageAndCssSrc,
|
||||||
"github.com": MediaAndCssSrc, // GitHub content (stuff uploaded to markdown forms), used by most themes
|
"127.0.0.1": ImageAndCssSrc,
|
||||||
"raw.githubusercontent.com": MediaAndCssSrc, // GitHub raw, used by some themes
|
|
||||||
"*.gitlab.io": MediaAndCssSrc, // GitLab pages, used by some themes
|
|
||||||
"gitlab.com": MediaAndCssSrc, // GitLab raw, used by some themes
|
|
||||||
"*.codeberg.page": MediaAndCssSrc, // Codeberg pages, used by some themes
|
|
||||||
"codeberg.org": MediaAndCssSrc, // Codeberg raw, used by some themes
|
|
||||||
|
|
||||||
"*.githack.com": MediaAndCssSrc, // githack (namely raw.githack.com), used by some themes
|
"*.github.io": ImageAndCssSrc, // GitHub pages, used by most themes
|
||||||
"jsdelivr.net": MediaAndCssSrc, // jsDelivr, used by very few themes
|
"github.com": ImageAndCssSrc, // GitHub content (stuff uploaded to markdown forms), used by most themes
|
||||||
|
"raw.githubusercontent.com": ImageAndCssSrc, // GitHub raw, used by some themes
|
||||||
|
"*.gitlab.io": ImageAndCssSrc, // GitLab pages, used by some themes
|
||||||
|
"gitlab.com": ImageAndCssSrc, // GitLab raw, used by some themes
|
||||||
|
"*.codeberg.page": ImageAndCssSrc, // Codeberg pages, used by some themes
|
||||||
|
"codeberg.org": ImageAndCssSrc, // Codeberg raw, used by some themes
|
||||||
|
|
||||||
|
"*.githack.com": ImageAndCssSrc, // githack (namely raw.githack.com), used by some themes
|
||||||
|
"jsdelivr.net": ImageAndCssSrc, // jsDelivr, used by very few themes
|
||||||
|
|
||||||
"fonts.googleapis.com": CssSrc, // Google Fonts, used by many themes
|
"fonts.googleapis.com": CssSrc, // Google Fonts, used by many themes
|
||||||
|
|
||||||
"i.imgur.com": MediaSrc, // Imgur, used by some themes
|
"i.imgur.com": ImageSrc, // Imgur, used by some themes
|
||||||
"i.ibb.co": MediaSrc, // ImgBB, used by some themes
|
"i.ibb.co": ImageSrc, // ImgBB, used by some themes
|
||||||
"i.pinimg.com": MediaSrc, // Pinterest, used by some themes
|
"i.pinimg.com": ImageSrc, // Pinterest, used by some themes
|
||||||
"*.tenor.com": MediaSrc, // Tenor, used by some themes
|
"*.tenor.com": ImageSrc, // Tenor, used by some themes
|
||||||
"files.catbox.moe": MediaSrc, // Catbox, used by some themes
|
"files.catbox.moe": ImageAndCssSrc, // Catbox, used by some themes
|
||||||
|
|
||||||
"cdn.discordapp.com": MediaAndCssSrc, // Discord CDN, used by Vencord and some themes to load media
|
"cdn.discordapp.com": ImageAndCssSrc, // Discord CDN, used by Vencord and some themes to load media
|
||||||
"media.discordapp.net": MediaSrc, // Discord media CDN, possible alternative to Discord CDN
|
"media.discordapp.net": ImageSrc, // Discord media CDN, possible alternative to Discord CDN
|
||||||
|
|
||||||
// CDNs used for some things by Vencord.
|
// CDNs used for some things by Vencord.
|
||||||
// FIXME: we really should not be using CDNs anymore
|
// FIXME: we really should not be using CDNs anymore
|
||||||
"cdnjs.cloudflare.com": MediaScriptsAndCssSrc,
|
"cdnjs.cloudflare.com": ImageScriptsAndCssSrc,
|
||||||
"cdn.jsdelivr.net": MediaScriptsAndCssSrc,
|
"cdn.jsdelivr.net": ImageScriptsAndCssSrc,
|
||||||
|
|
||||||
// Function Specific
|
// Function Specific
|
||||||
"api.github.com": ConnectSrc, // used for updating Vencord itself
|
"api.github.com": ConnectSrc, // used for updating Vencord itself
|
||||||
"ws.audioscrobbler.com": ConnectSrc, // Last.fm API
|
"ws.audioscrobbler.com": ConnectSrc, // Last.fm API
|
||||||
"translate-pa.googleapis.com": ConnectSrc, // Google Translate API
|
"translate-pa.googleapis.com": ConnectSrc, // Google Translate API
|
||||||
"*.vencord.dev": MediaSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev)
|
"*.vencord.dev": ImageSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev)
|
||||||
"manti.vendicated.dev": MediaSrc, // ReviewDB API
|
"manti.vendicated.dev": ImageSrc, // ReviewDB API
|
||||||
"decor.fieryflames.dev": ConnectSrc, // Decor API
|
"decor.fieryflames.dev": ConnectSrc, // Decor API
|
||||||
"ugc.decor.fieryflames.dev": MediaSrc, // Decor CDN
|
"ugc.decor.fieryflames.dev": ImageSrc, // Decor CDN
|
||||||
"sponsor.ajay.app": ConnectSrc, // Dearrow API
|
"sponsor.ajay.app": ConnectSrc, // Dearrow API
|
||||||
"dearrow-thumb.ajay.app": MediaSrc, // Dearrow Thumbnail CDN
|
"dearrow-thumb.ajay.app": ImageSrc, // Dearrow Thumbnail CDN
|
||||||
"usrbg.is-hardly.online": MediaSrc, // USRBG API
|
"usrbg.is-hardly.online": ImageSrc, // USRBG API
|
||||||
"icons.duckduckgo.com": MediaSrc, // DuckDuckGo Favicon API (Reverse Image Search)
|
"icons.duckduckgo.com": ImageSrc, // DuckDuckGo Favicon API (Reverse Image Search)
|
||||||
};
|
};
|
||||||
|
|
||||||
const findHeader = (headers: PolicyMap, headerName: Lowercase<string>) => {
|
const findHeader = (headers: PolicyMap, headerName: Lowercase<string>) => {
|
||||||
|
@ -107,6 +111,12 @@ const patchCsp = (headers: PolicyMap) => {
|
||||||
pushDirective(directive, "blob:", "data:", "vencord:");
|
pushDirective(directive, "blob:", "data:", "vencord:");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [host, directives] of Object.entries(NativeSettings.store.customCspRules)) {
|
||||||
|
for (const directive of directives) {
|
||||||
|
pushDirective(directive, host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [host, directives] of Object.entries(CspPolicies)) {
|
for (const [host, directives] of Object.entries(CspPolicies)) {
|
||||||
for (const directive of directives) {
|
for (const directive of directives) {
|
||||||
pushDirective(directive, host);
|
pushDirective(directive, host);
|
125
src/main/csp/manager.ts
Normal file
125
src/main/csp/manager.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2025 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NativeSettings } from "@main/settings";
|
||||||
|
import { IpcEvents } from "@shared/IpcEvents";
|
||||||
|
import { dialog, ipcMain, IpcMainInvokeEvent } from "electron";
|
||||||
|
|
||||||
|
import { CspPolicies, ImageAndCssSrc } from ".";
|
||||||
|
|
||||||
|
export type CspRequestResult = "invalid" | "cancelled" | "unchecked" | "ok" | "conflict";
|
||||||
|
|
||||||
|
export function registerCspIpcHandlers() {
|
||||||
|
ipcMain.handle(IpcEvents.CSP_REMOVE_OVERRIDE, removeCspRule);
|
||||||
|
ipcMain.handle(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, addCspRule);
|
||||||
|
ipcMain.handle(IpcEvents.CSP_IS_DOMAIN_ALLOWED, isDomainAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(url: string, directives: string[]) {
|
||||||
|
try {
|
||||||
|
const { hostname } = new URL(url);
|
||||||
|
|
||||||
|
if (/[;'"\\]/.test(hostname)) return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directives.length === 0) return false;
|
||||||
|
if (directives.some(d => !ImageAndCssSrc.includes(d))) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessage(url: string, directives: string[], callerName: string) {
|
||||||
|
const domain = new URL(url).hostname;
|
||||||
|
|
||||||
|
const message = `${callerName} wants to allow connections to ${domain}`;
|
||||||
|
|
||||||
|
let detail =
|
||||||
|
`Unless you recognise and fully trust ${domain}, you should cancel this request!\n\n` +
|
||||||
|
`You will have to fully close and restart ${IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} for the changes to take effect.`;
|
||||||
|
|
||||||
|
if (directives.length === 1 && directives[0] === "connect-src") {
|
||||||
|
return { message, detail };
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTypes = directives
|
||||||
|
.filter(type => type !== "connect-src")
|
||||||
|
.map(type => {
|
||||||
|
switch (type) {
|
||||||
|
case "img-src":
|
||||||
|
return "Images";
|
||||||
|
case "style-src":
|
||||||
|
return "CSS & Themes";
|
||||||
|
case "font-src":
|
||||||
|
return "Fonts";
|
||||||
|
default:
|
||||||
|
throw new Error(`Illegal CSP directive: ${type}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
detail = `The following types of content will be allowed to load from ${domain}:\n${contentTypes}\n\n${detail}`;
|
||||||
|
|
||||||
|
return { message, detail };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addCspRule(_: IpcMainInvokeEvent, url: string, directives: string[], callerName: string): Promise<CspRequestResult> {
|
||||||
|
if (!validate(url, directives)) {
|
||||||
|
return "invalid";
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = new URL(url).hostname;
|
||||||
|
|
||||||
|
if (domain in NativeSettings.store.customCspRules) {
|
||||||
|
return "conflict";
|
||||||
|
}
|
||||||
|
|
||||||
|
const { checkboxChecked, response } = await dialog.showMessageBox({
|
||||||
|
...getMessage(url, directives, callerName),
|
||||||
|
type: callerName ? "info" : "warning",
|
||||||
|
title: "Vencord Host Permissions",
|
||||||
|
buttons: ["Cancel", "Allow"],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 0,
|
||||||
|
checkboxLabel: `I fully trust ${domain} and understand the risks of allowing connections to it.`,
|
||||||
|
checkboxChecked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response !== 1) {
|
||||||
|
return "cancelled";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkboxChecked) {
|
||||||
|
return "unchecked";
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeSettings.store.customCspRules[domain] = directives;
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCspRule(_: IpcMainInvokeEvent, domain: string) {
|
||||||
|
if (domain in NativeSettings.store.customCspRules) {
|
||||||
|
delete NativeSettings.store.customCspRules[domain];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDomainAllowed(_: IpcMainInvokeEvent, url: string, directives: string[]) {
|
||||||
|
try {
|
||||||
|
const domain = new URL(url).hostname;
|
||||||
|
|
||||||
|
const ruleForDomain = CspPolicies[domain] ?? NativeSettings.store.customCspRules[domain];
|
||||||
|
if (!ruleForDomain) return false;
|
||||||
|
|
||||||
|
return directives.every(d => ruleForDomain.includes(d));
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,12 +28,15 @@ import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
|
||||||
import { open, readdir, readFile } from "fs/promises";
|
import { open, readdir, readFile } from "fs/promises";
|
||||||
import { join, normalize } from "path";
|
import { join, normalize } from "path";
|
||||||
|
|
||||||
|
import { registerCspIpcHandlers } from "./csp/manager";
|
||||||
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
||||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
|
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
|
||||||
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
||||||
|
|
||||||
mkdirSync(THEMES_DIR, { recursive: true });
|
mkdirSync(THEMES_DIR, { recursive: true });
|
||||||
|
|
||||||
|
registerCspIpcHandlers();
|
||||||
|
|
||||||
export function ensureSafePath(basePath: string, path: string) {
|
export function ensureSafePath(basePath: string, path: string) {
|
||||||
const normalizedBasePath = normalize(basePath + "/");
|
const normalizedBasePath = normalize(basePath + "/");
|
||||||
const newPath = join(basePath, path);
|
const newPath = join(basePath, path);
|
||||||
|
|
|
@ -49,16 +49,18 @@ export interface NativeSettings {
|
||||||
[setting: string]: any;
|
[setting: string]: any;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
customCspRules: Record<string, string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultNativeSettings: NativeSettings = {
|
const DefaultNativeSettings: NativeSettings = {
|
||||||
plugins: {}
|
plugins: {},
|
||||||
|
customCspRules: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE);
|
const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE);
|
||||||
mergeDefaults(nativeSettings, DefaultNativeSettings);
|
mergeDefaults(nativeSettings, DefaultNativeSettings);
|
||||||
|
|
||||||
export const NativeSettings = new SettingsStore(nativeSettings);
|
export const NativeSettings = new SettingsStore(nativeSettings as NativeSettings);
|
||||||
|
|
||||||
NativeSettings.addGlobalChangeListener(() => {
|
NativeSettings.addGlobalChangeListener(() => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -42,4 +42,8 @@ export const enum IpcEvents {
|
||||||
|
|
||||||
OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect",
|
OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect",
|
||||||
VOICE_MESSAGES_READ_RECORDING = "VencordVMReadRecording",
|
VOICE_MESSAGES_READ_RECORDING = "VencordVMReadRecording",
|
||||||
|
|
||||||
|
CSP_IS_DOMAIN_ALLOWED = "VencordCspIsDomainAllowed",
|
||||||
|
CSP_REMOVE_OVERRIDE = "VencordCspRemoveOverride",
|
||||||
|
CSP_REQUEST_ADD_OVERRIDE = "VencordCspRequestAddOverride",
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,15 +19,40 @@
|
||||||
import * as DataStore from "@api/DataStore";
|
import * as DataStore from "@api/DataStore";
|
||||||
import { showNotification } from "@api/Notifications";
|
import { showNotification } from "@api/Notifications";
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import { OAuth2AuthorizeModal, UserStore } from "@webpack/common";
|
import { Alerts, OAuth2AuthorizeModal, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import { Logger } from "./Logger";
|
import { Logger } from "./Logger";
|
||||||
import { openModal } from "./modal";
|
import { openModal } from "./modal";
|
||||||
|
import { relaunch } from "./native";
|
||||||
|
|
||||||
export const cloudLogger = new Logger("Cloud", "#39b7e0");
|
export const cloudLogger = new Logger("Cloud", "#39b7e0");
|
||||||
export const getCloudUrl = () => new URL(Settings.cloud.url);
|
|
||||||
|
|
||||||
const cloudUrlOrigin = () => getCloudUrl().origin;
|
export const getCloudUrl = () => new URL(Settings.cloud.url);
|
||||||
|
const getCloudUrlOrigin = () => getCloudUrl().origin;
|
||||||
|
|
||||||
|
export async function checkCloudUrlCsp() {
|
||||||
|
if (IS_WEB) return true;
|
||||||
|
|
||||||
|
const { host } = getCloudUrl();
|
||||||
|
if (host === "api.vencord.dev") return true;
|
||||||
|
|
||||||
|
if (await VencordNative.csp.isDomainAllowed(Settings.cloud.url, ["connect-src"])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await VencordNative.csp.requestAddOverride(Settings.cloud.url, ["connect-src"], "Cloud Sync");
|
||||||
|
if (res === "ok") {
|
||||||
|
Alerts.show({
|
||||||
|
title: "Cloud Integration enabled",
|
||||||
|
body: `${host} has been added to the whitelist. Please restart the app for the changes to take effect.`,
|
||||||
|
confirmText: "Restart now",
|
||||||
|
cancelText: "Later!",
|
||||||
|
onConfirm: relaunch
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const getUserId = () => {
|
const getUserId = () => {
|
||||||
const id = UserStore.getCurrentUser()?.id;
|
const id = UserStore.getCurrentUser()?.id;
|
||||||
if (!id) throw new Error("User not yet logged in");
|
if (!id) throw new Error("User not yet logged in");
|
||||||
|
@ -37,7 +62,7 @@ const getUserId = () => {
|
||||||
export async function getAuthorization() {
|
export async function getAuthorization() {
|
||||||
const secrets = await DataStore.get<Record<string, string>>("Vencord_cloudSecret") ?? {};
|
const secrets = await DataStore.get<Record<string, string>>("Vencord_cloudSecret") ?? {};
|
||||||
|
|
||||||
const origin = cloudUrlOrigin();
|
const origin = getCloudUrlOrigin();
|
||||||
|
|
||||||
// we need to migrate from the old format here
|
// we need to migrate from the old format here
|
||||||
if (secrets[origin]) {
|
if (secrets[origin]) {
|
||||||
|
@ -59,7 +84,7 @@ export async function getAuthorization() {
|
||||||
async function setAuthorization(secret: string) {
|
async function setAuthorization(secret: string) {
|
||||||
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
|
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
|
||||||
secrets ??= {};
|
secrets ??= {};
|
||||||
secrets[`${cloudUrlOrigin()}:${getUserId()}`] = secret;
|
secrets[`${getCloudUrlOrigin()}:${getUserId()}`] = secret;
|
||||||
return secrets;
|
return secrets;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -67,7 +92,7 @@ async function setAuthorization(secret: string) {
|
||||||
export async function deauthorizeCloud() {
|
export async function deauthorizeCloud() {
|
||||||
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
|
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
|
||||||
secrets ??= {};
|
secrets ??= {};
|
||||||
delete secrets[`${cloudUrlOrigin()}:${getUserId()}`];
|
delete secrets[`${getCloudUrlOrigin()}:${getUserId()}`];
|
||||||
return secrets;
|
return secrets;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -78,6 +103,8 @@ export async function authorizeCloud() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!await checkCloudUrlCsp()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const oauthConfiguration = await fetch(new URL("/v1/oauth/settings", getCloudUrl()));
|
const oauthConfiguration = await fetch(new URL("/v1/oauth/settings", getCloudUrl()));
|
||||||
var { clientId, redirectUri } = await oauthConfiguration.json();
|
var { clientId, redirectUri } = await oauthConfiguration.json();
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { PlainSettings, Settings } from "@api/Settings";
|
||||||
import { moment, Toasts } from "@webpack/common";
|
import { moment, Toasts } from "@webpack/common";
|
||||||
import { deflateSync, inflateSync } from "fflate";
|
import { deflateSync, inflateSync } from "fflate";
|
||||||
|
|
||||||
import { getCloudAuth, getCloudUrl } from "./cloud";
|
import { checkCloudUrlCsp, getCloudAuth, getCloudUrl } from "./cloud";
|
||||||
import { Logger } from "./Logger";
|
import { Logger } from "./Logger";
|
||||||
import { relaunch } from "./native";
|
import { relaunch } from "./native";
|
||||||
import { chooseFile, saveFile } from "./web";
|
import { chooseFile, saveFile } from "./web";
|
||||||
|
@ -115,6 +115,8 @@ const cloudSettingsLogger = new Logger("Cloud:Settings", "#39b7e0");
|
||||||
export async function putCloudSettings(manual?: boolean) {
|
export async function putCloudSettings(manual?: boolean) {
|
||||||
const settings = await exportSettings({ minify: true });
|
const settings = await exportSettings({ minify: true });
|
||||||
|
|
||||||
|
if (!await checkCloudUrlCsp()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
@ -159,6 +161,8 @@ export async function putCloudSettings(manual?: boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCloudSettings(shouldNotify = true, force = false) {
|
export async function getCloudSettings(shouldNotify = true, force = false) {
|
||||||
|
if (!await checkCloudUrlCsp()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -248,6 +252,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteCloudSettings() {
|
export async function deleteCloudSettings() {
|
||||||
|
if (!await checkCloudUrlCsp()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue