From 37de676069cb7052fafdc349fb50e90f185edc19 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 5 Nov 2024 15:44:54 +0200 Subject: [PATCH] tighten up csp --- electron/src/index.ts | 2 +- electron/src/preload.ts | 9 +- electron/src/setup.ts | 103 +++++++++- src/ExtStates/NotAuthenticated.tsx | 298 +++++++++++++++-------------- 4 files changed, 259 insertions(+), 153 deletions(-) diff --git a/electron/src/index.ts b/electron/src/index.ts index 759f662..338f553 100644 --- a/electron/src/index.ts +++ b/electron/src/index.ts @@ -23,7 +23,7 @@ const capacitorFileConfig: CapacitorElectronConfig = getCapacitorElectronConfig( // Initialize our app. You can pass menu templates into the app here. // const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig); -const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig, trayMenuTemplate, appMenuBarMenuTemplate); +export const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig, trayMenuTemplate, appMenuBarMenuTemplate); // If deeplinking is enabled then we will set it up here. if (capacitorFileConfig.electron?.deepLinkingEnabled) { diff --git a/electron/src/preload.ts b/electron/src/preload.ts index 9911817..f3e42a9 100644 --- a/electron/src/preload.ts +++ b/electron/src/preload.ts @@ -5,11 +5,16 @@ console.log('User Preload!'); const { contextBridge, shell, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electronAPI', { - openExternal: (url) => shell.openExternal(url) + openExternal: (url) => shell.openExternal(url), + setAllowedDomains: (domains) => { + ipcRenderer.send('set-allowed-domains', domains); + }, }); contextBridge.exposeInMainWorld('electron', { onUpdateAvailable: (callback) => ipcRenderer.on('update_available', callback), onUpdateDownloaded: (callback) => ipcRenderer.on('update_downloaded', callback), restartApp: () => ipcRenderer.send('restart_app') -}); \ No newline at end of file +}); + +ipcRenderer.send('test-ipc'); \ No newline at end of file diff --git a/electron/src/setup.ts b/electron/src/setup.ts index 3f267b8..b757b96 100644 --- a/electron/src/setup.ts +++ b/electron/src/setup.ts @@ -6,13 +6,34 @@ import { } from '@capacitor-community/electron'; import chokidar from 'chokidar'; import type { MenuItemConstructorOptions } from 'electron'; -import { app, BrowserWindow, Menu, MenuItem, nativeImage, Tray, session } from 'electron'; +import { app, BrowserWindow, Menu, MenuItem, nativeImage, Tray, session, ipcMain } from 'electron'; import electronIsDev from 'electron-is-dev'; import electronServe from 'electron-serve'; import windowStateKeeper from 'electron-window-state'; import { join } from 'path'; +import { myCapacitorApp } from '.'; +const defaultDomains = [ + 'http://127.0.0.1:12391', + 'ws://127.0.0.1:12391', + 'https://ext-node.qortal.link', + 'wss://ext-node.qortal.link', + 'https://appnode.qortal.org', + "https://api.qortal.org", + "https://api2.qortal.org", + "https://appnode.qortal.org", + "https://apinode.qortalnodes.live", + "https://apinode1.qortalnodes.live", + "https://apinode2.qortalnodes.live", + "https://apinode3.qortalnodes.live", + "https://apinode4.qortalnodes.live" + +]; +// let allowedDomains: string[] = [...defaultDomains] +const domainHolder = { + allowedDomains: [...defaultDomains], +}; // Define components for a watcher to detect when the webapp is changed so we can reload in Dev mode. const reloadWatcher = { debouncer: null, @@ -220,15 +241,79 @@ export class ElectronCapacitorApp { } // Set a CSP up for our application based on the custom scheme +// export function setupContentSecurityPolicy(customScheme: string): void { +// session.defaultSession.webRequest.onHeadersReceived((details, callback) => { +// callback({ +// responseHeaders: { +// ...details.responseHeaders, +// 'Content-Security-Policy': [ +// "script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval'; object-src 'self'; connect-src 'self' https://*:* http://*:* wss://*:* ws://*:*", +// ], +// }, +// }); +// }); +// } + + export function setupContentSecurityPolicy(customScheme: string): void { session.defaultSession.webRequest.onHeadersReceived((details, callback) => { - callback({ - responseHeaders: { - ...details.responseHeaders, - 'Content-Security-Policy': [ - "script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval'; object-src 'self'; connect-src 'self' https://*:* http://*:* wss://*:* ws://*:*", - ], - }, + const allowedSources = ["'self'", ...domainHolder.allowedDomains].join(' '); + const csp = ` + script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' ${allowedSources}; + object-src 'self'; + connect-src ${allowedSources}; + `.replace(/\s+/g, ' ').trim(); + + callback({ + responseHeaders: { + ...details.responseHeaders, + 'Content-Security-Policy': [csp], + }, + }); }); - }); + } + + + +// IPC listener for updating allowed domains +ipcMain.on('set-allowed-domains', (event, domains: string[]) => { + + // Validate and transform user-provided domains + const validatedUserDomains = domains + .flatMap((domain) => { + try { + const url = new URL(domain); + const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; + const socketUrl = `${protocol}//${url.hostname}${url.port ? ':' + url.port : ''}`; + return [url.origin, socketUrl]; + } catch { + return []; + } + }) + .filter(Boolean) as string[]; + + // Combine default and validated user domains + const newAllowedDomains = [...new Set([...defaultDomains, ...validatedUserDomains])]; + + // Sort both current allowed domains and new domains for comparison + const sortedCurrentDomains = [...domainHolder.allowedDomains].sort(); + const sortedNewDomains = [...newAllowedDomains].sort(); + + // Check if the lists are different + const hasChanged = + sortedCurrentDomains.length !== sortedNewDomains.length || + sortedCurrentDomains.some((domain, index) => domain !== sortedNewDomains[index]); + + // If there's a change, update allowedDomains and reload the window + if (hasChanged) { + domainHolder.allowedDomains = newAllowedDomains; + + const mainWindow = myCapacitorApp.getMainWindow(); + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.reload(); + } + } +}); + + diff --git a/src/ExtStates/NotAuthenticated.tsx b/src/ExtStates/NotAuthenticated.tsx index e92357d..cd5f8d8 100644 --- a/src/ExtStates/NotAuthenticated.tsx +++ b/src/ExtStates/NotAuthenticated.tsx @@ -23,15 +23,14 @@ import { set } from "lodash"; import { cleanUrl, isUsingLocal } from "../background"; const manifestData = { - version: '0.2.0' -} + version: "0.2.0", +}; export const NotAuthenticated = ({ getRootProps, getInputProps, setExtstate, - apiKey, setApiKey, globalApiKey, @@ -54,9 +53,9 @@ export const NotAuthenticated = ({ const [customApikey, setCustomApiKey] = React.useState(""); const [customNodeToSaveIndex, setCustomNodeToSaveIndex] = React.useState(null); - const importedApiKeyRef = useRef(null) - const currentNodeRef = useRef(null) - const hasLocalNodeRef = useRef(null) + const importedApiKeyRef = useRef(null); + const currentNodeRef = useRef(null); + const hasLocalNodeRef = useRef(null); const isLocal = cleanUrl(currentNode?.url) === "127.0.0.1:12391"; const handleFileChangeApiKey = (event) => { const file = event.target.files[0]; // Get the selected file @@ -71,7 +70,6 @@ export const NotAuthenticated = ({ } }; - const checkIfUserHasLocalNode = useCallback(async () => { try { const url = `http://127.0.0.1:12391/admin/status`; @@ -93,43 +91,48 @@ export const NotAuthenticated = ({ }, []); useEffect(() => { - window.sendMessage("getCustomNodesFromStorage") - .then((response) => { - if (response) { - setCustomNodes(response || []); - } - }) - .catch((error) => { - console.error("Failed to get custom nodes from storage:", error.message || "An error occurred"); - }); - + window + .sendMessage("getCustomNodesFromStorage") + .then((response) => { + if (response) { + setCustomNodes(response || []); + window.electronAPI.setAllowedDomains(response?.map((node)=> node.url)) + + } + }) + .catch((error) => { + console.error( + "Failed to get custom nodes from storage:", + error.message || "An error occurred" + ); + }); }, []); - useEffect(()=> { - importedApiKeyRef.current = importedApiKey - }, [importedApiKey]) - useEffect(()=> { - currentNodeRef.current = currentNode - }, [currentNode]) + useEffect(() => { + importedApiKeyRef.current = importedApiKey; + }, [importedApiKey]); + useEffect(() => { + currentNodeRef.current = currentNode; + }, [currentNode]); - useEffect(()=> { - hasLocalNodeRef.current = hasLocalNode - }, [hasLocalNode]) + useEffect(() => { + hasLocalNodeRef.current = hasLocalNode; + }, [hasLocalNode]); const validateApiKey = useCallback(async (key, fromStartUp) => { try { - if(!currentNodeRef.current) return - const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391"; - if(isLocalKey && !hasLocalNodeRef.current && !fromStartUp){ - throw new Error('Please turn on your local node') - - } - const isCurrentNodeLocal = cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391"; - if(isLocalKey && !isCurrentNodeLocal) { - setIsValidApiKey(false); - setUseLocalNode(false); - return - } + if (!currentNodeRef.current) return; + const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391"; + if (isLocalKey && !hasLocalNodeRef.current && !fromStartUp) { + throw new Error("Please turn on your local node"); + } + const isCurrentNodeLocal = + cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391"; + if (isLocalKey && !isCurrentNodeLocal) { + setIsValidApiKey(false); + setUseLocalNode(false); + return; + } let payload = {}; if (currentNodeRef.current?.url === "http://127.0.0.1:12391") { @@ -137,7 +140,7 @@ export const NotAuthenticated = ({ apikey: importedApiKeyRef.current || key?.apikey, url: currentNodeRef.current?.url, }; - } else if(currentNodeRef.current) { + } else if (currentNodeRef.current) { payload = currentNodeRef.current; } const url = `${payload?.url}/admin/apikey/test`; @@ -152,21 +155,24 @@ export const NotAuthenticated = ({ // Assuming the response is in plain text and will be 'true' or 'false' const data = await response.text(); if (data === "true") { - window.sendMessage("setApiKey", payload) - .then((response) => { - if (response) { - handleSetGlobalApikey(payload); - setIsValidApiKey(true); - setUseLocalNode(true); - if (!fromStartUp) { - setApiKey(payload); + window + .sendMessage("setApiKey", payload) + .then((response) => { + if (response) { + handleSetGlobalApikey(payload); + setIsValidApiKey(true); + setUseLocalNode(true); + if (!fromStartUp) { + setApiKey(payload); + } } - } - }) - .catch((error) => { - console.error("Failed to set API key:", error.message || "An error occurred"); - }); - + }) + .catch((error) => { + console.error( + "Failed to set API key:", + error.message || "An error occurred" + ); + }); } else { setIsValidApiKey(false); setUseLocalNode(false); @@ -213,24 +219,28 @@ export const NotAuthenticated = ({ } setCustomNodes(nodes); + window.electronAPI.setAllowedDomains(nodes?.map((node)=> node.url)) + setCustomNodeToSaveIndex(null); if (!nodes) return; - window.sendMessage("setCustomNodes", nodes) - .then((response) => { - if (response) { - setMode("list"); - setUrl("http://"); - setCustomApiKey(""); - // add alert if needed - } - }) - .catch((error) => { - console.error("Failed to set custom nodes:", error.message || "An error occurred"); - }); - + window + .sendMessage("setCustomNodes", nodes) + .then((response) => { + if (response) { + setMode("list"); + setUrl("http://"); + setCustomApiKey(""); + // add alert if needed + } + }) + .catch((error) => { + console.error( + "Failed to set custom nodes:", + error.message || "An error occurred" + ); + }); }; - return ( <> @@ -296,16 +306,16 @@ export const NotAuthenticated = ({ }} /> - - - - {"Using node: "} {currentNode?.url} - + + + + {"Using node: "} {currentNode?.url} + <> { + if (response) { + setApiKey(null); + handleSetGlobalApikey(null); + } }) - setUseLocalNode(false) - window.sendMessage("setApiKey", null) - .then((response) => { - if (response) { - setApiKey(null); - handleSetGlobalApikey(null); - } - }) - .catch((error) => { - console.error("Failed to set API key:", error.message || "An error occurred"); - }); - + .catch((error) => { + console.error( + "Failed to set API key:", + error.message || "An error occurred" + ); + }); } - }} disabled={false} defaultChecked /> } - label={`Use ${isLocal ? 'Local' : 'Custom'} Node`} + label={`Use ${isLocal ? "Local" : "Custom"} Node`} /> {currentNode?.url === "http://127.0.0.1:12391" && ( @@ -379,31 +391,33 @@ export const NotAuthenticated = ({ onChange={handleFileChangeApiKey} // File input handler /> - {`api key : ${importedApiKey}`} - - - - + {`api key : ${importedApiKey}`} )} - + - Build version: {manifestData?.version} + + Build version: {manifestData?.version} + - {mode === "list" && ( { - if (response) { - setApiKey(null); - handleSetGlobalApikey(null); - } - }) - .catch((error) => { - console.error("Failed to set API key:", error.message || "An error occurred"); - }); - + window + .sendMessage("setApiKey", null) + .then((response) => { + if (response) { + setApiKey(null); + handleSetGlobalApikey(null); + } + }) + .catch((error) => { + console.error( + "Failed to set API key:", + error.message || "An error occurred" + ); + }); }} variant="contained" > @@ -527,18 +543,21 @@ export const NotAuthenticated = ({ setMode("list"); setShow(false); setIsValidApiKey(false); - setUseLocalNode(false); - window.sendMessage("setApiKey", null) - .then((response) => { - if (response) { - setApiKey(null); - handleSetGlobalApikey(null); - } - }) - .catch((error) => { - console.error("Failed to set API key:", error.message || "An error occurred"); - }); - + setUseLocalNode(false); + window + .sendMessage("setApiKey", null) + .then((response) => { + if (response) { + setApiKey(null); + handleSetGlobalApikey(null); + } + }) + .catch((error) => { + console.error( + "Failed to set API key:", + error.message || "An error occurred" + ); + }); }} variant="contained" > @@ -562,7 +581,6 @@ export const NotAuthenticated = ({ const nodesToSave = [ ...(customNodes || []), ].filter((item) => item?.url !== node?.url); - saveCustomNodes(nodesToSave); }} @@ -601,9 +619,7 @@ export const NotAuthenticated = ({ /> )} - - {mode === "list" && (