Skip to content

Commit f976f6d

Browse files
authored
fabric: validate launcher jar and use HTTP/2 (#616)
1 parent 1ae1a3a commit f976f6d

File tree

10 files changed

+113
-19
lines changed

10 files changed

+113
-19
lines changed

src/main/java/me/itzg/helpers/McImageHelper.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ private static void setLevel(boolean enabled, Level level) {
150150
if (Level.TRACE.isGreaterOrEqual(level)) {
151151
((Logger) LoggerFactory.getLogger("org.apache.hc.client5.http")).setLevel(
152152
enabled ? level : Level.INFO);
153+
((Logger) LoggerFactory.getLogger("reactor.netty.http.client.HttpClient")).setLevel(
154+
enabled ? level : Level.INFO);
153155
}
154156
}
155157

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package me.itzg.helpers.errors;
2+
3+
import java.io.IOException;
4+
5+
public class InvalidContentException extends IOException {
6+
7+
public InvalidContentException(String message) {
8+
super(message);
9+
}
10+
public InvalidContentException(String message, Throwable cause) {
11+
super(message, cause);
12+
}
13+
}

src/main/java/me/itzg/helpers/fabric/FabricLauncherInstaller.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.nio.file.Path;
99
import java.nio.file.Paths;
1010
import java.util.Properties;
11+
import lombok.AccessLevel;
1112
import lombok.Getter;
1213
import lombok.NonNull;
1314
import lombok.RequiredArgsConstructor;
@@ -42,6 +43,12 @@ public class FabricLauncherInstaller {
4243
@Getter @Setter
4344
private boolean forceReinstall;
4445

46+
/**
47+
* For testing purposes.
48+
*/
49+
@Getter @Setter(AccessLevel.PACKAGE)
50+
private boolean skipValidation;
51+
4552
public void installUsingVersions(
4653
Options sharedFetchOptions, @NonNull String minecraftVersion,
4754
@Nullable String loaderVersion,
@@ -97,7 +104,8 @@ private Mono<FabricManifest> downloadResolvedLauncher(FabricMetaClient fabricMet
97104
if (needsInstall) {
98105
return fabricMetaClient.downloadLauncher(
99106
outputDir, minecraftVersion, loaderVersion, installerVersion,
100-
Fetch.loggingDownloadStatusHandler(log)
107+
Fetch.loggingDownloadStatusHandler(log),
108+
skipValidation
101109
)
102110
.publishOn(Schedulers.boundedElastic())
103111
.flatMap(launcherPath ->

src/main/java/me/itzg/helpers/fabric/FabricMetaClient.java

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
package me.itzg.helpers.fabric;
22

3+
import java.io.BufferedReader;
34
import java.io.IOException;
5+
import java.nio.file.Files;
46
import java.nio.file.Path;
57
import java.time.Duration;
68
import java.util.List;
9+
import java.util.Properties;
710
import java.util.function.Predicate;
811
import lombok.Setter;
12+
import lombok.extern.slf4j.Slf4j;
913
import me.itzg.helpers.errors.GenericException;
14+
import me.itzg.helpers.errors.InvalidContentException;
15+
import me.itzg.helpers.files.IoStreams;
1016
import me.itzg.helpers.http.FileDownloadStatusHandler;
1117
import me.itzg.helpers.http.SharedFetch;
1218
import me.itzg.helpers.http.UriBuilder;
1319
import org.jetbrains.annotations.NotNull;
1420
import org.jetbrains.annotations.Nullable;
1521
import reactor.core.publisher.Mono;
22+
import reactor.core.scheduler.Schedulers;
1623
import reactor.util.retry.Retry;
1724

25+
@Slf4j
1826
public class FabricMetaClient {
1927

2028
private final SharedFetch sharedFetch;
@@ -32,7 +40,7 @@ public class FabricMetaClient {
3240
private Duration retryMinBackoff = Duration.ofMillis(500);
3341

3442
@Setter
35-
private int downloadRetryMaxAttempts = 5;
43+
private int downloadRetryMaxAttempts = 10;
3644
@Setter
3745
private Duration downloadRetryMinBackoff = Duration.ofMillis(500);
3846

@@ -134,7 +142,8 @@ public Mono<String> resolveInstallerVersion(String installerVersion) {
134142

135143
public Mono<Path> downloadLauncher(
136144
Path outputDir, String minecraftVersion, String loaderVersion, String installerVersion,
137-
FileDownloadStatusHandler statusHandler
145+
FileDownloadStatusHandler statusHandler,
146+
boolean skipValidation
138147
) {
139148
return sharedFetch.fetch(
140149
uriBuilder.resolve(
@@ -146,9 +155,56 @@ public Mono<Path> downloadLauncher(
146155
.handleStatus(statusHandler)
147156
.assemble()
148157
.retryWhen(Retry.backoff(downloadRetryMaxAttempts, downloadRetryMinBackoff).filter(IOException.class::isInstance))
158+
.flatMap(path -> skipValidation ? Mono.just(path) : validateLauncherJar(path))
159+
.doOnError(InvalidContentException.class, e -> log.warn("Invalid launcher jar, will try again", e))
160+
.retryWhen(Retry.backoff(downloadRetryMaxAttempts, downloadRetryMinBackoff).filter(InvalidContentException.class::isInstance))
149161
.checkpoint("downloadLauncher");
150162
}
151163

164+
private Mono<Path> validateLauncherJar(Path path) {
165+
return Mono.fromCallable(() -> {
166+
log.debug("Validating Fabric launcher file {}", path);
167+
168+
if (!path.toFile().isFile()) {
169+
throw new InvalidContentException("Downloaded launcher jar is not a file");
170+
}
171+
try {
172+
final Properties installProperties = IoStreams.readFileFromZip(path, "install.properties", in -> {
173+
Properties p = new Properties();
174+
p.load(in);
175+
return p;
176+
}
177+
);
178+
if (installProperties == null) {
179+
debugDownloadedContent(path);
180+
throw new InvalidContentException("Downloaded launcher jar does not contain an install.properties");
181+
}
182+
if (!installProperties.containsKey("game-version")) {
183+
debugDownloadedContent(path);
184+
throw new InvalidContentException("Downloaded launcher jar does not contain a valid install.properties");
185+
}
186+
} catch (IOException e) {
187+
debugDownloadedContent(path);
188+
throw new InvalidContentException("Downloaded launcher jar could not be read as a jar/zip", e);
189+
}
190+
191+
return path;
192+
})
193+
.subscribeOn(Schedulers.boundedElastic());
194+
}
195+
196+
private static void debugDownloadedContent(Path path) {
197+
if (log.isDebugEnabled()) {
198+
try (BufferedReader reader = Files.newBufferedReader(path)) {
199+
final char[] buf = new char[100];
200+
final int amount = reader.read(buf);
201+
log.debug("Downloaded launcher jar content starts with: {}", new String(buf, 0, amount));
202+
} catch (IOException e) {
203+
throw new GenericException("Failed to read downloaded launcher jar for debugging", e);
204+
}
205+
}
206+
}
207+
152208
@NotNull
153209
private static Mono<String> findFirst(List<VersionEntry> versionEntries, Predicate<VersionEntry> condition
154210
) {

src/main/java/me/itzg/helpers/http/OutputToDirectoryFetchBuilder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ else if (skipUpToDate) {
231231
return failedRequestMono(resp, byteBufFlux.aggregate(), "Downloading file");
232232
}
233233

234+
if (log.isTraceEnabled()) {
235+
resp.responseHeaders().forEach(header -> log.trace("Response header: {}={}", header.getKey(), header.getValue()));
236+
}
237+
234238
return copyBodyInputStreamToFile(byteBufFlux, outputFile);
235239
})
236240
.last()

src/main/java/me/itzg/helpers/http/SharedFetch.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import lombok.extern.slf4j.Slf4j;
1313
import me.itzg.helpers.McImageHelper;
1414
import me.itzg.helpers.errors.GenericException;
15-
import reactor.netty.http.Http11SslContextSpec;
15+
import reactor.netty.http.Http2SslContextSpec;
16+
import reactor.netty.http.HttpProtocol;
1617
import reactor.netty.http.client.HttpClient;
1718
import reactor.netty.resources.ConnectionProvider;
19+
import reactor.netty.tcp.SslProvider.GenericSslContextSpec;
1820

1921
/**
2022
* Provides an efficient way to make multiple web requests since a single client
@@ -58,6 +60,8 @@ public SharedFetch(String forCommand, Options options) {
5860

5961
reactiveClient = HttpClient.create(connectionProvider)
6062
.proxyWithSystemProperties()
63+
// https://projectreactor.io/docs/netty/release/reference/http-client.html#HTTP2
64+
.protocol(HttpProtocol.HTTP11, HttpProtocol.H2)
6165
.headers(headers -> {
6266
headers
6367
.set(HttpHeaderNames.USER_AGENT.toString(), userAgent)
@@ -69,11 +73,14 @@ public SharedFetch(String forCommand, Options options) {
6973
)
7074
// Reference https://projectreactor.io/docs/netty/release/reference/index.html#response-timeout
7175
.responseTimeout(options.getResponseTimeout())
72-
// Reference https://projectreactor.io/docs/netty/release/reference/index.html#ssl-tls-timeout
7376
.secure(spec ->
74-
spec.sslContext(Http11SslContextSpec.forClient())
77+
// Http2 SSL supports both HTTP/2 and HTTP/1.1
78+
spec.sslContext((GenericSslContextSpec<?>) Http2SslContextSpec.forClient())
79+
// Reference https://projectreactor.io/docs/netty/release/reference/index.html#ssl-tls-timeout
7580
.handshakeTimeout(options.getTlsHandshakeTimeout())
76-
);
81+
)
82+
83+
;
7784

7885
headers.put("x-fetch-session", fetchSessionId);
7986

src/main/java/me/itzg/helpers/modrinth/ModrinthApiClient.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import me.itzg.helpers.errors.InvalidParameterException;
2121
import me.itzg.helpers.http.FailedRequestException;
2222
import me.itzg.helpers.http.Fetch;
23-
import me.itzg.helpers.http.FileDownloadedHandler;
2423
import me.itzg.helpers.http.SharedFetch;
2524
import me.itzg.helpers.http.SharedFetch.Options;
2625
import me.itzg.helpers.http.UriBuilder;
@@ -266,10 +265,10 @@ public void close() {
266265
sharedFetch.close();
267266
}
268267

269-
public Mono<Path> downloadFileFromUrl(Path outputFile, URI uri, FileDownloadedHandler fileDownloadedHandler) {
268+
public Mono<Path> downloadFileFromUrl(Path outputFile, URI uri) {
270269
return sharedFetch.fetch(uri)
271270
.toFile(outputFile)
272-
.handleDownloaded(fileDownloadedHandler)
271+
.handleStatus(Fetch.loggingDownloadStatusHandler(log))
273272
.skipExisting(true)
274273
.assemble();
275274
}

src/main/java/me/itzg/helpers/modrinth/ModrinthHttpPackFetcher.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ public class ModrinthHttpPackFetcher implements ModrinthPackFetcher {
2222
@Override
2323
public Mono<FetchedPack> fetchModpack(ModrinthModpackManifest prevManifest) {
2424
return apiClient.downloadFileFromUrl(
25-
destFilePath, modpackUri,
26-
(uri, file, contentSizeBytes) ->
27-
log.info("Downloaded {}", destFilePath)
25+
destFilePath, modpackUri
2826
)
2927
.map(mrPackFile -> new FetchedPack(mrPackFile, "custom", deriveVersionId(), deriveVersionName()));
3028
}

src/main/java/me/itzg/helpers/modrinth/ModrinthPackInstaller.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ public Mono<Installation> processModpack(SharedFetch sharedFetch) {
118118
));
119119
}
120120

121+
log.info("Processing modpack files for {} {}", modpackIndex.getName(), modpackIndex.getVersionId());
122+
121123
return processModFiles(modpackIndex)
122124
.collectList()
123125
.map(modFiles ->
@@ -168,9 +170,7 @@ private Flux<Path> processModFiles(ModpackIndex modpackIndex) {
168170

169171
return this.apiClient.downloadFileFromUrl(
170172
outFilePath,
171-
modpackFile.getDownloads().get(0),
172-
(uri, file, contentSizeBytes) ->
173-
log.info("Downloaded {}", modpackFilePath)
173+
modpackFile.getDownloads().get(0)
174174
);
175175
}, maxConcurrentDownloads);
176176
}

src/test/java/me/itzg/helpers/fabric/FabricLauncherInstallerTest.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ void testInstallUsingVersions_onlyGameVersion(WireMockRuntimeInfo wmRuntimeInfo)
5656
final FabricLauncherInstaller installer = new FabricLauncherInstaller(
5757
tempDir
5858
)
59+
.setSkipValidation(true)
5960
.setResultsFile(resultsFile);
6061
installer.setFabricMetaBaseUrl(wmRuntimeInfo.getHttpBaseUrl());
6162

@@ -106,6 +107,7 @@ void testWithProvidedUri(WireMockRuntimeInfo wmRuntimeInfo) throws IOException {
106107

107108
final Path expectedResultsPath = tempDir.resolve("results.env");
108109
final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir)
110+
.setSkipValidation(true)
109111
.setResultsFile(expectedResultsPath);
110112
final URI loaderUri = URI.create(wmRuntimeInfo.getHttpBaseUrl() + "/fabric-launcher.jar");
111113

@@ -142,7 +144,8 @@ void testWithProvidedUri_contentDisposition(WireMockRuntimeInfo wmRuntimeInfo) t
142144
)
143145
);
144146

145-
final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir);
147+
final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir)
148+
.setSkipValidation(true);
146149
installer.installUsingUri(
147150
Options.builder().build(), URI.create(wmRuntimeInfo.getHttpBaseUrl() + "/server")
148151
);
@@ -157,6 +160,7 @@ void testWithProvidedUri_contentDisposition(WireMockRuntimeInfo wmRuntimeInfo) t
157160
void testWithLocalLauncherFile() throws IOException {
158161
final Path expectedResultsPath = tempDir.resolve("results.env");
159162
final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir)
163+
.setSkipValidation(true)
160164
.setResultsFile(expectedResultsPath);
161165

162166
final Path launcherFile = Paths.get("src/test/resources/__files/fabric-empty-launcher.jar");
@@ -179,7 +183,8 @@ void testUpgradeFromVersionToVersion(WireMockRuntimeInfo wmRuntimeInfo) {
179183

180184
final FabricLauncherInstaller installer = new FabricLauncherInstaller(
181185
tempDir
182-
);
186+
)
187+
.setSkipValidation(true);
183188
installer.setFabricMetaBaseUrl(wmRuntimeInfo.getHttpBaseUrl());
184189

185190
installer.installUsingVersions(
@@ -213,7 +218,8 @@ void testNoNetworkUsageWhenVersionMatches(WireMockRuntimeInfo wmRuntimeInfo) {
213218

214219
final FabricLauncherInstaller installer = new FabricLauncherInstaller(
215220
tempDir
216-
);
221+
)
222+
.setSkipValidation(true);
217223
installer.setFabricMetaBaseUrl(wmRuntimeInfo.getHttpBaseUrl());
218224

219225
installer.installUsingVersions(
@@ -255,6 +261,7 @@ void testNoNetworkUsageWhenVersionMatches(WireMockRuntimeInfo wmRuntimeInfo) {
255261
void forRecordingVersionDiscovery() {
256262
final Path resultsFile = tempDir.resolve("results.env");
257263
final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir)
264+
.setSkipValidation(true)
258265
.setResultsFile(resultsFile);
259266
installer.setFabricMetaBaseUrl("http://localhost:8080");
260267

0 commit comments

Comments
 (0)