diff options
author | syeyoung <cyoung06@naver.com> | 2022-11-16 17:45:55 +0900 |
---|---|---|
committer | syeyoung <cyoung06@naver.com> | 2022-11-16 17:59:56 +0900 |
commit | 60032db80aefe4a9f58b354b5a26938ed76f5c46 (patch) | |
tree | 17a7b6e873db2ea3c40aef508134842ce69c5991 /loader/src/main | |
parent | 3708965c0c22c216336f8aa28158ba22bfc03b60 (diff) | |
parent | 241893934ef119566693165589fce0921c35e4af (diff) | |
download | Skyblock-Dungeons-Guide-60032db80aefe4a9f58b354b5a26938ed76f5c46.tar.gz Skyblock-Dungeons-Guide-60032db80aefe4a9f58b354b5a26938ed76f5c46.tar.bz2 Skyblock-Dungeons-Guide-60032db80aefe4a9f58b354b5a26938ed76f5c46.zip |
- Update Downloading and Signature Verification
Signed-off-by: syeyoung <cyoung06@naver.com>
Diffstat (limited to 'loader/src/main')
15 files changed, 681 insertions, 112 deletions
diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/DungeonsGuideReloadListener.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/DungeonsGuideReloadListener.java index 7252a9db..a96805c1 100644 --- a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/DungeonsGuideReloadListener.java +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/DungeonsGuideReloadListener.java @@ -19,6 +19,9 @@ package kr.syeyoung.dungeonsguide.launcher; public interface DungeonsGuideReloadListener { + /** + * @implNote This is very important that you GET RID OF referene to DGInterface when this is called, or else dg is gonna crash with ReferenceLeakedException. + */ public void unloadReference(); public void onLoad(DGInterface dgInterface); } diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/Main.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/Main.java index 0ec65780..e8d3e36b 100755 --- a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/Main.java +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/Main.java @@ -19,7 +19,9 @@ package kr.syeyoung.dungeonsguide.launcher; import kr.syeyoung.dungeonsguide.launcher.auth.AuthManager; +import kr.syeyoung.dungeonsguide.launcher.branch.UpdateRetrieverUtil; import kr.syeyoung.dungeonsguide.launcher.exceptions.NoSuitableLoaderFoundException; +import kr.syeyoung.dungeonsguide.launcher.exceptions.NoVersionFoundException; import kr.syeyoung.dungeonsguide.launcher.exceptions.ReferenceLeakedException; import kr.syeyoung.dungeonsguide.launcher.gui.screen.GuiDisplayer; import kr.syeyoung.dungeonsguide.launcher.gui.screen.GuiLoadingError; @@ -28,6 +30,7 @@ import kr.syeyoung.dungeonsguide.launcher.gui.screen.SpecialGuiScreen; import kr.syeyoung.dungeonsguide.launcher.loader.IDGLoader; import kr.syeyoung.dungeonsguide.launcher.loader.JarLoader; import kr.syeyoung.dungeonsguide.launcher.loader.LocalLoader; +import kr.syeyoung.dungeonsguide.launcher.loader.RemoteLoader; import net.minecraft.client.Minecraft; import net.minecraft.client.resources.IReloadableResourceManager; import net.minecraftforge.common.MinecraftForge; @@ -96,7 +99,7 @@ public class Main } currentLoader = null; } - private void load(IDGLoader newLoader) throws ClassNotFoundException, InstantiationException, IllegalAccessException { + private void load(IDGLoader newLoader) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException { if (dgInterface != null) throw new IllegalStateException("DG is loaded"); dgInterface = newLoader.loadDungeonsGuide(); currentLoader = newLoader; @@ -157,7 +160,19 @@ public class Main return new JarLoader(); } else if (loader.equals("auto") ){ // remote load - throw new UnsupportedOperationException(""); // yet + String branch = System.getProperty("branch") == null ? configuration.get("loader", "remoteBranch", "$default").getString() : System.getProperty("branch"); + String version = System.getProperty("version") == null ? configuration.get("loader", "remoteVersion", "latest").getString() : System.getProperty("version"); + try { + UpdateRetrieverUtil.VersionInfo versionInfo = UpdateRetrieverUtil.getIds( + branch, + version + ); + if (versionInfo == null) throw new NoVersionFoundException(branch, version); + + return new RemoteLoader(versionInfo.getFriendlyBranchName(), versionInfo.getBranchId(), versionInfo.getUpdateId()); + } catch (IOException e) { + throw new NoVersionFoundException(branch, version, e); + } } else { throw new NoSuitableLoaderFoundException(System.getProperty("dg.loader"), configuration.get("loader", "modsource", "auto").getString()); } diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/auth/AuthManager.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/auth/AuthManager.java index 1613e687..c3dd747a 100644 --- a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/auth/AuthManager.java +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/auth/AuthManager.java @@ -3,6 +3,7 @@ package kr.syeyoung.dungeonsguide.launcher.auth; import com.google.common.base.Throwables; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.mojang.authlib.exceptions.AuthenticationException; +import kr.syeyoung.dungeonsguide.launcher.Main; import kr.syeyoung.dungeonsguide.launcher.auth.token.*; import kr.syeyoung.dungeonsguide.launcher.events.AuthChangedEvent; import kr.syeyoung.dungeonsguide.launcher.exceptions.AuthFailedExeption; @@ -34,10 +35,6 @@ public class AuthManager { if(INSTANCE == null) INSTANCE = new AuthManager(); return INSTANCE; } - - @Setter - private String baseserverurl = "https://dungeons.guide"; - private AuthToken currentToken = new NullToken(); public AuthToken getToken() { @@ -98,9 +95,9 @@ public class AuthManager { reauthLock = true; try { - String token = DgAuthUtil.requestAuth(baseserverurl); + String token = DgAuthUtil.requestAuth(); byte[] encSecret = DgAuthUtil.checkSessionAuthenticityAndReturnEncryptedSecret(token); - currentToken = DgAuthUtil.verifyAuth(token, encSecret, baseserverurl); + currentToken = DgAuthUtil.verifyAuth(token, encSecret); MinecraftForge.EVENT_BUS.post(new AuthChangedEvent(currentToken)); if (currentToken instanceof PrivacyPolicyRequiredToken) { @@ -130,7 +127,7 @@ public class AuthManager { if (currentToken instanceof PrivacyPolicyRequiredToken) { reauthLock = true; try { - currentToken = DgAuthUtil.acceptNewPrivacyPolicy(currentToken.getToken(), baseserverurl); + currentToken = DgAuthUtil.acceptNewPrivacyPolicy(currentToken.getToken()); if (currentToken instanceof PrivacyPolicyRequiredToken) throw new PrivacyPolicyRequiredException(); } catch (IOException e) { currentToken = new FailedAuthToken(e); diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/auth/DgAuthUtil.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/auth/DgAuthUtil.java index f585ac20..78fc91c9 100644 --- a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/auth/DgAuthUtil.java +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/auth/DgAuthUtil.java @@ -5,6 +5,7 @@ import com.google.gson.JsonParser; import com.mojang.authlib.GameProfile; import com.mojang.authlib.exceptions.AuthenticationException; import com.mojang.authlib.minecraft.MinecraftSessionService; +import kr.syeyoung.dungeonsguide.launcher.Main; import kr.syeyoung.dungeonsguide.launcher.auth.token.AuthToken; import kr.syeyoung.dungeonsguide.launcher.auth.token.DGAuthToken; import kr.syeyoung.dungeonsguide.launcher.auth.token.PrivacyPolicyRequiredToken; @@ -52,14 +53,16 @@ public class DgAuthUtil { ); } catch (Exception e) { throw new ResponseParsingException(payload, e.getMessage()); + } finally { + toRead.close(); } } - public static String requestAuth(String baseurl) throws IOException { + public static String requestAuth() throws IOException { GameProfile profile = Minecraft.getMinecraft().getSession().getProfile(); - HttpsURLConnection connection = (HttpsURLConnection) new URL(baseurl + "/auth/requestAuth").openConnection(); + HttpsURLConnection connection = (HttpsURLConnection) new URL(Main.DOMAIN + "/auth/requestAuth").openConnection(); connection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestMethod("POST"); @@ -96,8 +99,8 @@ public class DgAuthUtil { return result; } - public static AuthToken verifyAuth(String tempToken, byte[] encSecret, String baseurl) throws IOException { - HttpsURLConnection urlConnection = (HttpsURLConnection) new URL(baseurl + "/auth/authenticate").openConnection(); + public static AuthToken verifyAuth(String tempToken, byte[] encSecret) throws IOException { + HttpsURLConnection urlConnection = (HttpsURLConnection) new URL(Main.DOMAIN + "/auth/authenticate").openConnection(); urlConnection.setRequestMethod("POST"); urlConnection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); urlConnection.setRequestProperty("Content-Type", "application/json"); @@ -120,8 +123,8 @@ public class DgAuthUtil { } } - public static AuthToken acceptNewPrivacyPolicy(String tempToken, String baseurl) throws IOException { - HttpsURLConnection urlConnection = (HttpsURLConnection) new URL(baseurl + "/auth/acceptPrivacyPolicy").openConnection(); + public static AuthToken acceptNewPrivacyPolicy(String tempToken) throws IOException { + HttpsURLConnection urlConnection = (HttpsURLConnection) new URL(Main.DOMAIN + "/auth/acceptPrivacyPolicy").openConnection(); urlConnection.setRequestMethod("POST"); urlConnection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); urlConnection.setRequestProperty("Content-Type", "application/json"); diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/branch/UpdateRetrieverUtil.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/branch/UpdateRetrieverUtil.java new file mode 100644 index 00000000..fe7e54dd --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/branch/UpdateRetrieverUtil.java @@ -0,0 +1,182 @@ +package kr.syeyoung.dungeonsguide.launcher.branch; + +import kr.syeyoung.dungeonsguide.launcher.Main; +import kr.syeyoung.dungeonsguide.launcher.auth.DGResponse; +import kr.syeyoung.dungeonsguide.launcher.exceptions.AssetNotFoundException; +import kr.syeyoung.dungeonsguide.launcher.exceptions.NoVersionFoundException; +import kr.syeyoung.dungeonsguide.launcher.exceptions.ResponseParsingException; +import lombok.Builder; +import lombok.Data; +import org.apache.commons.io.IOUtils; +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.net.ssl.HttpsURLConnection; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URL; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +public class UpdateRetrieverUtil { + private static String getResponse(HttpsURLConnection connection) throws IOException { + connection.getResponseCode(); + InputStream toRead = connection.getErrorStream(); + if (toRead == null) + toRead = connection.getInputStream(); + String payload = IOUtils.readLines(toRead).stream().collect(Collectors.joining("\n")); + return payload; + } + + public static List<UpdateBranch> getUpdateBranches() throws IOException { + HttpsURLConnection connection = (HttpsURLConnection) new URL(Main.DOMAIN + "/updates/").openConnection(); + connection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestMethod("GET"); + connection.setDoInput(true); + connection.setDoOutput(true); + + JSONArray jsonArray = new JSONArray(getResponse(connection)); + return jsonArray.toList() + .stream() + .map(a -> (JSONObject)a) + .map(a -> { + UpdateBranch updateBranch = new UpdateBranch(); + updateBranch.setId(a.getLong("id")); + updateBranch.setName(a.getString("name")); + updateBranch.setMetadata(a.getJSONObject("metadata")); + return updateBranch; + }).collect(Collectors.toList()); + } + + public static List<Update> getLatestUpdates(long branchId, int page) throws IOException { + HttpsURLConnection connection = (HttpsURLConnection) new URL(Main.DOMAIN + "/updates/"+branchId+"/?page="+page).openConnection(); + connection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); + connection.setRequestMethod("GET"); + connection.setDoInput(true); + connection.setDoOutput(true); + + JSONArray jsonArray = new JSONArray(getResponse(connection)); + return jsonArray.toList() + .stream() + .map(a -> (JSONObject)a) + .map(a -> { + Update update = new Update(); + update.setId(a.getLong("id")); + update.setBranchId(a.getLong("branchId")); + update.setName(a.getString("name")); + update.setUpdateLog(a.getString("updateLog")); + update.setMetadata(a.getJSONObject("metadata")); + update.setAssets(a.getJSONObject("assets").getJSONArray("assets").toList().stream().map(b -> (JSONObject)b) + .map(b -> { + Update.Asset asset = new Update.Asset(); + asset.setName(b.getString("name")); + asset.setAssetId(UUID.fromString(b.getString("assetId"))); + asset.setSize(b.getLong("size")); + asset.setObjectId(b.getString("objectId")); + return asset; + }).collect(Collectors.toList())); + update.setReleaseDate(Instant.parse(a.getString("releaseDate"))); + return update; + }).collect(Collectors.toList()); + } + + public static Update getUpdate(long branchId, long updateId) throws IOException { + HttpsURLConnection connection = (HttpsURLConnection) new URL(Main.DOMAIN + "/updates/"+branchId+"/"+updateId).openConnection(); + connection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); + connection.setRequestMethod("GET"); + connection.setDoInput(true); + connection.setDoOutput(true); + + JSONObject a = new JSONObject(getResponse(connection)); + + Update update = new Update(); + update.setId(a.getLong("id")); + update.setBranchId(a.getLong("branchId")); + update.setName(a.getString("name")); + update.setUpdateLog(a.getString("updateLog")); + update.setMetadata(a.getJSONObject("metadata")); + update.setAssets(a.getJSONObject("assets").getJSONArray("assets").toList().stream().map(b -> (JSONObject)b) + .map(b -> { + Update.Asset asset = new Update.Asset(); + asset.setName(b.getString("name")); + asset.setAssetId(UUID.fromString(b.getString("assetId"))); + asset.setSize(b.getLong("size")); + asset.setObjectId(b.getString("objectId")); + return asset; + }).collect(Collectors.toList())); + update.setReleaseDate(Instant.parse(a.getString("releaseDate"))); + return update; + } + + public static InputStream downloadFile(Update update, String assetName) throws IOException { + Update.Asset asset = update.getAssets().stream().filter(a -> a.getName().equals(assetName)) + .findFirst().orElseThrow(() -> new AssetNotFoundException(update.getBranchId()+"", update.getId()+"("+update.getName()+")", assetName)); + + + HttpsURLConnection connection = (HttpsURLConnection) new URL(Main.DOMAIN + "/updates/"+update.getBranchId()+"/"+update.getId()+"/"+asset.getAssetId()).openConnection(); + connection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); + connection.setRequestMethod("GET"); + connection.setDoInput(true); + connection.setDoOutput(true); + + JSONObject result = new JSONObject(getResponse(connection)); + String url = result.getString("url"); + String method = result.getString("method"); + + connection = (HttpsURLConnection) new URL(url).openConnection(); + connection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); + connection.setRequestMethod(method); + return connection.getInputStream(); + } + + @Data @Builder + public static class VersionInfo { + String friendlyBranchName = ""; + long branchId; + String friendlyVersionName = ""; + long updateId; + } + public static VersionInfo getIds(String branch, String version) throws IOException { + long branchId = -1, updateId = -1; + UpdateBranch branch1 = null; + for (UpdateBranch updateBranch : UpdateRetrieverUtil.getUpdateBranches()) { + if (updateBranch.getName().equals(branch) || (branch.equals("$default") && + Optional.ofNullable(updateBranch.getMetadata()) + .map(a -> a.getJSONObject("additionalMeta")) + .map(a -> a.getBoolean("defaultMod")).orElse(false))) { + branchId = updateBranch.getId(); + branch1 = updateBranch; + break; + } + } + if (branchId == -1) return null; + + Update target = null; + int page = 0; + while (updateId == -1) { + List<Update> updateList = UpdateRetrieverUtil.getLatestUpdates(branchId, page++); + if (updateList == null || updateList.isEmpty()) return null; + for (Update update : updateList) { + if (update.getName().equals(version) || version.equals("latest")) { // if latest, get the first one. + updateId = update.getId(); + target = update; + break; + } + } + } + + + return VersionInfo.builder() + .branchId(branchId) + .updateId(updateId) + .friendlyBranchName(branch1.getName()) + .friendlyVersionName(target.getName()) + .build(); + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/AssetNotFoundException.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/AssetNotFoundException.java new file mode 100644 index 00000000..d04608e4 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/AssetNotFoundException.java @@ -0,0 +1,35 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.exceptions; + +import lombok.Getter; + +@Getter +public class AssetNotFoundException extends RuntimeException { + private String branch; + private String version; + private String asset; + + public AssetNotFoundException(String branch, String version, String asset) { + super("No asset found: "+branch+" - "+version+" - "+asset); + this.branch = branch; + this.version = version; + this.asset = asset; + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/InvalidSignatureException.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/InvalidSignatureException.java new file mode 100644 index 00000000..bda43ea4 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/InvalidSignatureException.java @@ -0,0 +1,37 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.exceptions; + +import kr.syeyoung.dungeonsguide.launcher.branch.Update; +import lombok.Getter; + +@Getter +public class InvalidSignatureException extends RuntimeException { + private Update update; + + public InvalidSignatureException(Update update, String message) { + super(message); + this.update = update; + } + + public InvalidSignatureException(Update update, Throwable t) { + super(t); + this.update = update; + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/NoVersionFoundException.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/NoVersionFoundException.java new file mode 100644 index 00000000..b99d12c1 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/NoVersionFoundException.java @@ -0,0 +1,38 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.exceptions; + +import lombok.Getter; + +@Getter +public class NoVersionFoundException extends RuntimeException { + private String branch; + private String version; + + public NoVersionFoundException(String branch, String version) { + super("No version found: "+branch+" - "+version); + this.branch = branch; + this.version = version; + } + public NoVersionFoundException(String branch, String version, Throwable e) { + super("No version found: "+branch+" - "+version, e); + this.branch = branch; + this.version = version; + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/ByteStreamURLHandler.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/ByteStreamURLHandler.java new file mode 100644 index 00000000..d0859076 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/ByteStreamURLHandler.java @@ -0,0 +1,46 @@ +package kr.syeyoung.dungeonsguide.launcher.loader; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +public class ByteStreamURLHandler extends URLStreamHandler { + private InputStreamGenerator converter; + public ByteStreamURLHandler(InputStreamGenerator converter) { + this.converter = converter; + } + public interface InputStreamGenerator { + InputStream convert(String name); + } + + public class ByteStreamURLConnection extends URLConnection { + + /** + * Constructs a URL connection to the specified URL. A connection to + * the object referenced by the URL is not created. + * + * @param url the specified URL. + */ + protected ByteStreamURLConnection(URL url) { + super(url); + connected = false; + } + + @Override + public void connect() throws IOException { + connected = true; + } + + @Override + public InputStream getInputStream() throws IOException { + return converter.convert(url.getPath()); + } + } + + @Override + protected URLConnection openConnection(URL u) throws IOException { + return new ByteStreamURLConnection(u); + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/DGClassLoader.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/DGClassLoader.java new file mode 100644 index 00000000..1fbc39b0 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/DGClassLoader.java @@ -0,0 +1,81 @@ +package kr.syeyoung.dungeonsguide.launcher.loader; + +import sun.misc.Resource; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +public abstract class DGClassLoader extends ClassLoader implements ByteStreamURLHandler.InputStreamGenerator{ + public DGClassLoader(ClassLoader parent) { + super(parent); + } + + public Class<?> loadClass(String name, boolean resolve) + throws ClassNotFoundException + { + synchronized (getClassLoadingLock(name)) { + // First, check if the class has already been loaded + Class<?> c = findLoadedClass(name); + if (c == null) { + try { + if (c == null) { + long t0 = System.nanoTime(); + c = findClass(name); + + sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t0); + sun.misc.PerfCounter.getFindClasses().increment(); + } + } catch (ClassNotFoundException e) { + // ClassNotFoundException thrown if class not found + // from the non-null parent class loader + } + if (getParent() != null && c == null) { + long t0 = System.nanoTime(); + c = getParent().loadClass(name); + long t1 = System.nanoTime(); + sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); + } + } + if (resolve) { + resolveClass(c); + } + return c; + } + } + + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + byte[] res; + try { + res = getClassBytes(name); + } catch (IOException e) { + throw new ClassNotFoundException(name, e); + } + if (res != null) { + return defineClass(name, res, 0, res.length); + } else { + throw new ClassNotFoundException(name); + } + } + + public abstract byte[] getClassBytes(String name) throws IOException; + + public URL getResource(String name) { + URL url = findResource(name); + if (url == null && getParent() != null ) { + url = getParent().getResource(name); + } + return url; + } + + private ByteStreamURLHandler urlHandler = new ByteStreamURLHandler(this); + @Override + public URL findResource(String name) { + try { + return new URL("dungeonsguide", "",0, name, urlHandler); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/IDGLoader.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/IDGLoader.java index f5149f82..a79fdb7e 100644 --- a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/IDGLoader.java +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/IDGLoader.java @@ -21,8 +21,10 @@ package kr.syeyoung.dungeonsguide.launcher.loader; import kr.syeyoung.dungeonsguide.launcher.DGInterface; import kr.syeyoung.dungeonsguide.launcher.exceptions.ReferenceLeakedException; +import java.io.IOException; + public interface IDGLoader { - DGInterface loadDungeonsGuide() throws InstantiationException, IllegalAccessException, ClassNotFoundException; + DGInterface loadDungeonsGuide() throws InstantiationException, IllegalAccessException, ClassNotFoundException, IOException; DGInterface getInstance(); void unloadDungeonsGuide() throws ReferenceLeakedException; diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/JarLoader.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/JarLoader.java index 6baee0f3..fa86054d 100644 --- a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/JarLoader.java +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/JarLoader.java @@ -21,7 +21,12 @@ package kr.syeyoung.dungeonsguide.launcher.loader; import kr.syeyoung.dungeonsguide.launcher.DGInterface; import kr.syeyoung.dungeonsguide.launcher.Main; import kr.syeyoung.dungeonsguide.launcher.exceptions.ReferenceLeakedException; +import org.apache.commons.io.IOUtils; +import sun.misc.Resource; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.lang.ref.PhantomReference; import java.lang.ref.Reference; @@ -29,6 +34,9 @@ import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.net.URL; import java.net.URLClassLoader; +import java.util.HashMap; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; public class JarLoader implements IDGLoader { private DGInterface dgInterface; @@ -37,60 +45,40 @@ public class JarLoader implements IDGLoader { private boolean loaded; - public static class JarClassLoader extends URLClassLoader { - public JarClassLoader(URL[] urls, ClassLoader parent) { - super(urls, parent); - } + public static class JarClassLoader extends DGClassLoader { + public JarClassLoader(ClassLoader parent, ZipInputStream zipInputStream) throws IOException { + super(parent); - @Override - protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { - - synchronized (getClassLoadingLock(name)) { - Class<?> c = findLoadedClass(name); - if (c == null) { - - try { - if (c == null) { - long t0 = System.nanoTime(); - c = findClass(name); - - sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t0); - sun.misc.PerfCounter.getFindClasses().increment(); - } - } catch (ClassNotFoundException e) { - // ClassNotFoundException thrown if class not found - // from the non-null parent class loader - } - if (getParent() != null && c == null) { - long t0 = System.nanoTime(); - c = getParent().loadClass(name); - long t1 = System.nanoTime(); - sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); - } - } - if (resolve) { - resolveClass(c); - } - return c; + ZipEntry zipEntry; + while ((zipEntry=zipInputStream.getNextEntry()) != null) { + this.loadedResources.put(zipEntry.getName(), IOUtils.toByteArray(zipInputStream)); } + + zipInputStream.close(); + } + private final HashMap<String, byte[]> loadedResources = new HashMap<String, byte[]>(); + @Override + public byte[] getClassBytes(String name) throws IOException { // . separated. + return this.loadedResources.get(name.replace(".", "/")); } - public Class<?> loadClassResolve(String name, boolean resolve) throws ClassNotFoundException { - return this.loadClass(name, resolve); + @Override + public InputStream convert(String name) { // / separated + if (this.loadedResources.containsKey(name.substring(1))) + return new ByteArrayInputStream(this.loadedResources.get(name.substring(1))); + return null; } } private JarClassLoader classLoader; @Override - public DGInterface loadDungeonsGuide() throws ClassNotFoundException, InstantiationException, IllegalAccessException { + public DGInterface loadDungeonsGuide() throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException { if (dgInterface != null) throw new IllegalStateException("Already loaded"); - classLoader = new JarClassLoader(new URL[] { - Main.class.getResource("/mod.jar") - }, this.getClass().getClassLoader()); + classLoader = new JarClassLoader(this.getClass().getClassLoader(), new ZipInputStream(JarLoader.class.getResourceAsStream("/mod.jar"))); - dgInterface = (DGInterface) classLoader.loadClassResolve("kr.syeyoung.dungeonsguide.DungeonsGuide", true).newInstance(); + dgInterface = (DGInterface) classLoader.loadClass("kr.syeyoung.dungeonsguide.DungeonsGuide", true).newInstance(); phantomReference = new PhantomReference<>(classLoader, refQueue); return dgInterface; } diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/LocalLoader.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/LocalLoader.java index ef9b3e7a..683f77d0 100644 --- a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/LocalLoader.java +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/LocalLoader.java @@ -20,16 +20,54 @@ package kr.syeyoung.dungeonsguide.launcher.loader; import kr.syeyoung.dungeonsguide.launcher.DGInterface; import kr.syeyoung.dungeonsguide.launcher.exceptions.ReferenceLeakedException; +import org.apache.commons.io.IOUtils; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; +import java.lang.ref.PhantomReference; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.util.HashMap; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; public class LocalLoader implements IDGLoader { private DGInterface dgInterface; + private ReferenceQueue<ClassLoader> refQueue = new ReferenceQueue<>(); + private PhantomReference<ClassLoader> phantomReference; + + private boolean loaded; + + public static class LocalClassLoader extends DGClassLoader { + public LocalClassLoader(ClassLoader parent) throws IOException { + super(parent); + } + @Override + public byte[] getClassBytes(String name) throws IOException { // . separated. + InputStream in = convert("/"+name.replace(".", "/")); + if (in == null) return null; + return IOUtils.toByteArray(in); + } + + @Override + public InputStream convert(String name) { // / separated + return LocalLoader.class.getResourceAsStream(name); + } + } + + private LocalClassLoader classLoader; + @Override - public DGInterface loadDungeonsGuide() throws ClassNotFoundException, InstantiationException, IllegalAccessException { + public DGInterface loadDungeonsGuide() throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException { if (dgInterface != null) throw new IllegalStateException("Already loaded"); - return dgInterface = (DGInterface) Class.forName("kr.syeyoung.dungeonsguide.DungeonsGuide").newInstance(); + + classLoader = new LocalClassLoader(this.getClass().getClassLoader()); + + dgInterface = (DGInterface) classLoader.loadClass("kr.syeyoung.dungeonsguide.DungeonsGuide", true).newInstance(); + phantomReference = new PhantomReference<>(classLoader, refQueue); + return dgInterface; } @Override @@ -39,11 +77,19 @@ public class LocalLoader implements IDGLoader { @Override public void unloadDungeonsGuide() throws ReferenceLeakedException { - throw new UnsupportedOperationException(); + classLoader = null; + dgInterface.unload(); + dgInterface = null; + System.gc();// pls do + Reference<? extends ClassLoader> t = refQueue.poll(); + if (t == null) throw new ReferenceLeakedException(); // Why do you have to be that strict? Well, to tell them to actually listen on DungeonsGuideReloadListener. If it starts causing issues then I will remove check cus it's not really loaded (classes are loaded by child classloader) + t.clear(); + phantomReference = null; } + @Override public boolean isUnloadable() { - return false; + return true; } @Override diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/RemoteLoader.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/RemoteLoader.java index 98155245..719f5cce 100644 --- a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/RemoteLoader.java +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/RemoteLoader.java @@ -1,99 +1,152 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + package kr.syeyoung.dungeonsguide.launcher.loader; import kr.syeyoung.dungeonsguide.launcher.DGInterface; +import kr.syeyoung.dungeonsguide.launcher.branch.Update; +import kr.syeyoung.dungeonsguide.launcher.branch.UpdateBranch; +import kr.syeyoung.dungeonsguide.launcher.branch.UpdateRetrieverUtil; +import kr.syeyoung.dungeonsguide.launcher.exceptions.InvalidSignatureException; +import kr.syeyoung.dungeonsguide.launcher.exceptions.NoVersionFoundException; import kr.syeyoung.dungeonsguide.launcher.exceptions.ReferenceLeakedException; +import org.apache.commons.io.IOUtils; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import java.lang.ref.PhantomReference; +import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; -import java.net.URL; -import java.net.URLClassLoader; +import java.util.HashMap; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; public class RemoteLoader implements IDGLoader { - private String branch; - private String version; - - + private DGInterface dgInterface; private ReferenceQueue<ClassLoader> refQueue = new ReferenceQueue<>(); private PhantomReference<ClassLoader> phantomReference; private boolean loaded; - public static class JarClassLoader extends URLClassLoader { - public JarClassLoader(URL[] urls, ClassLoader parent) { - super(urls, parent); - } + public RemoteLoader(String friendlyBranchName, long branchId, long updateId) { + this.friendlyBranchName = friendlyBranchName; + this.branchId = branchId; + this.updateId = updateId; + } - @Override - protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { - - synchronized (getClassLoadingLock(name)) { - Class<?> c = findLoadedClass(name); - if (c == null) { - - try { - if (c == null) { - long t0 = System.nanoTime(); - c = findClass(name); - - sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t0); - sun.misc.PerfCounter.getFindClasses().increment(); - } - } catch (ClassNotFoundException e) { - // ClassNotFoundException thrown if class not found - // from the non-null parent class loader - } - if (getParent() != null && c == null) { - long t0 = System.nanoTime(); - c = getParent().loadClass(name); - long t1 = System.nanoTime(); - sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); - } - } - if (resolve) { - resolveClass(c); - } - return c; + + public static class JarClassLoader extends DGClassLoader { + public JarClassLoader(ClassLoader parent, ZipInputStream zipInputStream) throws IOException { + super(parent); + + ZipEntry zipEntry; + while ((zipEntry=zipInputStream.getNextEntry()) != null) { + this.loadedResources.put(zipEntry.getName(), IOUtils.toByteArray(zipInputStream)); } + + zipInputStream.close(); + } + private final HashMap<String, byte[]> loadedResources = new HashMap<String, byte[]>(); + @Override + public byte[] getClassBytes(String name) throws IOException { // . separated. + return this.loadedResources.get(name.replace(".", "/")); } - public Class<?> loadClassResolve(String name, boolean resolve) throws ClassNotFoundException { - return this.loadClass(name, resolve); + @Override + public InputStream convert(String name) { // / separated + if (this.loadedResources.containsKey(name.substring(1))) + return new ByteArrayInputStream(this.loadedResources.get(name.substring(1))); + return null; } } - private JarLoader.JarClassLoader classLoader; + private JarClassLoader classLoader; + + + @Override - public DGInterface loadDungeonsGuide() throws InstantiationException, IllegalAccessException, ClassNotFoundException { - return null; + public DGInterface loadDungeonsGuide() throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException { + if (dgInterface != null) throw new IllegalStateException("Already loaded"); + + Update target = UpdateRetrieverUtil.getUpdate(branchId, updateId); + friendlyVersionName = target.getName(); + + InputStream in; + byte[] mod = IOUtils.toByteArray(in = UpdateRetrieverUtil.downloadFile(target, "mod.jar")); + in.close(); + byte[] signature =IOUtils.toByteArray(in = UpdateRetrieverUtil.downloadFile(target, "signature.asc")); + in.close(); + int version = target.getMetadata().getInt("signatureVersion"); + + if (version == 0) { + SignatureValidator.validateVersion1Signature(target, mod, signature); + } else { + throw new InvalidSignatureException(target, "Invalid Signature Version: "+version); + } + + classLoader = new JarClassLoader(this.getClass().getClassLoader(), new ZipInputStream(new ByteArrayInputStream(mod))); + + dgInterface = (DGInterface) classLoader.loadClass("kr.syeyoung.dungeonsguide.DungeonsGuide", true).newInstance(); + phantomReference = new PhantomReference<>(classLoader, refQueue); + return dgInterface; } @Override public DGInterface getInstance() { - return null; + return dgInterface; } @Override public void unloadDungeonsGuide() throws ReferenceLeakedException { - + classLoader = null; + dgInterface.unload(); + dgInterface = null; + System.gc();// pls do + Reference<? extends ClassLoader> t = refQueue.poll(); + if (t == null) throw new ReferenceLeakedException(); // Why do you have to be that strict? Well, to tell them to actually listen on DungeonsGuideReloadListener. If it starts causing issues then I will remove check cus it's not really loaded (classes are loaded by child classloader) + t.clear(); + phantomReference = null; } @Override public boolean isUnloadable() { - return false; + return true; } @Override public boolean isLoaded() { - return false; + return dgInterface != null; } @Override public String loaderName() { - return branch; + return "remote"; } + private long branchId = -1; // pre-retrieved + private long updateId = -1; // pre-retrieved + + private String friendlyBranchName = ""; + private String friendlyVersionName = ""; @Override public String version() { - return version; + return friendlyBranchName+"("+branchId+")@"+friendlyVersionName+"("+updateId+")"; // maybe read the thing... } } diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/SignatureValidator.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/SignatureValidator.java new file mode 100644 index 00000000..7a8ce832 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/SignatureValidator.java @@ -0,0 +1,43 @@ +package kr.syeyoung.dungeonsguide.launcher.loader; + +import kr.syeyoung.dungeonsguide.launcher.branch.Update; +import kr.syeyoung.dungeonsguide.launcher.exceptions.InvalidSignatureException; +import org.apache.commons.codec.binary.Base64; + +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +public class SignatureValidator { + private static PublicKey dgPublicKey; + private static PublicKey getDGPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException { + if (dgPublicKey != null) return dgPublicKey; + X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decodeBase64("MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxO89qtwG67jNucQ9Y44c" + + "IUs/B+5BeJPs7G+RG2gfs4/2+tzF/c1FLDc33M7yKw8aKk99vsBUY9Oo8gxxiEPB" + + "JitP/qfon2THp94oM77ZTpHlmFoqbZMcKGZVI8yfvEL4laTM8Hw+qh5poQwtpEbK" + + "Xo47AkxygxJasUnykER2+aSTZ6kWU2D4xiNtFA6lzqN+/oA+NaYfPS0amAvyVlHR" + + "n/8IuGkxb5RrlqVssQstFnxsJuv88qdGSEqlcKq2tLeg9hb8eCnl2OFzvXmgbVER" + + "0JaV+4Z02fVG1IlR3Xo1mSit7yIU6++3usRCjx2yfXpnGGJUW5pe6YETjNew3ax+" + + "FAZ4GePWCdmS7FvBnbbABKo5pE06ZTfDUTCjQlAJQiUgoF6ntMJvQAXPu48Vr8q/" + + "mTcuZWVnI6CDgyE7nNq3WNoq3397sBzxRohMxuqzl3T19zkfPKF05iV2Ju1HQMW5" + + "I119bYrmVD240aGESZc20Sx/9g1BFpNzQbM5PGUlWJ0dhLjl2ge4ip2hHciY3OEY" + + "p2Qy2k+xEdenpKdL+WMRimCQoO9gWe2Tp4NmP5dppDXZgPjXqjZpnGs0Uxs+fXqW" + + "cwlg3MbX3rFl9so/fhVf4p9oXZK3ve7z5D6XSSDRYECvsKIa08WAxJ/U6n204E/4" + + "xUF+3ZgFPdzZGn2PU7SsnOsCAwEAAQ==")); + return dgPublicKey = KeyFactory.getInstance("RSA").generatePublic(spec); + } + + public static void validateVersion1Signature(Update update, byte[] payload, byte[] signature) { + try { + Signature sign = Signature.getInstance("SHA512withRSA"); + sign.initVerify(getDGPublicKey()); + sign.update(payload); + boolean truth = sign.verify(signature); + if (!truth) throw new InvalidSignatureException(update, "DG SIGNATURE FORGED"); + }catch (NoSuchAlgorithmException | InvalidKeySpecException | SignatureException | InvalidKeyException e) { + throw new InvalidSignatureException(update, e); + } + } + + +} |