SignedUrlResponse class

Пакет: com.hypixel.hytale.server.core.update

Файл: com/hypixel/hytale/server/core/update/UpdateService.java

Поля (2)

МодификаторыТипИмя
public static final BuilderCodec<UpdateService.SignedUrlResponse> CODEC
public String url

Исходный код

Показать/скрыть
class="kw">package com.hypixel.hytale.server.core.update;

class="kw">import com.hypixel.hytale.codec.Codec;
class="kw">import com.hypixel.hytale.codec.EmptyExtraInfo;
class="kw">import com.hypixel.hytale.codec.KeyedCodec;
class="kw">import com.hypixel.hytale.codec.builder.BuilderCodec;
class="kw">import com.hypixel.hytale.codec.codecs.array.ArrayCodec;
class="kw">import com.hypixel.hytale.codec.util.RawJsonReader;
class="kw">import com.hypixel.hytale.common.util.java.ManifestUtil;
class="kw">import com.hypixel.hytale.logger.HytaleLogger;
class="kw">import com.hypixel.hytale.server.core.HytaleServer;
class="kw">import com.hypixel.hytale.server.core.HytaleServerConfig;
class="kw">import com.hypixel.hytale.server.core.auth.AuthConfig;
class="kw">import com.hypixel.hytale.server.core.auth.EncryptedAuthCredentialStore;
class="kw">import com.hypixel.hytale.server.core.auth.ServerAuthManager;
class="kw">import com.hypixel.hytale.server.core.config.UpdateConfig;
class="kw">import com.hypixel.hytale.server.core.util.ServiceHttpClientFactory;
class="kw">import com.hypixel.hytale.server.core.util.io.FileUtil;
class="kw">import java.io.IOException;
class="kw">import java.io.InputStream;
class="kw">import java.io.OutputStream;
class="kw">import java.net.URI;
class="kw">import java.net.http.HttpClient;
class="kw">import java.net.http.HttpRequest;
class="kw">import java.net.http.HttpResponse;
class="kw">import java.net.http.HttpClient.Redirect;
class="kw">import java.net.http.HttpResponse.BodyHandlers;
class="kw">import java.nio.file.AccessDeniedException;
class="kw">import java.nio.file.Files;
class="kw">import java.nio.file.Path;
class="kw">import java.nio.file.StandardCopyOption;
class="kw">import java.security.CodeSource;
class="kw">import java.security.MessageDigest;
class="kw">import java.security.NoSuchAlgorithmException;
class="kw">import java.time.Duration;
class="kw">import java.util.HexFormat;
class="kw">import java.util.Locale;
class="kw">import java.util.concurrent.CancellationException;
class="kw">import java.util.concurrent.CompletableFuture;
class="kw">import java.util.jar.Attributes;
class="kw">import java.util.jar.JarFile;
class="kw">import java.util.jar.Manifest;
class="kw">import java.util.logging.Level;
class="kw">import javax.annotation.Nonnull;
class="kw">import javax.annotation.Nullable;

class="kw">public class UpdateService {
   class="kw">private class="kw">static class="kw">final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
   class="kw">private class="kw">static class="kw">final Duration REQUEST_TIMEOUT = Duration.ofSeconds(30L);
   class="kw">private class="kw">static class="kw">final Duration DOWNLOAD_TIMEOUT = Duration.ofMinutes(30L);
   class="kw">private class="kw">static class="kw">final Path STAGING_DIR = Path.of("..").resolve("updater").resolve("staging");
   class="kw">private class="kw">static class="kw">final Path BACKUP_DIR = Path.of("..").resolve("updater").resolve("backup");
   class="kw">private class="kw">final HttpClient httpClient;
   class="kw">private class="kw">final String accountDataUrl = "https://account-data.hytale.com";

   class="kw">public UpdateService() {
      this.httpClient = ServiceHttpClientFactory.newBuilder(REQUEST_TIMEOUT).followRedirects(Redirect.NORMAL).build();
   }

