From fe7ae41f65def06c902c81ad13a874179164f379 Mon Sep 17 00:00:00 2001 From: Gravita <12893402+gravit0@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:18:47 +0700 Subject: [PATCH] [FEATURE] Remove deprecated http methods, fix download --- LaunchServer/build.gradle | 1 + .../auth/texture/JsonTextureProvider.java | 3 - .../gravit/launchserver/command/Command.java | 7 +- .../command/hash/DownloadAssetCommand.java | 7 +- .../launchserver/manangers/MirrorManager.java | 37 ++- Launcher/build.gradle | 5 +- .../launcher/utils/LauncherUpdater.java | 5 +- .../pro/gravit/launcher/AsyncDownloader.java | 213 ------------------ .../java/pro/gravit/launcher/HTTPRequest.java | 57 ----- .../java/pro/gravit/utils/Downloader.java | 62 ----- .../java/pro/gravit/utils/HttpDownloader.java | 76 ------- .../pro/gravit/launcher/HTTPRequest.java | 60 ----- LauncherModernCore/build.gradle | 72 ++++++ .../gravit/launcher/modern}/Downloader.java | 127 +++++++++-- modules | 2 +- settings.gradle | 1 + 16 files changed, 229 insertions(+), 506 deletions(-) delete mode 100644 LauncherCore/src/main/java/pro/gravit/launcher/AsyncDownloader.java delete mode 100644 LauncherCore/src/main/java/pro/gravit/launcher/HTTPRequest.java delete mode 100644 LauncherCore/src/main/java/pro/gravit/utils/Downloader.java delete mode 100644 LauncherCore/src/main/java/pro/gravit/utils/HttpDownloader.java delete mode 100644 LauncherCore/src/main/java11/pro/gravit/launcher/HTTPRequest.java create mode 100644 LauncherModernCore/build.gradle rename {LauncherCore/src/main/java11/pro/gravit/utils => LauncherModernCore/src/main/java/pro/gravit/launcher/modern}/Downloader.java (63%) diff --git a/LaunchServer/build.gradle b/LaunchServer/build.gradle index ac658662..b522f651 100644 --- a/LaunchServer/build.gradle +++ b/LaunchServer/build.gradle @@ -75,6 +75,7 @@ dependencies { pack project(':LauncherAPI') + pack project(':LauncherModernCore') bundle group: 'me.tongfei', name: 'progressbar', version: '0.9.2' bundle group: 'com.github.Marcono1234', name: 'gson-record-type-adapter-factory', version: 'v0.2.0' bundle group: 'org.fusesource.jansi', name: 'jansi', version: rootProject['verJansi'] diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/texture/JsonTextureProvider.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/texture/JsonTextureProvider.java index 035b3f2f..4f31c526 100644 --- a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/texture/JsonTextureProvider.java +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/texture/JsonTextureProvider.java @@ -3,15 +3,12 @@ import com.google.gson.reflect.TypeToken; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import pro.gravit.launcher.HTTPRequest; -import pro.gravit.launcher.Launcher; import pro.gravit.launcher.profiles.Texture; import pro.gravit.launchserver.HttpRequester; import pro.gravit.utils.helper.SecurityHelper; import java.io.IOException; import java.lang.reflect.Type; -import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.UUID; diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/command/Command.java b/LaunchServer/src/main/java/pro/gravit/launchserver/command/Command.java index d2ddc498..b4f7f5a3 100644 --- a/LaunchServer/src/main/java/pro/gravit/launchserver/command/Command.java +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/command/Command.java @@ -3,11 +3,10 @@ import me.tongfei.progressbar.ProgressBar; import me.tongfei.progressbar.ProgressBarBuilder; import me.tongfei.progressbar.ProgressBarStyle; -import pro.gravit.launcher.AsyncDownloader; import pro.gravit.launcher.Launcher; +import pro.gravit.launcher.modern.Downloader; import pro.gravit.launcher.profiles.ClientProfile; import pro.gravit.launchserver.LaunchServer; -import pro.gravit.utils.Downloader; import pro.gravit.utils.command.CommandException; import java.io.IOException; @@ -45,9 +44,9 @@ protected boolean showApplyDialog(String text) throws IOException { return response.equals("y"); } - protected Downloader downloadWithProgressBar(String taskName, List list, String baseUrl, Path targetDir) throws Exception { + protected Downloader downloadWithProgressBar(String taskName, List list, String baseUrl, Path targetDir) throws Exception { long total = 0; - for (AsyncDownloader.SizedFile file : list) { + for (Downloader.SizedFile file : list) { total += file.size; } long totalFiles = list.size(); diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/command/hash/DownloadAssetCommand.java b/LaunchServer/src/main/java/pro/gravit/launchserver/command/hash/DownloadAssetCommand.java index 206bdef0..249ea173 100644 --- a/LaunchServer/src/main/java/pro/gravit/launchserver/command/hash/DownloadAssetCommand.java +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/command/hash/DownloadAssetCommand.java @@ -3,12 +3,11 @@ import com.google.gson.JsonObject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import pro.gravit.launcher.AsyncDownloader; import pro.gravit.launcher.Launcher; +import pro.gravit.launcher.modern.Downloader; import pro.gravit.launchserver.HttpRequester; import pro.gravit.launchserver.LaunchServer; import pro.gravit.launchserver.command.Command; -import pro.gravit.utils.Downloader; import pro.gravit.utils.helper.IOHelper; import java.io.Writer; @@ -85,7 +84,7 @@ public void invoke(String... args) throws Exception { logger.info("Copy {} into {}", indexPath, targetPath); Files.copy(indexPath, targetPath, StandardCopyOption.REPLACE_EXISTING); } - List toDownload = new ArrayList<>(128); + List toDownload = new ArrayList<>(128); for (var e : objects.entrySet()) { var value = e.getValue().getAsJsonObject(); var hash = value.get("hash").getAsString(); @@ -101,7 +100,7 @@ public void invoke(String... args) throws Exception { continue; } } - toDownload.add(new AsyncDownloader.SizedFile(hash, path, size)); + toDownload.add(new Downloader.SizedFile(hash, path, size)); } logger.info("Download {} files", toDownload.size()); Downloader downloader = downloadWithProgressBar(dirName, toDownload, RESOURCES_DOWNLOAD_URL, assetDir); diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/manangers/MirrorManager.java b/LaunchServer/src/main/java/pro/gravit/launchserver/manangers/MirrorManager.java index 98ed7deb..07932e2d 100644 --- a/LaunchServer/src/main/java/pro/gravit/launchserver/manangers/MirrorManager.java +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/manangers/MirrorManager.java @@ -3,20 +3,27 @@ import com.google.gson.JsonElement; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import pro.gravit.launcher.HTTPRequest; -import pro.gravit.utils.HttpDownloader; +import pro.gravit.launcher.Launcher; import pro.gravit.utils.helper.IOHelper; import java.io.IOException; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; public class MirrorManager { protected final ArrayList list = new ArrayList<>(); private transient final Logger logger = LogManager.getLogger(); + private transient final HttpClient client = HttpClient.newBuilder().build(); private Mirror defaultMirror; public void addMirror(String mirror) { @@ -58,7 +65,7 @@ public boolean downloadZip(Mirror mirror, Path path, String mask, Object... args URL url = mirror.getURL(mask, args); logger.debug("Try download {}", url.toString()); try { - HttpDownloader.downloadZip(url, path); + downloadZip(url, path); } catch (IOException e) { logger.error("Download {} failed({}: {})", url.toString(), e.getClass().getName(), e.getMessage()); return false; @@ -82,8 +89,12 @@ public JsonElement jsonRequest(Mirror mirror, JsonElement request, String method if (!mirror.enabled) return null; URL url = mirror.getURL(mask, args); try { - return HTTPRequest.jsonRequest(request, method, url); - } catch (IOException e) { + var response = client.send(HttpRequest.newBuilder() + .method(method, request == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(Launcher.gsonManager.gson.toJson(request))) + .uri(url.toURI()) + .build(), HttpResponse.BodyHandlers.ofString()); + return Launcher.gsonManager.gson.fromJson(response.body(), JsonElement.class); + } catch (IOException | URISyntaxException | InterruptedException e) { logger.error("JsonRequest {} failed({}: {})", url.toString(), e.getClass().getName(), e.getMessage()); return null; } @@ -101,6 +112,22 @@ public JsonElement jsonRequest(JsonElement request, String method, String mask, throw new IOException("Error jsonRequest. All mirrors return error"); } + private void downloadZip(URL url, Path dir) throws IOException { + try (ZipInputStream input = IOHelper.newZipInput(url)) { + Files.createDirectory(dir); + for (ZipEntry entry = input.getNextEntry(); entry != null; entry = input.getNextEntry()) { + if (entry.isDirectory()) { + Files.createDirectory(dir.resolve(IOHelper.toPath(entry.getName()))); + continue; + } + // Unpack entry + String name = entry.getName(); + logger.debug("Downloading file: '{}'", name); + IOHelper.transfer(input, dir.resolve(IOHelper.toPath(name))); + } + } + } + public static class Mirror { final String baseUrl; boolean enabled; diff --git a/Launcher/build.gradle b/Launcher/build.gradle index 03cfe7ae..981b7c41 100644 --- a/Launcher/build.gradle +++ b/Launcher/build.gradle @@ -13,8 +13,8 @@ version = "12" modules = ['javafx.controls', 'javafx.fxml'] } -sourceCompatibility = '1.8' -targetCompatibility = '1.8' +sourceCompatibility = '17' +targetCompatibility = '17' configurations { bundle @@ -49,6 +49,7 @@ dependencies { pack project(':LauncherAPI') + pack project(':LauncherModernCore') pack project(':LauncherClient') pack project(':LauncherStart') bundle group: 'com.github.oshi', name: 'oshi-core', version: rootProject['verOshiCore'] diff --git a/Launcher/src/main/java/pro/gravit/launcher/utils/LauncherUpdater.java b/Launcher/src/main/java/pro/gravit/launcher/utils/LauncherUpdater.java index b4194ab5..f2712a0e 100644 --- a/Launcher/src/main/java/pro/gravit/launcher/utils/LauncherUpdater.java +++ b/Launcher/src/main/java/pro/gravit/launcher/utils/LauncherUpdater.java @@ -1,6 +1,5 @@ package pro.gravit.launcher.utils; -import pro.gravit.launcher.AsyncDownloader; import pro.gravit.launcher.LauncherEngine; import pro.gravit.launcher.LauncherInject; import pro.gravit.launcher.request.update.LauncherRequest; @@ -23,6 +22,8 @@ import java.util.Arrays; import java.util.List; +import static pro.gravit.launcher.modern.Downloader.makeSSLSocketFactory; + public class LauncherUpdater { @LauncherInject("launcher.certificatePinning") private static boolean isCertificatePinning; @@ -49,7 +50,7 @@ public static Path prepareUpdate(URL url) throws Exception { if (isCertificatePinning) { HttpsURLConnection connection1 = (HttpsURLConnection) connection; try { - connection1.setSSLSocketFactory(AsyncDownloader.makeSSLSocketFactory()); + connection1.setSSLSocketFactory(makeSSLSocketFactory()); } catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | KeyManagementException e) { throw new IOException(e); } diff --git a/LauncherCore/src/main/java/pro/gravit/launcher/AsyncDownloader.java b/LauncherCore/src/main/java/pro/gravit/launcher/AsyncDownloader.java deleted file mode 100644 index 2c495419..00000000 --- a/LauncherCore/src/main/java/pro/gravit/launcher/AsyncDownloader.java +++ /dev/null @@ -1,213 +0,0 @@ -package pro.gravit.launcher; - -import pro.gravit.utils.helper.IOHelper; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLConnection; -import java.nio.file.Path; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.cert.CertificateException; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.Executor; - -public class AsyncDownloader { - public static final Callback IGNORE = (ignored) -> { - }; - @LauncherInject("launcher.certificatePinning") - private static boolean isCertificatePinning; - private static volatile SSLSocketFactory sslSocketFactory; - private static volatile SSLContext sslContext; - public final Callback callback; - public volatile boolean isClosed; - - public AsyncDownloader(Callback callback) { - this.callback = callback; - } - - public AsyncDownloader() { - callback = IGNORE; - } - - public static SSLSocketFactory makeSSLSocketFactory() throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, KeyManagementException { - if (sslSocketFactory != null) return sslSocketFactory; - SSLContext sslContext = makeSSLContext(); - sslSocketFactory = sslContext.getSocketFactory(); - return sslSocketFactory; - } - - public static SSLContext makeSSLContext() throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, KeyManagementException { - if (sslContext != null) return sslContext; - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, CertificatePinningTrustManager.getTrustManager().getTrustManagers(), new SecureRandom()); - return sslContext; - } - - public void downloadFile(URL url, Path target, long size) throws IOException { - if (isClosed) throw new IOException("Download interrupted"); - URLConnection connection = url.openConnection(); - if (isCertificatePinning) { - HttpsURLConnection connection1 = (HttpsURLConnection) connection; - try { - connection1.setSSLSocketFactory(makeSSLSocketFactory()); - } catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | KeyManagementException e) { - throw new IOException(e); - } - } - try (InputStream input = connection.getInputStream()) { - transfer(input, target, size); - } - } - - public void downloadFile(URL url, Path target) throws IOException { - URLConnection connection = url.openConnection(); - if (isCertificatePinning) { - HttpsURLConnection connection1 = (HttpsURLConnection) connection; - try { - connection1.setSSLSocketFactory(makeSSLSocketFactory()); - } catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | KeyManagementException e) { - throw new IOException(e); - } - } - try (InputStream input = connection.getInputStream()) { - IOHelper.transfer(input, target); - } - } - - public void downloadListInOneThread(List files, String baseURL, Path targetDir) throws URISyntaxException, IOException { - URI baseUri = new URI(baseURL); - String scheme = baseUri.getScheme(); - String host = baseUri.getHost(); - int port = baseUri.getPort(); - if (port != -1) - host = host + ":" + port; - String path = baseUri.getPath(); - for (AsyncDownloader.SizedFile currentFile : files) { - URL url = new URI(scheme, host, path + currentFile.urlPath, "", "").toURL(); - downloadFile(url, targetDir.resolve(currentFile.filePath), currentFile.size); - } - } - - public void downloadListInOneThreadSimple(List files, String baseURL, Path targetDir) throws IOException { - - for (AsyncDownloader.SizedFile currentFile : files) { - downloadFile(new URL(baseURL + currentFile.urlPath), targetDir.resolve(currentFile.filePath), currentFile.size); - } - } - - public List> sortFiles(List files, int threads) { - files.sort(Comparator.comparingLong((f) -> -f.size)); - List> result = new ArrayList<>(); - for (int i = 0; i < threads; ++i) result.add(new LinkedList<>()); - long[] sizes = new long[threads]; - Arrays.fill(sizes, 0); - for (SizedFile file : files) { - long min = Long.MAX_VALUE; - int minIndex = 0; - for (int i = 0; i < threads; ++i) - if (sizes[i] < min) { - min = sizes[i]; - minIndex = i; - } - result.get(minIndex).add(file); - sizes[minIndex] += file.size; - } - for (List list : result) { - Collections.shuffle(list); - } - return result; - } - - @SuppressWarnings("rawtypes") - public CompletableFuture[] runDownloadList(List> files, String baseURL, Path targetDir, Executor executor) { - int threads = files.size(); - CompletableFuture[] futures = new CompletableFuture[threads]; - for (int i = 0; i < threads; ++i) { - List currentTasks = files.get(i); - futures[i] = CompletableFuture.runAsync(() -> { - try { - downloadListInOneThread(currentTasks, baseURL, targetDir); - } catch (URISyntaxException | IOException e) { - throw new CompletionException(e); - } - }, executor); - } - return futures; - } - - @SuppressWarnings("rawtypes") - public CompletableFuture[] runDownloadListSimple(List> files, String baseURL, Path targetDir, Executor executor) { - int threads = files.size(); - CompletableFuture[] futures = new CompletableFuture[threads]; - for (int i = 0; i < threads; ++i) { - List currentTasks = files.get(i); - futures[i] = CompletableFuture.runAsync(() -> { - try { - downloadListInOneThreadSimple(currentTasks, baseURL, targetDir); - } catch (IOException e) { - throw new CompletionException(e); - } - }, executor); - } - return futures; - } - - public void transfer(InputStream input, Path file, long size) throws IOException { - try (OutputStream fileOutput = IOHelper.newOutput(file)) { - long downloaded = 0L; - - // Download with digest update - byte[] bytes = IOHelper.newBuffer(); - while (downloaded < size) { - if (isClosed) throw new IOException("Download interrupted"); - int remaining = (int) Math.min(size - downloaded, bytes.length); - int length = input.read(bytes, 0, remaining); - if (length < 0) - throw new EOFException(String.format("%d bytes remaining", size - downloaded)); - - // Update file - fileOutput.write(bytes, 0, length); - - // Update state - downloaded += length; - //totalDownloaded += length; - callback.update(length); - } - } - } - - @FunctionalInterface - public interface Callback { - void update(long diff); - } - - public static class SizedFile { - public final String urlPath, filePath; - public final long size; - - public SizedFile(String path, long size) { - this.urlPath = path; - this.filePath = path; - this.size = size; - } - - public SizedFile(String urlPath, String filePath, long size) { - this.urlPath = urlPath; - this.filePath = filePath; - this.size = size; - } - } -} diff --git a/LauncherCore/src/main/java/pro/gravit/launcher/HTTPRequest.java b/LauncherCore/src/main/java/pro/gravit/launcher/HTTPRequest.java deleted file mode 100644 index d33adae1..00000000 --- a/LauncherCore/src/main/java/pro/gravit/launcher/HTTPRequest.java +++ /dev/null @@ -1,57 +0,0 @@ -package pro.gravit.launcher; - -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; -import pro.gravit.utils.helper.LogHelper; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; - -public final class HTTPRequest { - private static final int TIMEOUT = 10000; - - private HTTPRequest() { - } - - public static JsonElement jsonRequest(JsonElement request, URL url) throws IOException { - return jsonRequest(request, "POST", url); - } - - public static JsonElement jsonRequest(JsonElement request, String method, URL url) throws IOException { - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setDoInput(true); - if (request != null) connection.setDoOutput(true); - connection.setRequestMethod(method); - if (request != null) connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); - if (request != null) connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36"); - connection.setRequestProperty("Accept", "application/json"); - if (TIMEOUT > 0) - connection.setConnectTimeout(TIMEOUT); - if (request != null) - try (OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.UTF_8)) { - writer.write(request.toString()); - writer.flush(); - } - - InputStreamReader reader; - int statusCode = connection.getResponseCode(); - - if (200 <= statusCode && statusCode < 300) - reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8); - else - reader = new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8); - try { - return JsonParser.parseReader(reader); - } catch (Exception e) { - if (200 > statusCode || statusCode > 300) { - LogHelper.error("JsonRequest failed. Server response code %d", statusCode); - throw new IOException(e); - } - return null; - } - } -} diff --git a/LauncherCore/src/main/java/pro/gravit/utils/Downloader.java b/LauncherCore/src/main/java/pro/gravit/utils/Downloader.java deleted file mode 100644 index 39cec8f0..00000000 --- a/LauncherCore/src/main/java/pro/gravit/utils/Downloader.java +++ /dev/null @@ -1,62 +0,0 @@ -package pro.gravit.utils; - -import pro.gravit.launcher.AsyncDownloader; -import pro.gravit.utils.helper.LogHelper; - -import java.nio.file.Path; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class Downloader { - private final CompletableFuture future; - private final AsyncDownloader asyncDownloader; - private Downloader(CompletableFuture future, AsyncDownloader downloader) { - this.future = future; - this.asyncDownloader = downloader; - } - - public static Downloader downloadList(List files, String baseURL, Path targetDir, DownloadCallback callback, ExecutorService executor, int threads) { - final boolean closeExecutor; - LogHelper.info("Download with legacy mode"); - if (executor == null) { - executor = Executors.newWorkStealingPool(4); - closeExecutor = true; - } else { - closeExecutor = false; - } - AsyncDownloader asyncDownloader = new AsyncDownloader((diff) -> { - if (callback != null) { - callback.apply(diff); - } - }); - List> list = asyncDownloader.sortFiles(files, threads); - CompletableFuture future = CompletableFuture.allOf(asyncDownloader.runDownloadList(list, baseURL, targetDir, executor)); - - ExecutorService finalExecutor = executor; - return new Downloader(future.thenAccept(e -> { - if (closeExecutor) { - finalExecutor.shutdownNow(); - } - }), asyncDownloader); - } - - public CompletableFuture getFuture() { - return future; - } - - public void cancel() { - this.asyncDownloader.isClosed = true; - } - - public boolean isCanceled() { - return this.asyncDownloader.isClosed; - } - - public interface DownloadCallback { - void apply(long fullDiff); - - void onComplete(Path path); - } -} diff --git a/LauncherCore/src/main/java/pro/gravit/utils/HttpDownloader.java b/LauncherCore/src/main/java/pro/gravit/utils/HttpDownloader.java deleted file mode 100644 index 6a606f8d..00000000 --- a/LauncherCore/src/main/java/pro/gravit/utils/HttpDownloader.java +++ /dev/null @@ -1,76 +0,0 @@ -package pro.gravit.utils; - -import pro.gravit.utils.helper.IOHelper; -import pro.gravit.utils.helper.LogHelper; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -public final class HttpDownloader { - public static final int INTERVAL = 500; - public final AtomicInteger writed; - public final Thread thread; - private volatile String filename; - - public HttpDownloader(URL url, Path file) { - writed = new AtomicInteger(0); - filename = null; - thread = new Thread(() -> { - try { - filename = IOHelper.getFileName(file); - downloadFile(url, file, writed::set); - } catch (IOException e) { - e.printStackTrace(); - } - }); - thread.setDaemon(true); - thread.start(); - } - - public static void downloadFile(URL url, Path file, Consumer chanheTrack) throws IOException { - try (BufferedInputStream in = new BufferedInputStream(url.openStream()); OutputStream fout = IOHelper.newOutput(file, false)) { - - final byte[] data = new byte[IOHelper.BUFFER_SIZE]; - int count; - long timestamp = System.currentTimeMillis(); - int writed_local = 0; - while ((count = in.read(data, 0, IOHelper.BUFFER_SIZE)) != -1) { - fout.write(data, 0, count); - writed_local += count; - if (System.currentTimeMillis() - timestamp > INTERVAL) { - chanheTrack.accept(writed_local); - LogHelper.debug("Downloaded %d", writed_local); - } - } - chanheTrack.accept(writed_local); - } - } - - public static void downloadZip(URL url, Path dir) throws IOException { - try (ZipInputStream input = IOHelper.newZipInput(url)) { - Files.createDirectory(dir); - for (ZipEntry entry = input.getNextEntry(); entry != null; entry = input.getNextEntry()) { - if (entry.isDirectory()) { - Files.createDirectory(dir.resolve(IOHelper.toPath(entry.getName()))); - continue; - } - // Unpack entry - String name = entry.getName(); - LogHelper.subInfo("Downloading file: '%s'", name); - IOHelper.transfer(input, dir.resolve(IOHelper.toPath(name))); - } - } - } - - public String getFilename() { - return filename; - } -} diff --git a/LauncherCore/src/main/java11/pro/gravit/launcher/HTTPRequest.java b/LauncherCore/src/main/java11/pro/gravit/launcher/HTTPRequest.java deleted file mode 100644 index eef69ae4..00000000 --- a/LauncherCore/src/main/java11/pro/gravit/launcher/HTTPRequest.java +++ /dev/null @@ -1,60 +0,0 @@ -package pro.gravit.launcher; - -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; -import pro.gravit.utils.helper.IOHelper; -import pro.gravit.utils.helper.LogHelper; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; - -public final class HTTPRequest { - private static final int TIMEOUT = 10000; - - private HTTPRequest() { - } - - public static JsonElement jsonRequest(JsonElement request, URL url) throws IOException { - return jsonRequest(request, "POST", url); - } - - public static JsonElement jsonRequest(JsonElement request, String method, URL url) throws IOException { - HttpClient client = HttpClient.newBuilder() - .build(); - HttpRequest.BodyPublisher publisher; - if (request != null) { - publisher = HttpRequest.BodyPublishers.ofString(request.toString()); - } else { - publisher = HttpRequest.BodyPublishers.noBody(); - } - try { - HttpRequest request1 = HttpRequest.newBuilder() - .method(method, publisher) - .uri(url.toURI()) - .header("Content-Type", "application/json; charset=UTF-8") - .header("Accept", "application/json") - .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36") - .timeout(Duration.ofMillis(TIMEOUT)) - .build(); - HttpResponse response = client.send(request1, HttpResponse.BodyHandlers.ofInputStream()); - int statusCode = response.statusCode(); - try { - return JsonParser.parseReader(IOHelper.newReader(response.body())); - } catch (Exception e) { - if (200 > statusCode || statusCode > 300) { - LogHelper.error("JsonRequest failed. Server response code %d", statusCode); - throw new IOException(e); - } - return null; - } - } catch (URISyntaxException | InterruptedException e) { - throw new IOException(e); - } - } -} diff --git a/LauncherModernCore/build.gradle b/LauncherModernCore/build.gradle new file mode 100644 index 00000000..879f4a48 --- /dev/null +++ b/LauncherModernCore/build.gradle @@ -0,0 +1,72 @@ +sourceCompatibility = '17' +targetCompatibility = '17' + +dependencies { + api project(':LauncherCore') +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} +jar { + archiveClassifier.set('clean') + manifest.attributes("Multi-Release": "true") +} + +tasks.register('sourcesJar', Jar) { + from sourceSets.main.allJava + archiveClassifier.set('sources') +} + +tasks.register('javadocJar', Jar) { + from javadoc + archiveClassifier.set('javadoc') +} + +publishing { + publications { + launchermoderncore(MavenPublication) { + artifactId = 'launcher-modern-core' + artifact(jar) { + classifier "" + } + artifact sourcesJar + artifact javadocJar + pom { + name = 'GravitLauncher Core Utils with Java 17+' + description = 'GravitLauncher Core Utils' + url = 'https://gravitlauncher.com' + licenses { + license { + name = 'GNU General Public License, Version 3.0' + url = 'https://www.gnu.org/licenses/gpl-3.0.html' + } + } + developers { + developer { + id = 'gravita' + name = 'Gravita' + email = 'gravita@gravit.pro' + } + developer { + id = 'zaxar163' + name = 'Zaxar163' + email = 'zahar.vcherachny@yandex.ru' + } + } + scm { + connection = 'scm:git:https://github.com/GravitLauncher/Launcher.git' + developerConnection = 'scm:git:ssh://git@github.com:GravitLauncher/Launcher.git' + url = 'https://gravitlauncher.com/' + } + } + } + } +} + +signing { + sign publishing.publications.launchermoderncore +} diff --git a/LauncherCore/src/main/java11/pro/gravit/utils/Downloader.java b/LauncherModernCore/src/main/java/pro/gravit/launcher/modern/Downloader.java similarity index 63% rename from LauncherCore/src/main/java11/pro/gravit/utils/Downloader.java rename to LauncherModernCore/src/main/java/pro/gravit/launcher/modern/Downloader.java index daf1dc5e..986c50ad 100644 --- a/LauncherCore/src/main/java11/pro/gravit/utils/Downloader.java +++ b/LauncherModernCore/src/main/java/pro/gravit/launcher/modern/Downloader.java @@ -1,10 +1,13 @@ -package pro.gravit.utils; +package pro.gravit.launcher.modern; -import pro.gravit.launcher.AsyncDownloader; +import pro.gravit.launcher.CertificatePinningTrustManager; import pro.gravit.launcher.LauncherInject; import pro.gravit.utils.helper.IOHelper; import pro.gravit.utils.helper.LogHelper; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; @@ -13,6 +16,11 @@ import java.nio.ByteBuffer; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; import java.util.Collections; import java.util.List; import java.util.Queue; @@ -26,6 +34,8 @@ public class Downloader { private static boolean isCertificatePinning; @LauncherInject("launcher.noHttp2") private static boolean isNoHttp2; + private static volatile SSLSocketFactory sslSocketFactory; + private static volatile SSLContext sslContext; protected final HttpClient client; protected final ExecutorService executor; protected final Queue tasks = new ConcurrentLinkedDeque<>(); @@ -35,7 +45,50 @@ protected Downloader(HttpClient client, ExecutorService executor) { this.executor = executor; } - public static Downloader downloadList(List files, String baseURL, Path targetDir, DownloadCallback callback, ExecutorService executor, int threads) throws Exception { + public static ThreadFactory getDaemonThreadFactory(String name) { + return (task) -> { + Thread thread = new Thread(task); + thread.setName(name); + thread.setDaemon(true); + return thread; + }; + } + + public static SSLSocketFactory makeSSLSocketFactory() throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, KeyManagementException { + if (sslSocketFactory != null) return sslSocketFactory; + SSLContext sslContext = makeSSLContext(); + sslSocketFactory = sslContext.getSocketFactory(); + return sslSocketFactory; + } + + public static SSLContext makeSSLContext() throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, KeyManagementException { + if (sslContext != null) return sslContext; + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, CertificatePinningTrustManager.getTrustManager().getTrustManagers(), new SecureRandom()); + return sslContext; + } + + public static Downloader downloadFile(URI uri, Path path, ExecutorService executor) { + boolean closeExecutor = false; + if (executor == null) { + executor = Executors.newSingleThreadExecutor(getDaemonThreadFactory("Downloader")); + closeExecutor = true; + } + Downloader downloader = newDownloader(executor); + downloader.future = downloader.downloadFile(uri, path); + if (closeExecutor) { + ExecutorService finalExecutor = executor; + downloader.future = downloader.future.thenAccept((e) -> { + finalExecutor.shutdownNow(); + }).exceptionallyCompose((ex) -> { + finalExecutor.shutdownNow(); + return CompletableFuture.failedFuture(ex); + }); + } + return downloader; + } + + public static Downloader downloadList(List files, String baseURL, Path targetDir, DownloadCallback callback, ExecutorService executor, int threads) throws Exception { boolean closeExecutor = false; LogHelper.info("Download with Java 11+ HttpClient"); if (executor == null) { @@ -46,7 +99,12 @@ public static Downloader downloadList(List files, Str downloader.future = downloader.downloadFiles(files, baseURL, targetDir, callback, executor, threads); if (closeExecutor) { ExecutorService finalExecutor = executor; - downloader.future = downloader.future.thenAccept(e -> finalExecutor.shutdownNow()); + downloader.future = downloader.future.thenAccept((e) -> { + finalExecutor.shutdownNow(); + }).exceptionallyCompose((ex) -> { + finalExecutor.shutdownNow(); + return CompletableFuture.failedFuture(ex); + }); } return downloader; } @@ -61,7 +119,7 @@ public static Downloader newDownloader(ExecutorService executor) { .executor(executor); if (isCertificatePinning) { try { - builder.sslContext(AsyncDownloader.makeSSLContext()); + builder.sslContext(makeSSLContext()); } catch (Exception e) { throw new SecurityException(e); } @@ -88,11 +146,23 @@ public CompletableFuture getFuture() { return future; } - public CompletableFuture downloadFiles(List files, String baseURL, Path targetDir, DownloadCallback callback, ExecutorService executor, int threads) throws Exception { + public CompletableFuture downloadFile(URI uri, Path path) { + return client.sendAsync(HttpRequest.newBuilder() + .GET() + .uri(uri) + .build(), HttpResponse.BodyHandlers.ofFile(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)).thenCompose((t) -> { + if(t.statusCode() < 200 || t.statusCode() >= 400) { + return CompletableFuture.failedFuture(new IOException(String.format("Failed to download %s: code %d", uri.toString(), t.statusCode()))); + } + return CompletableFuture.completedFuture(null); + }); + } + + public CompletableFuture downloadFiles(List files, String baseURL, Path targetDir, DownloadCallback callback, ExecutorService executor, int threads) throws Exception { // URI scheme - URI baseUri = new URI(baseURL); + URI baseUri = baseURL == null ? null : new URI(baseURL); Collections.shuffle(files); - Queue queue = new ConcurrentLinkedDeque<>(files); + Queue queue = new ConcurrentLinkedDeque<>(files); CompletableFuture future = new CompletableFuture<>(); AtomicInteger currentThreads = new AtomicInteger(threads); ConsumerObject consumerObject = new ConsumerObject(); @@ -100,7 +170,7 @@ public CompletableFuture downloadFiles(List fil if (callback != null && e != null) { callback.onComplete(e.body()); } - AsyncDownloader.SizedFile file = queue.poll(); + SizedFile file = queue.poll(); if (file == null) { if (currentThreads.decrementAndGet() == 0) future.complete(null); @@ -124,7 +194,7 @@ public CompletableFuture downloadFiles(List fil return future; } - protected DownloadTask sendAsync(AsyncDownloader.SizedFile file, URI baseUri, Path targetDir, DownloadCallback callback) throws Exception { + protected DownloadTask sendAsync(SizedFile file, URI baseUri, Path targetDir, DownloadCallback callback) throws Exception { IOHelper.createParentDirs(targetDir.resolve(file.filePath)); ProgressTrackingBodyHandler bodyHandler = makeBodyHandler(targetDir.resolve(file.filePath), callback); CompletableFuture> future = client.sendAsync(makeHttpRequest(baseUri, file.urlPath), bodyHandler); @@ -139,15 +209,21 @@ protected DownloadTask sendAsync(AsyncDownloader.SizedFile file, URI baseUri, Pa } protected HttpRequest makeHttpRequest(URI baseUri, String filePath) throws URISyntaxException { - String scheme = baseUri.getScheme(); - String host = baseUri.getHost(); - int port = baseUri.getPort(); - if (port != -1) - host = host + ":" + port; - String path = baseUri.getPath(); + URI uri; + if(baseUri != null) { + String scheme = baseUri.getScheme(); + String host = baseUri.getHost(); + int port = baseUri.getPort(); + if (port != -1) + host = host + ":" + port; + String path = baseUri.getPath(); + uri = new URI(scheme, host, path + filePath, "", ""); + } else { + uri = new URI(filePath); + } return HttpRequest.newBuilder() .GET() - .uri(new URI(scheme, host, path + filePath, "", "")) + .uri(uri) .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36") .build(); } @@ -262,4 +338,21 @@ public void cancel() { } } } + + public static class SizedFile { + public final String urlPath, filePath; + public final long size; + + public SizedFile(String path, long size) { + this.urlPath = path; + this.filePath = path; + this.size = size; + } + + public SizedFile(String urlPath, String filePath, long size) { + this.urlPath = urlPath; + this.filePath = filePath; + this.size = size; + } + } } diff --git a/modules b/modules index 1229e6c9..f6bb0936 160000 --- a/modules +++ b/modules @@ -1 +1 @@ -Subproject commit 1229e6c912a187f8f5cd239934faa0586113207e +Subproject commit f6bb09363dc2a1de0f3528b5c9d870e18ca9680e diff --git a/settings.gradle b/settings.gradle index dfdf4434..9566cd8d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ rootProject.name = 'GravitLauncher' include 'Launcher' +include 'LauncherModernCore' include 'LauncherCore' include 'LauncherAPI' include 'LauncherClient'