Skip to content

Commit

Permalink
More graceful handling of blocked Curseforge browser downloads (Manua…
Browse files Browse the repository at this point in the history
…l downloading) (#1512)

* Switch to individual download buttons

* Improve Cloudflare detection
  • Loading branch information
Celeo authored Jan 19, 2023
1 parent e64eb6a commit a8dfa1c
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 18 deletions.
47 changes: 46 additions & 1 deletion public/electron.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}
}
);
Expand Down Expand Up @@ -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
Expand Down
105 changes: 88 additions & 17 deletions src/common/modals/OptedOutModsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -77,15 +87,20 @@ const ModRow = ({ mod, loadedMods, currentMod, missingMods }) => {
return (
<RowContainer ref={ref}>
<div>{`${addon?.name} - ${modManifest?.displayName}`}</div>
{loaded && !missing && <div className="dot" />}
{loaded && missing && (
{loaded && !missing && !cloudflareBlock && <div className="dot" />}
{loaded && missing && !cloudflareBlock && (
<FontAwesomeIcon
icon={faExclamationTriangle}
css={`
color: ${props => props.theme.palette.colors.yellow};
`}
/>
)}
{loaded && !missing && cloudflareBlock && (
<Button href={downloadUrl}>
<FontAwesomeIcon icon={faFileDownload} />
</Button>
)}
{!loaded && isCurrentMod && (
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} />
)}
Expand All @@ -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();
Expand Down Expand Up @@ -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(() => {
Expand All @@ -159,7 +183,7 @@ const OptedOutModsList = ({
listener
);
};
}, [loadedMods, missingMods]);
}, [loadedMods, missingMods, cloudflareBlock, manualDownloadUrls]);

return (
<Modal
Expand Down Expand Up @@ -191,15 +215,32 @@ const OptedOutModsList = ({
</div>
<ModsContainer>
{optedOutMods &&
optedOutMods.map(mod => (
<ModRow
mod={mod}
loadedMods={loadedMods}
currentMod={currentMod}
missingMods={missingMods}
/>
))}
optedOutMods.map(mod => {
return (
<ModRow
mod={mod}
loadedMods={loadedMods}
currentMod={currentMod}
missingMods={missingMods}
cloudflareBlock={cloudflareBlock}
downloadUrl={`${mod.addon.links.websiteUrl}/download/${mod.modManifest.id}`}
/>
);
})}
</ModsContainer>
{cloudflareBlock && (
<p
css={`
width: 90%;
margin: 20px auto 0 auto;
`}
>
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.
</p>
)}
<div
css={`
display: flex;
Expand All @@ -224,7 +265,7 @@ const OptedOutModsList = ({
>
Cancel
</Button>
{missingMods.length === 0 && (
{missingMods.length === 0 && !cloudflareBlock && (
<Button
type="primary"
disabled={downloading}
Expand Down Expand Up @@ -257,7 +298,7 @@ const OptedOutModsList = ({
Confirm
</Button>
)}
{missingMods.length > 0 && (
{missingMods.length > 0 && !cloudflareBlock && (
<Button
type="primary"
disabled={downloading}
Expand All @@ -272,6 +313,36 @@ const OptedOutModsList = ({
Continue
</Button>
)}
{cloudflareBlock && (
<>
<Button
type="primary"
disabled={downloading}
onClick={() => {
ipcRenderer.invoke('openFolder', instancePath);
}}
css={`
background-color: ${props => props.theme.palette.colors.blue};
`}
>
Open folder
</Button>
<Button
type="primary"
disabled={downloading}
onClick={() => {
resolve();
dispatch(closeModal());
}}
css={`
background-color: ${props =>
props.theme.palette.colors.green};
`}
>
Continue
</Button>
</>
)}
</div>
</Container>
</Modal>
Expand Down

0 comments on commit a8dfa1c

Please sign in to comment.