   @Nullable
   class="kw">public CompletableFuture<UpdateService.VersionManifest> checkForUpdate(@Nonnull String var1) {
      class="kw">return CompletableFuture.supplyAsync(
         () -> {
            try {
               ServerAuthManager var2 = ServerAuthManager.getInstance();
               String var3 = var2.getOAuthAccessToken();
               if (var3 == null) {
                  LOGGER.at(Level.WARNING).log("Cannot check for updates - not authenticated");
                  class="kw">return null;
               } else {
                  String var4 = String.format("version/%s.json", var1);
                  String var5 = this.getSignedUrl(var3, var4);
                  if (var5 == null) {
                     LOGGER.at(Level.WARNING).log("Failed to get signed URL for version manifest");
                     class="kw">return null;
                  } else {
                     HttpRequest var6 = HttpRequest.newBuilder()
                        .uri(URI.create(var5))
                        .header("Accept", "application/json")
                        .timeout(REQUEST_TIMEOUT)
                        .GET()
                        .build();
                     HttpResponse var7 = this.httpClient.send(var6, BodyHandlers.ofString());
                     if (var7.statusCode() != 200) {
                        LOGGER.at(Level.WARNING).log("Failed to fetch version manifest: HTTP %d", var7.statusCode());
                        class="kw">return null;
                     } else {
                        UpdateService.VersionManifest var8 = UpdateService.VersionManifest.CODEC
                           .decodeJson(new RawJsonReader(var7.body().toCharArray()), EmptyExtraInfo.EMPTY);
                        if (var8 != null && var8.version != null) {
                           LOGGER.at(Level.INFO).log("Found version: %s", var8.version);
                           class="kw">return var8;
                        } else {
                           LOGGER.at(Level.WARNING).log("Invalid version manifest response");
                           class="kw">return null;
                        }
                     }
                  }
               }
            } catch (IOException var9) {
               LOGGER.at(Level.WARNING).log("IO error checking for updates: %s", var9.getMessage());
               class="kw">return null;
            } catch (InterruptedException var10) {
               Thread.currentThread().interrupt();
               LOGGER.at(Level.WARNING).log("Update check interrupted");
               class="kw">return null;
            } catch (Exception var11) {
               ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var11)).log("Error checking for updates");
               class="kw">return null;
            }
         }
      );
   }

   class="kw">public UpdateService.DownloadTask downloadUpdate(
      @Nonnull UpdateService.VersionManifest var1, @Nonnull Path var2, @Nullable UpdateService.ProgressCallback var3
   ) {
      CompletableFuture var4 = new CompletableFuture<>();
      Thread var5 = new Thread(() -> {
         try {
            boolean var5 = this.performDownload(var1, var2, var3);
            var4.complete(var5);
         } catch (CancellationException var6) {
            var4.completeExceptionally(var6);
         } catch (InterruptedException var7) {
            Thread.currentThread().interrupt();
            var4.completeExceptionally(new CancellationException("Update download interrupted"));
         } catch (Exception var8) {
            ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var8)).log("Error downloading update");
            var4.complete(false);
         }
      }, "UpdateDownload");
      var5.setDaemon(true);
      var5.start();
      class="kw">return new UpdateService.DownloadTask(var4, var5);
   }

   class="kw">private boolean performDownload(@Nonnull UpdateService.VersionManifest var1, @Nonnull Path var2, @Nullable UpdateService.ProgressCallback var3) class="kw">throws IOException, InterruptedException {
      Path var4 = this.downloadAndVerifyZip(var1, var3);
      if (var4 == null) {
         class="kw">return false;
      }

      try {
         if (!clearStagingDir(var2)) {
            LOGGER.at(Level.WARNING).log("Failed to clear staging directory before extraction");
            class="kw">return false;
         }

         Files.createDirectories(var2);
         if (Thread.currentThread().isInterrupted()) {
            throw new CancellationException("Update download cancelled");
         }

         FileUtil.extractZip(var4, var2);
         LOGGER.at(Level.INFO).log("Update %s downloaded and extracted to staging", var1.version);
         class="kw">return true;
      } class="kw">finally {
         Files.deleteIfExists(var4);
      }
   }

   @Nullable
   class="kw">private Path downloadAndVerifyZip(@Nonnull UpdateService.VersionManifest var1, @Nullable UpdateService.ProgressCallback var2) class="kw">throws IOException, InterruptedException {
      ServerAuthManager var3 = ServerAuthManager.getInstance();
      String var4 = var3.getOAuthAccessToken();
      if (var4 == null) {
         LOGGER.at(Level.WARNING).log("Cannot download update - not authenticated");
         class="kw">return null;
      }

      String var5 = this.getSignedUrl(var4, var1.downloadUrl);
      if (var5 == null) {
         LOGGER.at(Level.WARNING).log("Failed to get signed URL for download");
         class="kw">return null;
      }

      HttpRequest var6 = HttpRequest.newBuilder().uri(URI.create(var5)).timeout(DOWNLOAD_TIMEOUT).GET().build();
      Path var7 = Files.createTempFile("hytale-update-", ".zip");
      boolean var8 = false;

      try {
         HttpResponse var9 = this.httpClient.send(var6, BodyHandlers.ofInputStream());
         if (var9.statusCode() != 200) {
            LOGGER.at(Level.WARNING).log("Failed to download update: HTTP %d", var9.statusCode());
            class="kw">return null;
         }

         long var10 = var9.headers().firstValueAsLong("Content-Length").orElse(-1L);

         MessageDigest var12;
         try {
            var12 = MessageDigest.getInstance("SHA-256");
         } catch (NoSuchAlgorithmException var29) {
            LOGGER.at(Level.SEVERE).log("SHA-256 not available - this should never happen");
            class="kw">return null;
         }

         try (
            InputStream var13 = var9.body();
            OutputStream var14 = Files.newOutputStream(var7);
         ) {
            byte[] var15 = new byte[8192];
            long var16 = 0L;

            int var18;
            while ((var18 = var13.read(var15)) != -1) {
               if (Thread.currentThread().isInterrupted()) {
                  throw new CancellationException("Update download cancelled");
               }

               var14.write(var15, 0, var18);
               var12.update(var15, 0, var18);
               var16 += var18;
               if (var2 != null && var10 > 0L) {
                  int var19 = (int)(var16 * 100L / var10);
                  var2.onProgress(var19, var16, var10);
               }
            }
         }

         String var33 = HexFormat.of().formatHex(var12.digest());
         if (var1.sha256 != null && !var1.sha256.equalsIgnoreCase(var33)) {
            LOGGER.at(Level.WARNING).log("Checksum mismatch! Expected: %s, Got: %s", var1.sha256, var33);
            class="kw">return null;
         } else {
            var8 = true;
            class="kw">return var7;
         }
      } class="kw">finally {
         if (!var8) {
            Files.deleteIfExists(var7);
         }
      }
   }

   class="kw">public UpdateService.DownloadTask performBootstrapInstall(@Nonnull UpdateService.VersionManifest var1, @Nullable UpdateService.ProgressCallback var2) {
      CompletableFuture var3 = new CompletableFuture<>();
      Thread var4 = new Thread(() -> {
         try {
            var3.complete(this.performBootstrapInstallSync(var1, var2));
         } catch (CancellationException var5) {
            var3.completeExceptionally(var5);
         } catch (InterruptedException var6) {
            Thread.currentThread().interrupt();
            var3.completeExceptionally(new CancellationException("Bootstrap install interrupted"));
         } catch (Exception var7) {
            ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var7)).log("Error performing bootstrap install");
            var3.complete(false);
         }
      }, "BootstrapInstall");
      var4.setDaemon(true);
      var4.start();
      class="kw">return new UpdateService.DownloadTask(var3, var4);
   }

   class="kw">private boolean performBootstrapInstallSync(@Nonnull UpdateService.VersionManifest var1, @Nullable UpdateService.ProgressCallback var2) class="kw">throws IOException, InterruptedException {
      Path var3 = this.downloadAndVerifyZip(var1, var2);
      if (var3 == null) {
         class="kw">return false;
      }

      try {
         Path var4 = Path.of(".").toAbsolutePath().normalize();
         if (Thread.currentThread().isInterrupted()) {
            throw new CancellationException("Bootstrap install cancelled");
         }

         FileUtil.extractZip(var3, var4);
         if (!System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win")) {
            Path var5 = var4.resolve("start.sh");
            if (Files.exists(var5) && !var5.toFile().setExecutable(true, false)) {
               LOGGER.at(Level.WARNING).log("Failed to set executable bit on %s", var5);
            }
         }

         Path var10 = var4.resolve("Server");
         if (Files.isDirectory(var10)) {
            Path var6 = var4.resolve("auth.enc");
            moveIntoServerDir(var6, var10);
            moveIntoServerDir(EncryptedAuthCredentialStore.keyFilePath(var6), var10);
            moveIntoServerDir(var4.resolve(HytaleServerConfig.PATH), var10);
         }

         deleteRunningJarIfPossible(var4);
         LOGGER.at(Level.INFO).log("Bootstrap install %s extracted to %s", var1.version, var4);
         class="kw">return true;
      } class="kw">finally {
         Files.deleteIfExists(var3);
      }
   }

   class="kw">private class="kw">static void moveIntoServerDir(@Nonnull Path var0, @Nonnull Path var1) {
      if (Files.exists(var0)) {
         Path var2 = var1.resolve(var0.getFileName());

         try {
            Files.move(var0, var2, StandardCopyOption.REPLACE_EXISTING);
            LOGGER.at(Level.INFO).log("Moved %s to %s", var0.getFileName(), var2);
         } catch (IOException var4) {
            ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var4)).log("Failed to move %s into Server/", var0.getFileName());
         }
      }
   }

   class="kw">private class="kw">static void deleteRunningJarIfPossible(@Nonnull Path var0) {
      if (ManifestUtil.isJar()) {
         try {
            CodeSource var1 = UpdateService.class.getProtectionDomain().getCodeSource();
            if (var1 == null) {
               class="kw">return;
            }

            Path var2 = Path.of(var1.getLocation().toURI());
            if (!var2.getParent().toAbsolutePath().normalize().equals(var0)) {
               LOGGER.at(Level.INFO).log("Installer JAR %s is outside cwd %s - skipping cleanup", var2, var0);
               class="kw">return;
            }

            Files.delete(var2);
            LOGGER.at(Level.INFO).log("Deleted installer JAR %s", var2);
         } catch (AccessDeniedException var3) {
            LOGGER.at(Level.INFO).log("Could not delete installer JAR (Windows locks the running JAR) - delete it manually after shutdown");
         } catch (Exception var4) {
            ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var4)).log("Failed to delete installer JAR");
         }
      }
   }

   @Nullable
   class="kw">private String getSignedUrl(String var1, String var2) class="kw">throws IOException, InterruptedException {
      String var3 = this.accountDataUrl + "/game-assets/" + var2;
      HttpRequest var4 = HttpRequest.newBuilder()
         .uri(URI.create(var3))
         .header("Accept", "application/json")
         .header("Authorization", "Bearer " + var1)
         .header("User-Agent", AuthConfig.USER_AGENT)
         .timeout(REQUEST_TIMEOUT)
         .GET()
         .build();
      HttpResponse var5 = this.httpClient.send(var4, BodyHandlers.ofString());
      if (var5.statusCode() != 200) {
         LOGGER.at(Level.WARNING).log("Failed to get signed URL: HTTP %d - %s", var5.statusCode(), var5.body());
         class="kw">return null;
      } else {
         UpdateService.SignedUrlResponse var6 = UpdateService.SignedUrlResponse.CODEC
            .decodeJson(new RawJsonReader(var5.body().toCharArray()), EmptyExtraInfo.EMPTY);
         class="kw">return var6 != null ? var6.url : null;
      }
   }

   @Nonnull
   class="kw">public class="kw">static String getEffectivePatchline() {
      UpdateConfig var0 = HytaleServer.get().getConfig().getUpdateConfig();
      String var1 = var0.getPatchline();
      if (var1 != null && !var1.isEmpty()) {
         class="kw">return var1;
      }

      var1 = ManifestUtil.getPatchline();
      class="kw">return var1 != null ? var1 : "release";
   }

   class="kw">public class="kw">static boolean isValidUpdateLayout() {
      Path var0 = Path.of("..").toAbsolutePath();
      class="kw">return Files.exists(var0.resolve("Assets.zip")) && (Files.exists(var0.resolve("start.sh")) || Files.exists(var0.resolve("start.bat")));
   }

   @Nonnull
   class="kw">public class="kw">static Path getStagingDir() {
      class="kw">return STAGING_DIR;
   }

   @Nonnull
   class="kw">public class="kw">static Path getBackupDir() {
      class="kw">return BACKUP_DIR;
   }

   @Nullable
   class="kw">public class="kw">static String getStagedVersion() {
      Path var0 = STAGING_DIR.resolve("Server").resolve("HytaleServer.jar");
      class="kw">return !Files.exists(var0) ? null : readVersionFromJar(var0);
   }

   class="kw">public class="kw">static boolean deleteStagedUpdate() {
      class="kw">return safeDeleteUpdaterDir(STAGING_DIR, "staging");
   }

   class="kw">public class="kw">static boolean deleteBackupDir() {
      class="kw">return safeDeleteUpdaterDir(BACKUP_DIR, "backup");
   }

   class="kw">private class="kw">static boolean clearStagingDir(@Nonnull Path var0) {
      if (!Files.exists(var0)) {
         class="kw">return true;
      }

      if (var0.toAbsolutePath().normalize().equals(STAGING_DIR.toAbsolutePath().normalize())) {
         class="kw">return deleteStagedUpdate();
      }

      try {
         FileUtil.deleteDirectory(var0);
         class="kw">return true;
      } catch (IOException var2) {
         LOGGER.at(Level.WARNING).log("Failed to delete staging dir %s: %s", var0, var2.getMessage());
         class="kw">return false;
      }
   }

   class="kw">private class="kw">static boolean safeDeleteUpdaterDir(Path var0, String var1) {
      try {
         if (!Files.exists(var0)) {
            class="kw">return true;
         } else {
            Path var2 = var0.toAbsolutePath().normalize();
            Path var3 = var2.getParent();
            if (var3 == null || !var3.getFileName().toString().equals("updater")) {
               LOGGER.at(Level.SEVERE).log("Refusing to delete %s - not within updater/ directory", var2);
               class="kw">return false;
            } else if (!var2.getFileName().toString().equals(var1)) {
               LOGGER.at(Level.SEVERE).log("Refusing to delete %s - unexpected directory name", var2);
               class="kw">return false;
            } else {
               FileUtil.deleteDirectory(var0);
               class="kw">return true;
            }
         }
      } catch (IOException var4) {
         LOGGER.at(Level.WARNING).log("Failed to delete %s: %s", var0, var4.getMessage());
         class="kw">return false;
      }
   }

   @Nullable
   class="kw">public class="kw">static String readVersionFromJar(@Nonnull Path var0) {
      try (JarFile var1 = new JarFile(var0.toFile())) {
         Manifest var2 = var1.getManifest();
         if (var2 == null) {
            class="kw">return null;
         }

         Attributes var3 = var2.getMainAttributes();
         String var4 = var3.getValue("Implementation-Vendor-Id");
         class="kw">return !"com.hypixel.hytale".equals(var4) ? null : var3.getValue("Implementation-Version");
      } catch (IOException var8) {
         LOGGER.at(Level.WARNING).log("Failed to read version from JAR: %s", var8.getMessage());
         class="kw">return null;
      }
   }

   @Nullable
   class="kw">public UpdateService.PatchlineSummary[] fetchPatchlines(@Nonnull String var1) class="kw">throws IOException, InterruptedException {
      String var2 = this.accountDataUrl + "/my-account/get-patchlines";
      HttpRequest var3 = HttpRequest.newBuilder()
         .uri(URI.create(var2))
         .header("Accept", "application/json")
         .header("Authorization", "Bearer " + var1)
         .header("User-Agent", AuthConfig.USER_AGENT)
         .timeout(REQUEST_TIMEOUT)
         .GET()
         .build();
      HttpResponse var4 = this.httpClient.send(var3, BodyHandlers.ofString());
      if (var4.statusCode() != 200) {
         LOGGER.at(Level.WARNING).log("Failed to fetch patchlines: HTTP %d", var4.statusCode());
         class="kw">return null;
      } else {
         UpdateService.GetPatchlinesResponse var5 = UpdateService.GetPatchlinesResponse.CODEC
            .decodeJson(new RawJsonReader(var4.body().toCharArray()), EmptyExtraInfo.EMPTY);
         class="kw">return var5 != null ? var5.patchlines : null;
      }
   }

   class="kw">private class="kw">static <T> KeyedCodec<T> externalKey(String var0, Codec<T> var1) {
      class="kw">return new KeyedCodec<>(var0, var1, false, true);
   }

   class="kw">public record DownloadTask(CompletableFuture<Boolean> future, Thread thread) {
      class="kw">public DownloadTask {
      }
   }

   class="kw">public class="kw">static class GetPatchlinesResponse {
      class="kw">public UpdateService.PatchlineSummary[] patchlines;
      class="kw">public class="kw">static class="kw">final BuilderCodec<UpdateService.GetPatchlinesResponse> CODEC = BuilderCodec.builder(
            UpdateService.GetPatchlinesResponse.class, UpdateService.GetPatchlinesResponse::new
         )
         .append(
            UpdateService.externalKey("patchlines", new ArrayCodec<>(UpdateService.PatchlineSummary.CODEC, UpdateService.PatchlineSummary[]::new)),
            (var0, var1) -> var0.patchlines = var1,
            var0 -> var0.patchlines
         )
         .add()
         .build();

      class="kw">public GetPatchlinesResponse() {
      }
   }

   class="kw">public class="kw">static class PatchlineSummary {
      class="kw">public String name;
      class="kw">public long expiresAt;
      class="kw">public class="kw">static class="kw">final BuilderCodec<UpdateService.PatchlineSummary> CODEC = BuilderCodec.builder(
            UpdateService.PatchlineSummary.class, UpdateService.PatchlineSummary::new
         )
         .append(UpdateService.externalKey("name", Codec.STRING), (var0, var1) -> var0.name = var1, var0 -> var0.name)
         .add()
         .append(UpdateService.externalKey("expiresAt", Codec.LONG), (var0, var1) -> var0.expiresAt = var1, var0 -> var0.expiresAt)
         .add()
         .build();

      class="kw">public PatchlineSummary() {
      }
   }

   @FunctionalInterface
   class="kw">public class="kw">interface ProgressCallback {
      void onProgress(int var1, long var2, long var4);
   }

   class="kw">private class="kw">static class SignedUrlResponse {
      class="kw">public String url;
      class="kw">public class="kw">static class="kw">final BuilderCodec<UpdateService.SignedUrlResponse> CODEC = BuilderCodec.builder(
            UpdateService.SignedUrlResponse.class, UpdateService.SignedUrlResponse::new
         )
         .append(UpdateService.externalKey("url", Codec.STRING), (var0, var1) -> var0.url = var1, var0 -> var0.url)
         .add()
         .build();

      class="kw">private SignedUrlResponse() {
      }
   }

   class="kw">public class="kw">static class VersionManifest {
      class="kw">public String version;
      class="kw">public String downloadUrl;
      class="kw">public String sha256;
      class="kw">public class="kw">static class="kw">final BuilderCodec<UpdateService.VersionManifest> CODEC = BuilderCodec.builder(
            UpdateService.VersionManifest.class, UpdateService.VersionManifest::new
         )
         .append(UpdateService.externalKey("version", Codec.STRING), (var0, var1) -> var0.version = var1, var0 -> var0.version)
         .add()
         .append(UpdateService.externalKey("download_url", Codec.STRING), (var0, var1) -> var0.downloadUrl = var1, var0 -> var0.downloadUrl)
         .add()
         .append(UpdateService.externalKey("sha256", Codec.STRING), (var0, var1) -> var0.sha256 = var1, var0 -> var0.sha256)
         .add()
         .build();

      class="kw">public VersionManifest() {
      }
   }
}