Skip to content

Commit ea801f9

Browse files
committed
fix: authenticate CurseForge CDN downloads
1 parent d2f093f commit ea801f9

13 files changed

Lines changed: 163 additions & 72 deletions

File tree

core/src/main/java/pl/skidam/automodpack_core/Constants.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package pl.skidam.automodpack_core;
22

3+
import java.nio.charset.StandardCharsets;
34
import java.nio.file.Path;
5+
import java.util.Base64;
46

57
import org.apache.logging.log4j.LogManager;
68
import org.apache.logging.log4j.Logger;
@@ -20,6 +22,9 @@ public class Constants {
2022
public static long PRELOAD_TIME;
2123
public static String MC_VERSION;
2224
public static String AM_VERSION;
25+
private static final String CURSEFORGE_KEY = "JDJhJDEwJHNrbDRkNFkyTVI2Yy5uWmhWM3VWSy5HQmVLZDNNTDRSS3lNbnM4RFpxajkxSGpmL0hZcmNT";
26+
public static final String CURSEFORGE_API_HOST = "api.curseforge.com";
27+
public static final String CURSEFORGE_CDN_HOST = "edge.forgecdn.net";
2328
public static String LOADER_VERSION;
2429
public static String LOADER;
2530
public static LoaderManagerService LOADER_MANAGER = new NullLoaderManager();
@@ -65,4 +70,8 @@ public class Constants {
6570
public static String clientConfigOverride; // read from inside a jar file on preload, used instead of clientConfigFile if exists
6671

6772
public static Path selectedModpackDir;
73+
74+
public static String summonCurseForgeKey() {
75+
return new String(Base64.getDecoder().decode(CURSEFORGE_KEY), StandardCharsets.UTF_8);
76+
}
6877
}

loader/core/src/main/java/pl/skidam/automodpack_loader_core/platforms/CurseForgeAPI.java renamed to core/src/main/java/pl/skidam/automodpack_core/platforms/CurseForgeAPI.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
package pl.skidam.automodpack_loader_core.platforms;
1+
package pl.skidam.automodpack_core.platforms;
22

3+
import static pl.skidam.automodpack_core.Constants.CURSEFORGE_API_HOST;
34
import static pl.skidam.automodpack_core.Constants.LOGGER;
45

56
import java.util.LinkedList;
@@ -15,7 +16,7 @@
1516
public record CurseForgeAPI(String requestUrl, String downloadUrl, String fileVersion, String fileName, String fileSize, String releaseType, String murmurHash,
1617
String sha1Hash) {
1718

18-
public static final String BASE_URL = "https://api.curseforge.com/v1";
19+
public static final String BASE_URL = "https://" + CURSEFORGE_API_HOST + "/v1";
1920

2021
// key - sha1, value - murmur
2122
// https://docs.curseforge.com/?java#get-fingerprints-matches

loader/core/src/main/java/pl/skidam/automodpack_loader_core/platforms/ModrinthAPI.java renamed to core/src/main/java/pl/skidam/automodpack_core/platforms/ModrinthAPI.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package pl.skidam.automodpack_loader_core.platforms;
1+
package pl.skidam.automodpack_core.platforms;
22

33
import static pl.skidam.automodpack_core.Constants.*;
44

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package pl.skidam.automodpack_core.utils;
2+
3+
public record DownloadSource(String url, Provider provider) {
4+
public enum Provider {
5+
MODRINTH, CURSEFORGE
6+
}
7+
}

loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/FetchManager.java renamed to core/src/main/java/pl/skidam/automodpack_core/utils/FetchManager.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package pl.skidam.automodpack_loader_core.utils;
1+
package pl.skidam.automodpack_core.utils;
22

33
import static pl.skidam.automodpack_core.Constants.LOGGER;
44

@@ -8,8 +8,8 @@
88
import java.util.concurrent.ThreadLocalRandom;
99
import java.util.concurrent.atomic.AtomicInteger;
1010

11-
import pl.skidam.automodpack_loader_core.platforms.CurseForgeAPI;
12-
import pl.skidam.automodpack_loader_core.platforms.ModrinthAPI;
11+
import pl.skidam.automodpack_core.platforms.CurseForgeAPI;
12+
import pl.skidam.automodpack_core.platforms.ModrinthAPI;
1313

1414
public class FetchManager {
1515

@@ -19,7 +19,7 @@ public class FetchManager {
1919
// Return the results i guess
2020

2121
public record FetchData(String file, String sha1, String murmur, String fileSize, String fileType) {}
22-
public record FetchedData(List<String> urls, List<String> mainPageUrls) {}
22+
public record FetchedData(List<DownloadSource> sources, List<String> mainPageUrls) {}
2323
public record Datas(FetchData fetchData, FetchedData fetchedData) {}
2424
private final Map<String, Datas> fetchDatas = new HashMap<>();
2525

@@ -65,13 +65,13 @@ public void fetch() {
6565
private void randomizeFinalOrder() {
6666
ThreadLocalRandom rng = ThreadLocalRandom.current();
6767
for (Datas data : fetchDatas.values()) {
68-
List<String> urls = data.fetchedData().urls();
68+
List<DownloadSource> sources = data.fetchedData().sources();
6969

7070
// Coin filp order
71-
if (urls.size() == 2 && rng.nextBoolean()) {
72-
String first = urls.get(0);
73-
urls.set(0, urls.get(1));
74-
urls.set(1, first);
71+
if (sources.size() == 2 && rng.nextBoolean()) {
72+
DownloadSource first = sources.get(0);
73+
sources.set(0, sources.get(1));
74+
sources.set(1, first);
7575
}
7676
}
7777
}
@@ -83,7 +83,7 @@ private void fetchBySha1(List<String> sha1s) {
8383
for (ModrinthAPI info : results) {
8484
Datas datas = fetchDatas.get(info.SHA1Hash());
8585
if (datas != null) {
86-
datas.fetchedData().urls().add(info.downloadUrl());
86+
datas.fetchedData().sources().add(new DownloadSource(info.downloadUrl(), DownloadSource.Provider.MODRINTH));
8787
String mainPageUrl = ModrinthAPI.getMainPageUrl(info.modrinthID(), datas.fetchData.fileType);
8888
datas.fetchedData().mainPageUrls().add(mainPageUrl);
8989
fetchesDone.incrementAndGet();
@@ -98,7 +98,7 @@ private void fetchByMurmur(Map<String, String> hashes) {
9898
for (CurseForgeAPI info : results) {
9999
Datas datas = fetchDatas.get(info.sha1Hash());
100100
if (datas != null) {
101-
datas.fetchedData().urls().add(info.downloadUrl());
101+
datas.fetchedData().sources().add(new DownloadSource(info.downloadUrl(), DownloadSource.Provider.CURSEFORGE));
102102
fetchesDone.incrementAndGet();
103103
}
104104
}

core/src/main/java/pl/skidam/automodpack_core/utils/Json.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package pl.skidam.automodpack_core.utils;
22

3-
import static pl.skidam.automodpack_core.Constants.AM_VERSION;
4-
import static pl.skidam.automodpack_core.Constants.LOGGER;
3+
import static pl.skidam.automodpack_core.Constants.*;
54

65
import java.io.IOException;
76
import java.io.InputStreamReader;
@@ -134,10 +133,15 @@ public static JsonObject fromCurseForgeUrl(final String requestUrl, List<String>
134133

135134
HttpURLConnection connection;
136135
URL url = new URL(requestUrl);
136+
if (!"https".equalsIgnoreCase(url.getProtocol()) || !CURSEFORGE_API_HOST.equalsIgnoreCase(url.getHost()) || url.getUserInfo() != null
137+
|| (url.getPort() != -1 && url.getPort() != 443)) {
138+
throw new IOException("Refusing to send the CurseForge API key to an untrusted endpoint");
139+
}
137140
connection = (HttpURLConnection) url.openConnection();
141+
connection.setInstanceFollowRedirects(false);
138142
connection.addRequestProperty("Content-Type", "application/json");
139143
connection.addRequestProperty("Accept", "application/json");
140-
connection.addRequestProperty("x-api-key", "$2a$10$skl4d4Y2MR6c.nZhV3uVK.GBeKd3ML4RKyMns8DZqj91Hjf/HYrcS");
144+
connection.addRequestProperty("x-api-key", summonCurseForgeKey());
141145
connection.setConnectTimeout(3000);
142146
connection.setReadTimeout(10000);
143147
connection.setRequestMethod("POST");
@@ -152,6 +156,8 @@ public static JsonObject fromCurseForgeUrl(final String requestUrl, List<String>
152156
try (InputStreamReader isr = new InputStreamReader(connection.getInputStream())) {
153157
element = new JsonParser().parse(isr); // Needed to parse by deprecated method because of older minecraft versions (<1.17.1)
154158
}
159+
} else if (code == HttpURLConnection.HTTP_UNAUTHORIZED) {
160+
LOGGER.error("CurseForge API authorization failed with HTTP 401");
155161
} else {
156162
LOGGER.warn("{} responded {} code", url, code);
157163
}

loader/core/src/main/java/pl/skidam/automodpack_loader_core/SelfUpdater.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818

1919
import pl.skidam.automodpack_core.config.Jsons;
2020
import pl.skidam.automodpack_core.loader.LoaderManagerService;
21+
import pl.skidam.automodpack_core.platforms.ModrinthAPI;
22+
import pl.skidam.automodpack_core.utils.DownloadSource;
2123
import pl.skidam.automodpack_core.utils.HashUtils;
2224
import pl.skidam.automodpack_core.utils.LockFreeInputStream;
2325
import pl.skidam.automodpack_core.utils.SemanticVersion;
2426
import pl.skidam.automodpack_core.utils.SmartFileUtils;
25-
import pl.skidam.automodpack_loader_core.platforms.ModrinthAPI;
2627
import pl.skidam.automodpack_loader_core.screen.ScreenManager;
2728
import pl.skidam.automodpack_loader_core.utils.DownloadManager;
2829
import pl.skidam.automodpack_loader_core.utils.UpdateType;
@@ -173,7 +174,8 @@ public static void installModVersion(ModrinthAPI automodpack) {
173174
DownloadManager downloadManager = new DownloadManager();
174175
new ScreenManager().download(downloadManager, "AutoModpack " + automodpack.fileVersion());
175176

176-
downloadManager.download(automodpackUpdateJar, automodpack.SHA1Hash(), List.of(automodpack.downloadUrl()), automodpack.fileSize(),
177+
downloadManager.download(automodpackUpdateJar, automodpack.SHA1Hash(),
178+
List.of(new DownloadSource(automodpack.downloadUrl(), DownloadSource.Provider.MODRINTH)), automodpack.fileSize(),
177179
() -> LOGGER.info("Downloaded update for AutoModpack."), () -> LOGGER.error("Failed to download update for AutoModpack."));
178180

179181
downloadManager.joinAll();

loader/core/src/main/java/pl/skidam/automodpack_loader_core/client/ModpackUpdater.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import pl.skidam.automodpack_core.config.ConfigTools;
2222
import pl.skidam.automodpack_core.config.Jsons;
2323
import pl.skidam.automodpack_core.protocol.DownloadClient;
24+
import pl.skidam.automodpack_core.utils.DownloadSource;
25+
import pl.skidam.automodpack_core.utils.FetchManager;
2426
import pl.skidam.automodpack_core.utils.FileInspection;
2527
import pl.skidam.automodpack_core.utils.LegacyClientCacheUtils;
2628
import pl.skidam.automodpack_core.utils.SmartFileUtils;
@@ -30,7 +32,6 @@
3032
import pl.skidam.automodpack_loader_core.ReLauncher;
3133
import pl.skidam.automodpack_loader_core.screen.ScreenManager;
3234
import pl.skidam.automodpack_loader_core.utils.DownloadManager;
33-
import pl.skidam.automodpack_loader_core.utils.FetchManager;
3435
import pl.skidam.automodpack_loader_core.utils.UpdateType;
3536

3637
// TODO: clean up this mess
@@ -303,12 +304,12 @@ private void downloadModpack(Set<Jsons.ModpackContentFields.ModpackContentItem>
303304

304305
if (!Files.exists(downloadFile)) { newDownloadedFiles.add(serverFilePath); }
305306

306-
List<String> urls = new ArrayList<>();
307+
List<DownloadSource> sources = new ArrayList<>();
307308
if (fetchManager != null && fetchManager.getFetchDatas().containsKey(serverFileHash)) {
308-
urls.addAll(fetchManager.getFetchDatas().get(serverFileHash).fetchedData().urls());
309+
sources.addAll(fetchManager.getFetchDatas().get(serverFileHash).fetchedData().sources());
309310
}
310311

311-
Runnable failureCallback = () -> failedDownloads.put(serverItem, urls);
312+
Runnable failureCallback = () -> failedDownloads.put(serverItem, sources.stream().map(DownloadSource::url).toList());
312313

313314
Runnable successCallback = () -> {
314315
List<String> mainPageUrls = new LinkedList<>();
@@ -325,7 +326,7 @@ private void downloadModpack(Set<Jsons.ModpackContentFields.ModpackContentItem>
325326
}
326327
};
327328

328-
downloadManager.download(downloadFile, serverFileHash, urls, serverFileSize, successCallback, failureCallback);
329+
downloadManager.download(downloadFile, serverFileHash, sources, serverFileSize, successCallback, failureCallback);
329330
}
330331

331332
downloadManager.joinAll();

loader/core/src/main/java/pl/skidam/automodpack_loader_core/utils/DownloadManager.java

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static pl.skidam.automodpack_core.Constants.*;
44

55
import java.io.*;
6+
import java.net.HttpURLConnection;
67
import java.nio.charset.StandardCharsets;
78
import java.nio.file.Files;
89
import java.nio.file.Path;
@@ -13,6 +14,7 @@
1314

1415
import pl.skidam.automodpack_core.protocol.DownloadClient;
1516
import pl.skidam.automodpack_core.utils.CustomThreadFactoryBuilder;
17+
import pl.skidam.automodpack_core.utils.DownloadSource;
1618
import pl.skidam.automodpack_core.utils.FileInspection;
1719
import pl.skidam.automodpack_core.utils.HashUtils;
1820
import pl.skidam.automodpack_core.utils.SmartFileUtils;
@@ -56,11 +58,11 @@ public void attachDownloadClient(DownloadClient downloadClient) {
5658
this.downloadClient = downloadClient;
5759
}
5860

59-
public synchronized void download(Path file, String sha1, List<String> urls, long fileSize, Runnable successCallback, Runnable failureCallback) {
61+
public synchronized void download(Path file, String sha1, List<DownloadSource> sources, long fileSize, Runnable successCallback, Runnable failureCallback) {
6062
FileInspection.HashPathPair hashPathPair = new FileInspection.HashPathPair(sha1, file);
6163
if (queuedDownloads.containsKey(hashPathPair)) return;
6264

63-
QueuedDownload task = new QueuedDownload(file, urls, fileSize, 0, successCallback, failureCallback);
65+
QueuedDownload task = new QueuedDownload(file, sources, fileSize, 0, successCallback, failureCallback);
6466
queuedDownloads.put(hashPathPair, task);
6567
totalFilesAdded++;
6668
downloadNext();
@@ -199,12 +201,9 @@ private synchronized void downloadNext() {
199201
}
200202

201203
private String predictSource(QueuedDownload task) {
202-
int numberOfIndexes = task.urls.size();
203-
int urlIndex = Math.min(task.attempts / MAX_DOWNLOAD_ATTEMPTS, numberOfIndexes);
204-
if (task.urls.size() > urlIndex) {
205-
String url = task.urls.get(urlIndex);
206-
if (!Objects.equals(url, "host")) { return getDomainFromUrl(url); }
207-
}
204+
int numberOfIndexes = task.sources.size();
205+
int sourceIndex = Math.min(task.attempts / MAX_DOWNLOAD_ATTEMPTS, numberOfIndexes);
206+
if (task.sources.size() > sourceIndex) { return getDomainFromUrl(task.sources.get(sourceIndex).url()); }
208207
return "internal_client";
209208
}
210209

@@ -248,14 +247,14 @@ private void processDownloadTask(FileInspection.HashPathPair hashPathPair, Queue
248247
}
249248

250249
private boolean attemptDownload(FileInspection.HashPathPair hashPathPair, QueuedDownload task, Path storeFile) throws InterruptedException {
251-
int numberOfIndexes = task.urls.size();
252-
int urlIndex = Math.min(task.attempts / MAX_DOWNLOAD_ATTEMPTS, numberOfIndexes);
253-
String url = (task.urls.size() > urlIndex) ? task.urls.get(urlIndex) : null;
250+
int numberOfIndexes = task.sources.size();
251+
int sourceIndex = Math.min(task.attempts / MAX_DOWNLOAD_ATTEMPTS, numberOfIndexes);
252+
DownloadSource source = (task.sources.size() > sourceIndex) ? task.sources.get(sourceIndex) : null;
254253
Path tempStoreFile = storeDir.resolve(hashPathPair.hash() + ".tmp");
255254

256255
try {
257-
if (url != null && !Objects.equals(url, "host") && task.attempts < MAX_DOWNLOAD_ATTEMPTS * numberOfIndexes) {
258-
httpDownloader.download(url, tempStoreFile, this::updateNetworkProgress);
256+
if (source != null && task.attempts < MAX_DOWNLOAD_ATTEMPTS * numberOfIndexes) {
257+
httpDownloader.download(source, tempStoreFile, this::updateNetworkProgress);
259258
} else if (downloadClient != null) {
260259
hostDownloadFile(hashPathPair, tempStoreFile, this::updateNetworkProgress);
261260
} else {
@@ -270,6 +269,13 @@ private boolean attemptDownload(FileInspection.HashPathPair hashPathPair, Queued
270269
SmartFileUtils.executeOrder66(tempStoreFile);
271270
return false;
272271
}
272+
} catch (HttpFileDownloader.HttpStatusException e) {
273+
if (source != null && source.provider() == DownloadSource.Provider.CURSEFORGE && e.statusCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
274+
LOGGER.warn("CurseForge rejected the download API key with HTTP 401; trying the next source");
275+
task.attempts = (sourceIndex + 1) * MAX_DOWNLOAD_ATTEMPTS - 1;
276+
}
277+
SmartFileUtils.executeOrder66(tempStoreFile);
278+
return false;
273279
} catch (IOException e) {
274280
SmartFileUtils.executeOrder66(tempStoreFile);
275281
return false;
@@ -314,7 +320,7 @@ private void handleRetry(FileInspection.HashPathPair key, QueuedDownload task, b
314320
}
315321
SmartFileUtils.executeOrder66(task.file);
316322

317-
if (task.attempts < (task.urls.size() + 1) * MAX_DOWNLOAD_ATTEMPTS) {
323+
if (task.attempts < (task.sources.size() + 1) * MAX_DOWNLOAD_ATTEMPTS) {
318324
LOGGER.warn("Retrying download: {}", task.file.getFileName());
319325
task.attempts++;
320326
queuedDownloads.put(key, task);
@@ -402,15 +408,15 @@ public void setCancelled(boolean cancelled) {
402408

403409
public static class QueuedDownload {
404410
public final Path file;
405-
public final List<String> urls;
411+
public final List<DownloadSource> sources;
406412
public final long fileSize;
407413
public int attempts;
408414
public final Runnable successCallback;
409415
public final Runnable failureCallback;
410416

411-
public QueuedDownload(Path f, List<String> u, long size, int a, Runnable s, Runnable fa) {
417+
public QueuedDownload(Path f, List<DownloadSource> sources, long size, int a, Runnable s, Runnable fa) {
412418
file = f;
413-
urls = u;
419+
this.sources = sources;
414420
fileSize = size;
415421
attempts = a;
416422
successCallback = s;

0 commit comments

Comments
 (0)