tighten up csp

This commit is contained in:
PhilReact 2024-11-05 15:44:54 +02:00
parent 1b8137fe35
commit 37de676069
4 changed files with 259 additions and 153 deletions

View File

@ -23,7 +23,7 @@ const capacitorFileConfig: CapacitorElectronConfig = getCapacitorElectronConfig(
// Initialize our app. You can pass menu templates into the app here. // Initialize our app. You can pass menu templates into the app here.
// const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig); // 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 deeplinking is enabled then we will set it up here.
if (capacitorFileConfig.electron?.deepLinkingEnabled) { if (capacitorFileConfig.electron?.deepLinkingEnabled) {

View File

@ -5,7 +5,10 @@ console.log('User Preload!');
const { contextBridge, shell, ipcRenderer } = require('electron'); const { contextBridge, shell, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
openExternal: (url) => shell.openExternal(url) openExternal: (url) => shell.openExternal(url),
setAllowedDomains: (domains) => {
ipcRenderer.send('set-allowed-domains', domains);
},
}); });
contextBridge.exposeInMainWorld('electron', { contextBridge.exposeInMainWorld('electron', {
@ -13,3 +16,5 @@ contextBridge.exposeInMainWorld('electron', {
onUpdateDownloaded: (callback) => ipcRenderer.on('update_downloaded', callback), onUpdateDownloaded: (callback) => ipcRenderer.on('update_downloaded', callback),
restartApp: () => ipcRenderer.send('restart_app') restartApp: () => ipcRenderer.send('restart_app')
}); });
ipcRenderer.send('test-ipc');

View File

@ -6,13 +6,34 @@ import {
} from '@capacitor-community/electron'; } from '@capacitor-community/electron';
import chokidar from 'chokidar'; import chokidar from 'chokidar';
import type { MenuItemConstructorOptions } from 'electron'; 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 electronIsDev from 'electron-is-dev';
import electronServe from 'electron-serve'; import electronServe from 'electron-serve';
import windowStateKeeper from 'electron-window-state'; import windowStateKeeper from 'electron-window-state';
import { join } from 'path'; 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. // Define components for a watcher to detect when the webapp is changed so we can reload in Dev mode.
const reloadWatcher = { const reloadWatcher = {
debouncer: null, debouncer: null,
@ -220,15 +241,79 @@ export class ElectronCapacitorApp {
} }
// Set a CSP up for our application based on the custom scheme // 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 { export function setupContentSecurityPolicy(customScheme: string): void {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => { session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({ const allowedSources = ["'self'", ...domainHolder.allowedDomains].join(' ');
responseHeaders: { const csp = `
...details.responseHeaders, script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' ${allowedSources};
'Content-Security-Policy': [ object-src 'self';
"script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval'; object-src 'self'; connect-src 'self' https://*:* http://*:* wss://*:* ws://*:*", 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();
}
}
});

View File

@ -23,15 +23,14 @@ import { set } from "lodash";
import { cleanUrl, isUsingLocal } from "../background"; import { cleanUrl, isUsingLocal } from "../background";
const manifestData = { const manifestData = {
version: '0.2.0' version: "0.2.0",
} };
export const NotAuthenticated = ({ export const NotAuthenticated = ({
getRootProps, getRootProps,
getInputProps, getInputProps,
setExtstate, setExtstate,
apiKey, apiKey,
setApiKey, setApiKey,
globalApiKey, globalApiKey,
@ -54,9 +53,9 @@ export const NotAuthenticated = ({
const [customApikey, setCustomApiKey] = React.useState(""); const [customApikey, setCustomApiKey] = React.useState("");
const [customNodeToSaveIndex, setCustomNodeToSaveIndex] = const [customNodeToSaveIndex, setCustomNodeToSaveIndex] =
React.useState(null); React.useState(null);
const importedApiKeyRef = useRef(null) const importedApiKeyRef = useRef(null);
const currentNodeRef = useRef(null) const currentNodeRef = useRef(null);
const hasLocalNodeRef = useRef(null) const hasLocalNodeRef = useRef(null);
const isLocal = cleanUrl(currentNode?.url) === "127.0.0.1:12391"; const isLocal = cleanUrl(currentNode?.url) === "127.0.0.1:12391";
const handleFileChangeApiKey = (event) => { const handleFileChangeApiKey = (event) => {
const file = event.target.files[0]; // Get the selected file const file = event.target.files[0]; // Get the selected file
@ -71,7 +70,6 @@ export const NotAuthenticated = ({
} }
}; };
const checkIfUserHasLocalNode = useCallback(async () => { const checkIfUserHasLocalNode = useCallback(async () => {
try { try {
const url = `http://127.0.0.1:12391/admin/status`; const url = `http://127.0.0.1:12391/admin/status`;
@ -93,43 +91,48 @@ export const NotAuthenticated = ({
}, []); }, []);
useEffect(() => { useEffect(() => {
window.sendMessage("getCustomNodesFromStorage") window
.then((response) => { .sendMessage("getCustomNodesFromStorage")
if (response) { .then((response) => {
setCustomNodes(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");
});
}
})
.catch((error) => {
console.error(
"Failed to get custom nodes from storage:",
error.message || "An error occurred"
);
});
}, []); }, []);
useEffect(()=> { useEffect(() => {
importedApiKeyRef.current = importedApiKey importedApiKeyRef.current = importedApiKey;
}, [importedApiKey]) }, [importedApiKey]);
useEffect(()=> { useEffect(() => {
currentNodeRef.current = currentNode currentNodeRef.current = currentNode;
}, [currentNode]) }, [currentNode]);
useEffect(()=> { useEffect(() => {
hasLocalNodeRef.current = hasLocalNode hasLocalNodeRef.current = hasLocalNode;
}, [hasLocalNode]) }, [hasLocalNode]);
const validateApiKey = useCallback(async (key, fromStartUp) => { const validateApiKey = useCallback(async (key, fromStartUp) => {
try { try {
if(!currentNodeRef.current) return if (!currentNodeRef.current) return;
const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391"; const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391";
if(isLocalKey && !hasLocalNodeRef.current && !fromStartUp){ if (isLocalKey && !hasLocalNodeRef.current && !fromStartUp) {
throw new Error('Please turn on your local node') throw new Error("Please turn on your local node");
}
} const isCurrentNodeLocal =
const isCurrentNodeLocal = cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391"; cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391";
if(isLocalKey && !isCurrentNodeLocal) { if (isLocalKey && !isCurrentNodeLocal) {
setIsValidApiKey(false); setIsValidApiKey(false);
setUseLocalNode(false); setUseLocalNode(false);
return return;
} }
let payload = {}; let payload = {};
if (currentNodeRef.current?.url === "http://127.0.0.1:12391") { if (currentNodeRef.current?.url === "http://127.0.0.1:12391") {
@ -137,7 +140,7 @@ export const NotAuthenticated = ({
apikey: importedApiKeyRef.current || key?.apikey, apikey: importedApiKeyRef.current || key?.apikey,
url: currentNodeRef.current?.url, url: currentNodeRef.current?.url,
}; };
} else if(currentNodeRef.current) { } else if (currentNodeRef.current) {
payload = currentNodeRef.current; payload = currentNodeRef.current;
} }
const url = `${payload?.url}/admin/apikey/test`; 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' // Assuming the response is in plain text and will be 'true' or 'false'
const data = await response.text(); const data = await response.text();
if (data === "true") { if (data === "true") {
window.sendMessage("setApiKey", payload) window
.then((response) => { .sendMessage("setApiKey", payload)
if (response) { .then((response) => {
handleSetGlobalApikey(payload); if (response) {
setIsValidApiKey(true); handleSetGlobalApikey(payload);
setUseLocalNode(true); setIsValidApiKey(true);
if (!fromStartUp) { setUseLocalNode(true);
setApiKey(payload); if (!fromStartUp) {
setApiKey(payload);
}
} }
} })
}) .catch((error) => {
.catch((error) => { console.error(
console.error("Failed to set API key:", error.message || "An error occurred"); "Failed to set API key:",
}); error.message || "An error occurred"
);
});
} else { } else {
setIsValidApiKey(false); setIsValidApiKey(false);
setUseLocalNode(false); setUseLocalNode(false);
@ -213,24 +219,28 @@ export const NotAuthenticated = ({
} }
setCustomNodes(nodes); setCustomNodes(nodes);
window.electronAPI.setAllowedDomains(nodes?.map((node)=> node.url))
setCustomNodeToSaveIndex(null); setCustomNodeToSaveIndex(null);
if (!nodes) return; if (!nodes) return;
window.sendMessage("setCustomNodes", nodes) window
.then((response) => { .sendMessage("setCustomNodes", nodes)
if (response) { .then((response) => {
setMode("list"); if (response) {
setUrl("http://"); setMode("list");
setCustomApiKey(""); setUrl("http://");
// add alert if needed setCustomApiKey("");
} // add alert if needed
}) }
.catch((error) => { })
console.error("Failed to set custom nodes:", error.message || "An error occurred"); .catch((error) => {
}); console.error(
"Failed to set custom nodes:",
error.message || "An error occurred"
);
});
}; };
return ( return (
<> <>
<Spacer height="35px" /> <Spacer height="35px" />
@ -296,16 +306,16 @@ export const NotAuthenticated = ({
}} }}
/> />
</Box> </Box>
<Spacer height="15px" /> <Spacer height="15px" />
<Typography <Typography
sx={{ sx={{
fontSize: "12px", fontSize: "12px",
visibility: !useLocalNode && 'hidden' visibility: !useLocalNode && "hidden",
}} }}
> >
{"Using node: "} {currentNode?.url} {"Using node: "} {currentNode?.url}
</Typography> </Typography>
<> <>
<Spacer height="15px" /> <Spacer height="15px" />
<Box <Box
@ -344,28 +354,30 @@ export const NotAuthenticated = ({
validateApiKey(currentNode); validateApiKey(currentNode);
} else { } else {
setCurrentNode({ setCurrentNode({
url: "http://127.0.0.1:12391", url: "http://127.0.0.1:12391",
});
setUseLocalNode(false);
window
.sendMessage("setApiKey", null)
.then((response) => {
if (response) {
setApiKey(null);
handleSetGlobalApikey(null);
}
}) })
setUseLocalNode(false) .catch((error) => {
window.sendMessage("setApiKey", null) console.error(
.then((response) => { "Failed to set API key:",
if (response) { error.message || "An error occurred"
setApiKey(null); );
handleSetGlobalApikey(null); });
}
})
.catch((error) => {
console.error("Failed to set API key:", error.message || "An error occurred");
});
} }
}} }}
disabled={false} disabled={false}
defaultChecked defaultChecked
/> />
} }
label={`Use ${isLocal ? 'Local' : 'Custom'} Node`} label={`Use ${isLocal ? "Local" : "Custom"} Node`}
/> />
</Box> </Box>
{currentNode?.url === "http://127.0.0.1:12391" && ( {currentNode?.url === "http://127.0.0.1:12391" && (
@ -379,31 +391,33 @@ export const NotAuthenticated = ({
onChange={handleFileChangeApiKey} // File input handler onChange={handleFileChangeApiKey} // File input handler
/> />
</Button> </Button>
<Typography sx={{ <Typography
fontSize: '12px', sx={{
visibility: importedApiKey ? 'visible' : 'hidden' fontSize: "12px",
}}>{`api key : ${importedApiKey}`}</Typography> visibility: importedApiKey ? "visible" : "hidden",
}}
>{`api key : ${importedApiKey}`}</Typography>
</> </>
)} )}
<Button <Button
size="small" size="small"
onClick={() => { onClick={() => {
setShow(true); setShow(true);
}} }}
variant="contained" variant="contained"
component="label" component="label"
> >
Choose custom node Choose custom node
</Button> </Button>
</> </>
<Typography sx={{ <Typography
color: "white", sx={{
fontSize: '12px' color: "white",
}}>Build version: {manifestData?.version}</Typography> fontSize: "12px",
}}
>
Build version: {manifestData?.version}
</Typography>
</Box> </Box>
</> </>
<CustomizedSnackbars <CustomizedSnackbars
@ -430,7 +444,6 @@ export const NotAuthenticated = ({
flexDirection: "column", flexDirection: "column",
}} }}
> >
{mode === "list" && ( {mode === "list" && (
<Box <Box
sx={{ sx={{
@ -472,17 +485,20 @@ export const NotAuthenticated = ({
setMode("list"); setMode("list");
setShow(false); setShow(false);
setUseLocalNode(false); setUseLocalNode(false);
window.sendMessage("setApiKey", null) window
.then((response) => { .sendMessage("setApiKey", null)
if (response) { .then((response) => {
setApiKey(null); if (response) {
handleSetGlobalApikey(null); 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"
);
});
}} }}
variant="contained" variant="contained"
> >
@ -527,18 +543,21 @@ export const NotAuthenticated = ({
setMode("list"); setMode("list");
setShow(false); setShow(false);
setIsValidApiKey(false); setIsValidApiKey(false);
setUseLocalNode(false); setUseLocalNode(false);
window.sendMessage("setApiKey", null) window
.then((response) => { .sendMessage("setApiKey", null)
if (response) { .then((response) => {
setApiKey(null); if (response) {
handleSetGlobalApikey(null); 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"
);
});
}} }}
variant="contained" variant="contained"
> >
@ -563,7 +582,6 @@ export const NotAuthenticated = ({
...(customNodes || []), ...(customNodes || []),
].filter((item) => item?.url !== node?.url); ].filter((item) => item?.url !== node?.url);
saveCustomNodes(nodesToSave); saveCustomNodes(nodesToSave);
}} }}
variant="contained" variant="contained"
@ -601,9 +619,7 @@ export const NotAuthenticated = ({
/> />
</Box> </Box>
)} )}
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
{mode === "list" && ( {mode === "list" && (