Skip to content

Commit a8dfa1c

Browse files
authored
More graceful handling of blocked Curseforge browser downloads (Manual downloading) (#1512)
* Switch to individual download buttons * Improve Cloudflare detection
1 parent e64eb6a commit a8dfa1c

File tree

2 files changed

+134
-18
lines changed

2 files changed

+134
-18
lines changed

public/electron.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,51 @@ ipcMain.handle('download-optedout-mods', async (e, { mods, instancePath }) => {
784784
error: false,
785785
warning: true
786786
});
787+
} else if (details.statusCode > 400) {
788+
/**
789+
* Check for Cloudflare blocking automated downloads.
790+
*
791+
* Sometimes, Cloudflare prevents the internal browser from navigating to the
792+
* Curseforge mod download page and starting the download. The HTTP status code
793+
* it returns is (generally) either 403 or 503. The code below retrieves the
794+
* HTML of the page returned to the browser and checks for the title and some
795+
* content on the page to determine if the returned page is Cloudflare.
796+
* Unfortunately using the `webContents.getTitle()` returns an empty string.
797+
*/
798+
details.webContents
799+
.executeJavaScript(
800+
`
801+
function getHTML () {
802+
return new Promise((resolve, reject) => { resolve(document.documentElement.innerHTML); });
803+
}
804+
getHTML();
805+
`
806+
)
807+
.then(content => {
808+
const isCloudflare =
809+
content.includes('Just a moment...') &&
810+
content.includes(
811+
'needs to review the security of your connection before proceeding.'
812+
);
813+
814+
if (isCloudflare) {
815+
resolve();
816+
mainWindow.webContents.send(
817+
'opted-out-download-mod-status',
818+
{
819+
modId: modManifest.id,
820+
error: false,
821+
warning: true,
822+
cloudflareBlock: true
823+
}
824+
);
825+
}
826+
827+
return null;
828+
})
829+
.catch(() => {
830+
// no-op
831+
});
787832
}
788833
}
789834
);
@@ -934,7 +979,7 @@ ipcMain.handle('installUpdateAndQuitOrRestart', async (e, quitAfterInstall) => {
934979

935980
await fs.writeFile(
936981
path.join(tempFolder, updaterVbs),
937-
`Set WshShell = CreateObject("WScript.Shell")
982+
`Set WshShell = CreateObject("WScript.Shell")
938983
WshShell.Run chr(34) & "${path.join(
939984
tempFolder,
940985
updaterBat

src/common/modals/OptedOutModsList.js

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { LoadingOutlined } from '@ant-design/icons';
33
import { useDispatch, useSelector } from 'react-redux';
44
import { Button, Spin } from 'antd';
55
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
6-
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
6+
import {
7+
faExclamationTriangle,
8+
faFileDownload
9+
} from '@fortawesome/free-solid-svg-icons';
710
import { ipcRenderer } from 'electron';
811
import styled from 'styled-components';
912
import Modal from '../components/Modal';
@@ -56,7 +59,14 @@ const RowContainer = styled.div`
5659
}
5760
`;
5861

59-
const ModRow = ({ mod, loadedMods, currentMod, missingMods }) => {
62+
const ModRow = ({
63+
mod,
64+
loadedMods,
65+
currentMod,
66+
missingMods,
67+
cloudflareBlock,
68+
downloadUrl
69+
}) => {
6070
const { modManifest, addon } = mod;
6171
const loaded = loadedMods.includes(modManifest.id);
6272
const missing = missingMods.includes(modManifest.id);
@@ -77,15 +87,20 @@ const ModRow = ({ mod, loadedMods, currentMod, missingMods }) => {
7787
return (
7888
<RowContainer ref={ref}>
7989
<div>{`${addon?.name} - ${modManifest?.displayName}`}</div>
80-
{loaded && !missing && <div className="dot" />}
81-
{loaded && missing && (
90+
{loaded && !missing && !cloudflareBlock && <div className="dot" />}
91+
{loaded && missing && !cloudflareBlock && (
8292
<FontAwesomeIcon
8393
icon={faExclamationTriangle}
8494
css={`
8595
color: ${props => props.theme.palette.colors.yellow};
8696
`}
8797
/>
8898
)}
99+
{loaded && !missing && cloudflareBlock && (
100+
<Button href={downloadUrl}>
101+
<FontAwesomeIcon icon={faFileDownload} />
102+
</Button>
103+
)}
89104
{!loaded && isCurrentMod && (
90105
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} />
91106
)}
@@ -102,6 +117,8 @@ const OptedOutModsList = ({
102117
}) => {
103118
const [loadedMods, setLoadedMods] = useState([]);
104119
const [missingMods, setMissingMods] = useState([]);
120+
const [cloudflareBlock, setCloudflareBlock] = useState(false);
121+
const [manualDownloadUrls, setManualDownloadUrls] = useState([]);
105122
const [downloading, setDownloading] = useState(false);
106123

107124
const dispatch = useDispatch();
@@ -135,14 +152,21 @@ const OptedOutModsList = ({
135152
const listener = (e, status) => {
136153
if (!status.error) {
137154
if (optedOutMods.length === loadedMods.length + 1) {
138-
if (missingMods.length === 0) {
155+
if (missingMods.length === 0 && !cloudflareBlock) {
139156
resolve();
140157
dispatch(closeModal());
141158
}
142159
setDownloading(false);
143160
}
144161
setLoadedMods(prev => [...prev, status.modId]);
145-
if (status.warning) setMissingMods(prev => [...prev, status.modId]);
162+
if (status.warning) {
163+
if (!status.cloudflareBlock) {
164+
setMissingMods(prev => [...prev, status.modId]);
165+
} else {
166+
setCloudflareBlock(true);
167+
setManualDownloadUrls(prev => [...prev, status.modId]);
168+
}
169+
}
146170
} else {
147171
dispatch(closeModal());
148172
setTimeout(() => {
@@ -159,7 +183,7 @@ const OptedOutModsList = ({
159183
listener
160184
);
161185
};
162-
}, [loadedMods, missingMods]);
186+
}, [loadedMods, missingMods, cloudflareBlock, manualDownloadUrls]);
163187

164188
return (
165189
<Modal
@@ -191,15 +215,32 @@ const OptedOutModsList = ({
191215
</div>
192216
<ModsContainer>
193217
{optedOutMods &&
194-
optedOutMods.map(mod => (
195-
<ModRow
196-
mod={mod}
197-
loadedMods={loadedMods}
198-
currentMod={currentMod}
199-
missingMods={missingMods}
200-
/>
201-
))}
218+
optedOutMods.map(mod => {
219+
return (
220+
<ModRow
221+
mod={mod}
222+
loadedMods={loadedMods}
223+
currentMod={currentMod}
224+
missingMods={missingMods}
225+
cloudflareBlock={cloudflareBlock}
226+
downloadUrl={`${mod.addon.links.websiteUrl}/download/${mod.modManifest.id}`}
227+
/>
228+
);
229+
})}
202230
</ModsContainer>
231+
{cloudflareBlock && (
232+
<p
233+
css={`
234+
width: 90%;
235+
margin: 20px auto 0 auto;
236+
`}
237+
>
238+
Cloudflare is currently blocking automated downloads. You can
239+
manually download the mods and place them in the mods folder to
240+
continue. Use the download buttons in the rows above, and the button
241+
below to open the instance folder.
242+
</p>
243+
)}
203244
<div
204245
css={`
205246
display: flex;
@@ -224,7 +265,7 @@ const OptedOutModsList = ({
224265
>
225266
Cancel
226267
</Button>
227-
{missingMods.length === 0 && (
268+
{missingMods.length === 0 && !cloudflareBlock && (
228269
<Button
229270
type="primary"
230271
disabled={downloading}
@@ -257,7 +298,7 @@ const OptedOutModsList = ({
257298
Confirm
258299
</Button>
259300
)}
260-
{missingMods.length > 0 && (
301+
{missingMods.length > 0 && !cloudflareBlock && (
261302
<Button
262303
type="primary"
263304
disabled={downloading}
@@ -272,6 +313,36 @@ const OptedOutModsList = ({
272313
Continue
273314
</Button>
274315
)}
316+
{cloudflareBlock && (
317+
<>
318+
<Button
319+
type="primary"
320+
disabled={downloading}
321+
onClick={() => {
322+
ipcRenderer.invoke('openFolder', instancePath);
323+
}}
324+
css={`
325+
background-color: ${props => props.theme.palette.colors.blue};
326+
`}
327+
>
328+
Open folder
329+
</Button>
330+
<Button
331+
type="primary"
332+
disabled={downloading}
333+
onClick={() => {
334+
resolve();
335+
dispatch(closeModal());
336+
}}
337+
css={`
338+
background-color: ${props =>
339+
props.theme.palette.colors.green};
340+
`}
341+
>
342+
Continue
343+
</Button>
344+
</>
345+
)}
275346
</div>
276347
</Container>
277348
</Modal>

0 commit comments

Comments
 (0)