Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/java/me/itzg/helpers/McImageHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ private static void setLevel(boolean enabled, Level level) {
if (Level.TRACE.isGreaterOrEqual(level)) {
((Logger) LoggerFactory.getLogger("org.apache.hc.client5.http")).setLevel(
enabled ? level : Level.INFO);
((Logger) LoggerFactory.getLogger("reactor.netty.http.client.HttpClient")).setLevel(
enabled ? level : Level.INFO);
}
}

Expand Down
13 changes: 13 additions & 0 deletions src/main/java/me/itzg/helpers/errors/InvalidContentException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package me.itzg.helpers.errors;

import java.io.IOException;

public class InvalidContentException extends IOException {

public InvalidContentException(String message) {
super(message);
}
public InvalidContentException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -42,6 +43,12 @@ public class FabricLauncherInstaller {
@Getter @Setter
private boolean forceReinstall;

/**
* For testing purposes.
*/
@Getter @Setter(AccessLevel.PACKAGE)
private boolean skipValidation;

public void installUsingVersions(
Options sharedFetchOptions, @NonNull String minecraftVersion,
@Nullable String loaderVersion,
Expand Down Expand Up @@ -97,7 +104,8 @@ private Mono<FabricManifest> downloadResolvedLauncher(FabricMetaClient fabricMet
if (needsInstall) {
return fabricMetaClient.downloadLauncher(
outputDir, minecraftVersion, loaderVersion, installerVersion,
Fetch.loggingDownloadStatusHandler(log)
Fetch.loggingDownloadStatusHandler(log),
skipValidation
)
.publishOn(Schedulers.boundedElastic())
.flatMap(launcherPath ->
Expand Down
60 changes: 58 additions & 2 deletions src/main/java/me/itzg/helpers/fabric/FabricMetaClient.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
package me.itzg.helpers.fabric;

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Properties;
import java.util.function.Predicate;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import me.itzg.helpers.errors.GenericException;
import me.itzg.helpers.errors.InvalidContentException;
import me.itzg.helpers.files.IoStreams;
import me.itzg.helpers.http.FileDownloadStatusHandler;
import me.itzg.helpers.http.SharedFetch;
import me.itzg.helpers.http.UriBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.retry.Retry;

@Slf4j
public class FabricMetaClient {

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

@Setter
private int downloadRetryMaxAttempts = 5;
private int downloadRetryMaxAttempts = 10;
@Setter
private Duration downloadRetryMinBackoff = Duration.ofMillis(500);

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

public Mono<Path> downloadLauncher(
Path outputDir, String minecraftVersion, String loaderVersion, String installerVersion,
FileDownloadStatusHandler statusHandler
FileDownloadStatusHandler statusHandler,
boolean skipValidation
) {
return sharedFetch.fetch(
uriBuilder.resolve(
Expand All @@ -146,9 +155,56 @@ public Mono<Path> downloadLauncher(
.handleStatus(statusHandler)
.assemble()
.retryWhen(Retry.backoff(downloadRetryMaxAttempts, downloadRetryMinBackoff).filter(IOException.class::isInstance))
.flatMap(path -> skipValidation ? Mono.just(path) : validateLauncherJar(path))
.doOnError(InvalidContentException.class, e -> log.warn("Invalid launcher jar, will try again", e))
.retryWhen(Retry.backoff(downloadRetryMaxAttempts, downloadRetryMinBackoff).filter(InvalidContentException.class::isInstance))
.checkpoint("downloadLauncher");
}

private Mono<Path> validateLauncherJar(Path path) {
return Mono.fromCallable(() -> {
log.debug("Validating Fabric launcher file {}", path);

if (!path.toFile().isFile()) {
throw new InvalidContentException("Downloaded launcher jar is not a file");
}
try {
final Properties installProperties = IoStreams.readFileFromZip(path, "install.properties", in -> {
Properties p = new Properties();
p.load(in);
return p;
}
);
if (installProperties == null) {
debugDownloadedContent(path);
throw new InvalidContentException("Downloaded launcher jar does not contain an install.properties");
}
if (!installProperties.containsKey("game-version")) {
debugDownloadedContent(path);
throw new InvalidContentException("Downloaded launcher jar does not contain a valid install.properties");
}
} catch (IOException e) {
debugDownloadedContent(path);
throw new InvalidContentException("Downloaded launcher jar could not be read as a jar/zip", e);
}

return path;
})
.subscribeOn(Schedulers.boundedElastic());
}

private static void debugDownloadedContent(Path path) {
if (log.isDebugEnabled()) {
try (BufferedReader reader = Files.newBufferedReader(path)) {
final char[] buf = new char[100];
final int amount = reader.read(buf);
log.debug("Downloaded launcher jar content starts with: {}", new String(buf, 0, amount));
} catch (IOException e) {
throw new GenericException("Failed to read downloaded launcher jar for debugging", e);
}
}
}

@NotNull
private static Mono<String> findFirst(List<VersionEntry> versionEntries, Predicate<VersionEntry> condition
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ else if (skipUpToDate) {
return failedRequestMono(resp, byteBufFlux.aggregate(), "Downloading file");
}

if (log.isTraceEnabled()) {
resp.responseHeaders().forEach(header -> log.trace("Response header: {}={}", header.getKey(), header.getValue()));
}

return copyBodyInputStreamToFile(byteBufFlux, outputFile);
})
.last()
Expand Down
15 changes: 11 additions & 4 deletions src/main/java/me/itzg/helpers/http/SharedFetch.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import lombok.extern.slf4j.Slf4j;
import me.itzg.helpers.McImageHelper;
import me.itzg.helpers.errors.GenericException;
import reactor.netty.http.Http11SslContextSpec;
import reactor.netty.http.Http2SslContextSpec;
import reactor.netty.http.HttpProtocol;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import reactor.netty.tcp.SslProvider.GenericSslContextSpec;

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

reactiveClient = HttpClient.create(connectionProvider)
.proxyWithSystemProperties()
// https://projectreactor.io/docs/netty/release/reference/http-client.html#HTTP2
.protocol(HttpProtocol.HTTP11, HttpProtocol.H2)
.headers(headers -> {
headers
.set(HttpHeaderNames.USER_AGENT.toString(), userAgent)
Expand All @@ -69,11 +73,14 @@ public SharedFetch(String forCommand, Options options) {
)
// Reference https://projectreactor.io/docs/netty/release/reference/index.html#response-timeout
.responseTimeout(options.getResponseTimeout())
// Reference https://projectreactor.io/docs/netty/release/reference/index.html#ssl-tls-timeout
.secure(spec ->
spec.sslContext(Http11SslContextSpec.forClient())
// Http2 SSL supports both HTTP/2 and HTTP/1.1
spec.sslContext((GenericSslContextSpec<?>) Http2SslContextSpec.forClient())
// Reference https://projectreactor.io/docs/netty/release/reference/index.html#ssl-tls-timeout
.handshakeTimeout(options.getTlsHandshakeTimeout())
);
)

;

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

Expand Down
5 changes: 2 additions & 3 deletions src/main/java/me/itzg/helpers/modrinth/ModrinthApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import me.itzg.helpers.errors.InvalidParameterException;
import me.itzg.helpers.http.FailedRequestException;
import me.itzg.helpers.http.Fetch;
import me.itzg.helpers.http.FileDownloadedHandler;
import me.itzg.helpers.http.SharedFetch;
import me.itzg.helpers.http.SharedFetch.Options;
import me.itzg.helpers.http.UriBuilder;
Expand Down Expand Up @@ -266,10 +265,10 @@ public void close() {
sharedFetch.close();
}

public Mono<Path> downloadFileFromUrl(Path outputFile, URI uri, FileDownloadedHandler fileDownloadedHandler) {
public Mono<Path> downloadFileFromUrl(Path outputFile, URI uri) {
return sharedFetch.fetch(uri)
.toFile(outputFile)
.handleDownloaded(fileDownloadedHandler)
.handleStatus(Fetch.loggingDownloadStatusHandler(log))
.skipExisting(true)
.assemble();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ public class ModrinthHttpPackFetcher implements ModrinthPackFetcher {
@Override
public Mono<FetchedPack> fetchModpack(ModrinthModpackManifest prevManifest) {
return apiClient.downloadFileFromUrl(
destFilePath, modpackUri,
(uri, file, contentSizeBytes) ->
log.info("Downloaded {}", destFilePath)
destFilePath, modpackUri
)
.map(mrPackFile -> new FetchedPack(mrPackFile, "custom", deriveVersionId(), deriveVersionName()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ public Mono<Installation> processModpack(SharedFetch sharedFetch) {
));
}

log.info("Processing modpack files for {} {}", modpackIndex.getName(), modpackIndex.getVersionId());

return processModFiles(modpackIndex)
.collectList()
.map(modFiles ->
Expand Down Expand Up @@ -168,9 +170,7 @@ private Flux<Path> processModFiles(ModpackIndex modpackIndex) {

return this.apiClient.downloadFileFromUrl(
outFilePath,
modpackFile.getDownloads().get(0),
(uri, file, contentSizeBytes) ->
log.info("Downloaded {}", modpackFilePath)
modpackFile.getDownloads().get(0)
);
}, maxConcurrentDownloads);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ void testInstallUsingVersions_onlyGameVersion(WireMockRuntimeInfo wmRuntimeInfo)
final FabricLauncherInstaller installer = new FabricLauncherInstaller(
tempDir
)
.setSkipValidation(true)
.setResultsFile(resultsFile);
installer.setFabricMetaBaseUrl(wmRuntimeInfo.getHttpBaseUrl());

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

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

Expand Down Expand Up @@ -142,7 +144,8 @@ void testWithProvidedUri_contentDisposition(WireMockRuntimeInfo wmRuntimeInfo) t
)
);

final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir);
final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir)
.setSkipValidation(true);
installer.installUsingUri(
Options.builder().build(), URI.create(wmRuntimeInfo.getHttpBaseUrl() + "/server")
);
Expand All @@ -157,6 +160,7 @@ void testWithProvidedUri_contentDisposition(WireMockRuntimeInfo wmRuntimeInfo) t
void testWithLocalLauncherFile() throws IOException {
final Path expectedResultsPath = tempDir.resolve("results.env");
final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir)
.setSkipValidation(true)
.setResultsFile(expectedResultsPath);

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

final FabricLauncherInstaller installer = new FabricLauncherInstaller(
tempDir
);
)
.setSkipValidation(true);
installer.setFabricMetaBaseUrl(wmRuntimeInfo.getHttpBaseUrl());

installer.installUsingVersions(
Expand Down Expand Up @@ -213,7 +218,8 @@ void testNoNetworkUsageWhenVersionMatches(WireMockRuntimeInfo wmRuntimeInfo) {

final FabricLauncherInstaller installer = new FabricLauncherInstaller(
tempDir
);
)
.setSkipValidation(true);
installer.setFabricMetaBaseUrl(wmRuntimeInfo.getHttpBaseUrl());

installer.installUsingVersions(
Expand Down Expand Up @@ -255,6 +261,7 @@ void testNoNetworkUsageWhenVersionMatches(WireMockRuntimeInfo wmRuntimeInfo) {
void forRecordingVersionDiscovery() {
final Path resultsFile = tempDir.resolve("results.env");
final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir)
.setSkipValidation(true)
.setResultsFile(resultsFile);
installer.setFabricMetaBaseUrl("http://localhost:8080");

Expand Down