diff --git a/public/electron.js b/public/electron.js index d741f3a12..9b1576790 100644 --- a/public/electron.js +++ b/public/electron.js @@ -784,6 +784,51 @@ ipcMain.handle('download-optedout-mods', async (e, { mods, instancePath }) => { error: false, warning: true }); + } else if (details.statusCode > 400) { + /** + * Check for Cloudflare blocking automated downloads. + * + * Sometimes, Cloudflare prevents the internal browser from navigating to the + * Curseforge mod download page and starting the download. The HTTP status code + * it returns is (generally) either 403 or 503. The code below retrieves the + * HTML of the page returned to the browser and checks for the title and some + * content on the page to determine if the returned page is Cloudflare. + * Unfortunately using the `webContents.getTitle()` returns an empty string. + */ + details.webContents + .executeJavaScript( + ` + function getHTML () { + return new Promise((resolve, reject) => { resolve(document.documentElement.innerHTML); }); + } + getHTML(); + ` + ) + .then(content => { + const isCloudflare = + content.includes('Just a moment...') && + content.includes( + 'needs to review the security of your connection before proceeding.' + ); + + if (isCloudflare) { + resolve(); + mainWindow.webContents.send( + 'opted-out-download-mod-status', + { + modId: modManifest.id, + error: false, + warning: true, + cloudflareBlock: true + } + ); + } + + return null; + }) + .catch(() => { + // no-op + }); } } ); @@ -934,7 +979,7 @@ ipcMain.handle('installUpdateAndQuitOrRestart', async (e, quitAfterInstall) => { await fs.writeFile( path.join(tempFolder, updaterVbs), - `Set WshShell = CreateObject("WScript.Shell") + `Set WshShell = CreateObject("WScript.Shell") WshShell.Run chr(34) & "${path.join( tempFolder, updaterBat diff --git a/src/common/modals/OptedOutModsList.js b/src/common/modals/OptedOutModsList.js index 3c27083c3..a47d53a72 100644 --- a/src/common/modals/OptedOutModsList.js +++ b/src/common/modals/OptedOutModsList.js @@ -3,7 +3,10 @@ import { LoadingOutlined } from '@ant-design/icons'; import { useDispatch, useSelector } from 'react-redux'; import { Button, Spin } from 'antd'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; +import { + faExclamationTriangle, + faFileDownload +} from '@fortawesome/free-solid-svg-icons'; import { ipcRenderer } from 'electron'; import styled from 'styled-components'; import Modal from '../components/Modal'; @@ -56,7 +59,14 @@ const RowContainer = styled.div` } `; -const ModRow = ({ mod, loadedMods, currentMod, missingMods }) => { +const ModRow = ({ + mod, + loadedMods, + currentMod, + missingMods, + cloudflareBlock, + downloadUrl +}) => { const { modManifest, addon } = mod; const loaded = loadedMods.includes(modManifest.id); const missing = missingMods.includes(modManifest.id); @@ -77,8 +87,8 @@ const ModRow = ({ mod, loadedMods, currentMod, missingMods }) => { return (
{`${addon?.name} - ${modManifest?.displayName}`}
- {loaded && !missing &&
} - {loaded && missing && ( + {loaded && !missing && !cloudflareBlock &&
} + {loaded && missing && !cloudflareBlock && ( { `} /> )} + {loaded && !missing && cloudflareBlock && ( + + )} {!loaded && isCurrentMod && ( } /> )} @@ -102,6 +117,8 @@ const OptedOutModsList = ({ }) => { const [loadedMods, setLoadedMods] = useState([]); const [missingMods, setMissingMods] = useState([]); + const [cloudflareBlock, setCloudflareBlock] = useState(false); + const [manualDownloadUrls, setManualDownloadUrls] = useState([]); const [downloading, setDownloading] = useState(false); const dispatch = useDispatch(); @@ -135,14 +152,21 @@ const OptedOutModsList = ({ const listener = (e, status) => { if (!status.error) { if (optedOutMods.length === loadedMods.length + 1) { - if (missingMods.length === 0) { + if (missingMods.length === 0 && !cloudflareBlock) { resolve(); dispatch(closeModal()); } setDownloading(false); } setLoadedMods(prev => [...prev, status.modId]); - if (status.warning) setMissingMods(prev => [...prev, status.modId]); + if (status.warning) { + if (!status.cloudflareBlock) { + setMissingMods(prev => [...prev, status.modId]); + } else { + setCloudflareBlock(true); + setManualDownloadUrls(prev => [...prev, status.modId]); + } + } } else { dispatch(closeModal()); setTimeout(() => { @@ -159,7 +183,7 @@ const OptedOutModsList = ({ listener ); }; - }, [loadedMods, missingMods]); + }, [loadedMods, missingMods, cloudflareBlock, manualDownloadUrls]); return ( {optedOutMods && - optedOutMods.map(mod => ( - - ))} + optedOutMods.map(mod => { + return ( + + ); + })} + {cloudflareBlock && ( +

+ Cloudflare is currently blocking automated downloads. You can + manually download the mods and place them in the mods folder to + continue. Use the download buttons in the rows above, and the button + below to open the instance folder. +

+ )}
Cancel - {missingMods.length === 0 && ( + {missingMods.length === 0 && !cloudflareBlock && ( + + + )}