diff --git a/LaunchServer/src/main/resources/pro/gravit/launchserver/defaults/proguard.cfg b/LaunchServer/src/main/resources/pro/gravit/launchserver/defaults/proguard.cfg index d3f335ad..23797bd8 100644 --- a/LaunchServer/src/main/resources/pro/gravit/launchserver/defaults/proguard.cfg +++ b/LaunchServer/src/main/resources/pro/gravit/launchserver/defaults/proguard.cfg @@ -9,7 +9,6 @@ -target 8 -forceprocessing --overloadaggressively -repackageclasses 'pro.gravit.launcher' -keepattributes SourceFile,LineNumberTable,*Annotation* -renamesourcefileattribute SourceFile diff --git a/Launcher/src/main/java/pro/gravit/launcher/LauncherEngine.java b/Launcher/src/main/java/pro/gravit/launcher/LauncherEngine.java index 2759dc37..aaaaff5b 100644 --- a/Launcher/src/main/java/pro/gravit/launcher/LauncherEngine.java +++ b/Launcher/src/main/java/pro/gravit/launcher/LauncherEngine.java @@ -39,6 +39,7 @@ public static X509Certificate[] getCertificates(Class clazz) { } public static final AtomicBoolean IS_CLIENT = new AtomicBoolean(false); + public static ClientLauncherProcess.ClientParams clientParams; public static void checkClass(Class clazz) throws SecurityException { LauncherTrustManager trustManager = Launcher.getConfig().trustManager; diff --git a/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherEntryPoint.java b/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherEntryPoint.java new file mode 100644 index 00000000..12302ff0 --- /dev/null +++ b/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherEntryPoint.java @@ -0,0 +1,261 @@ +package pro.gravit.launcher.client; + +import pro.gravit.launcher.Launcher; +import pro.gravit.launcher.LauncherAgent; +import pro.gravit.launcher.LauncherConfig; +import pro.gravit.launcher.LauncherEngine; +import pro.gravit.launcher.api.AuthService; +import pro.gravit.launcher.api.ClientService; +import pro.gravit.launcher.client.events.ClientLaunchPhase; +import pro.gravit.launcher.client.events.ClientLauncherInitPhase; +import pro.gravit.launcher.client.events.ClientLauncherPostInitPhase; +import pro.gravit.launcher.guard.LauncherGuardManager; +import pro.gravit.launcher.hasher.FileNameMatcher; +import pro.gravit.launcher.hasher.HashedDir; +import pro.gravit.launcher.managers.ClientGsonManager; +import pro.gravit.launcher.modules.events.PreConfigPhase; +import pro.gravit.launcher.patches.FMLPatcher; +import pro.gravit.launcher.profiles.ClientProfile; +import pro.gravit.launcher.request.Request; +import pro.gravit.launcher.request.RequestException; +import pro.gravit.launcher.request.auth.RestoreSessionRequest; +import pro.gravit.launcher.serialize.HInput; +import pro.gravit.launcher.utils.DirWatcher; +import pro.gravit.utils.helper.*; + +import javax.swing.*; +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.net.*; +import java.nio.file.FileVisitResult; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.spec.InvalidKeySpecException; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ClientLauncherEntryPoint { + private ClientLauncherProcess.ClientParams readParams(SocketAddress address) throws IOException { + try (Socket socket = IOHelper.newSocket()) + { + socket.connect(address); + try(HInput input = new HInput(socket.getInputStream())) + { + byte[] serialized = input.readByteArray(0); + ClientLauncherProcess.ClientParams params = Launcher.gsonManager.gson.fromJson(new String(serialized, IOHelper.UNICODE_CHARSET), ClientLauncherProcess.ClientParams.class); + params.clientHDir = new HashedDir(input); + params.assetHDir = new HashedDir(input); + params.javaHDir = new HashedDir(input); + return params; + } + } + } + private static ClientClassLoader classLoader; + public void main(String[] args) throws Throwable { + LauncherEngine.IS_CLIENT.set(true); + LauncherEngine engine = LauncherEngine.clientInstance(); + LauncherEngine.checkClass(LauncherEngine.class); + LauncherEngine.checkClass(LauncherAgent.class); + LauncherEngine.checkClass(ClientLauncher.class); + LauncherEngine.modulesManager = new ClientModuleManager(); + LauncherConfig.initModules(LauncherEngine.modulesManager); //INIT + LauncherEngine.modulesManager.initModules(null); + initGson(LauncherEngine.modulesManager); + LauncherEngine.verifyNoAgent(); + LauncherEngine.modulesManager.invokeEvent(new PreConfigPhase()); + JVMHelper.verifySystemProperties(ClientLauncher.class, true); + EnvHelper.checkDangerousParams(); + JVMHelper.checkStackTrace(ClientLauncher.class); + LogHelper.printVersion("Client Launcher"); + engine.readKeys(); + LauncherGuardManager.initGuard(true); + LogHelper.debug("Reading ClientLauncher params"); + ClientLauncherProcess.ClientParams params = readParams(new InetSocketAddress("127.0.0.1", Launcher.getConfig().clientPort)); + ClientProfile profile = params.profile; + Launcher.profile = profile; + AuthService.profile = profile; + LauncherEngine.clientParams = params; + Request.setSession(params.session); + checkJVMBitsAndVersion(); + LauncherEngine.modulesManager.invokeEvent(new ClientLauncherInitPhase(null)); + + Path clientDir = Paths.get(params.clientDir); + Path assetDir = Paths.get(params.assetDir); + + // Verify ClientLauncher sign and classpath + LogHelper.debug("Verifying ClientLauncher sign and classpath"); + List classpath = new LinkedList<>(); + resolveClassPathStream(clientDir, params.profile.getClassPath()).map(IOHelper::toURL).collect(Collectors.toCollection(() -> classpath)); + + params.profile.pushOptionalClassPath((opt) -> { + resolveClassPathStream(clientDir, opt).map(IOHelper::toURL).collect(Collectors.toCollection(() -> classpath)); + }); + classLoader = new ClientClassLoader(classpath.toArray(new URL[0]), ClassLoader.getSystemClassLoader()); + Thread.currentThread().setContextClassLoader(classLoader); + classLoader.nativePath = clientDir.resolve("natives").toString(); + // Start client with WatchService monitoring + boolean digest = !profile.isUpdateFastCheck(); + LogHelper.debug("Restore sessions"); + RestoreSessionRequest request = new RestoreSessionRequest(Request.getSession()); + request.request(); + Request.service.reconnectCallback = () -> + { + LogHelper.debug("WebSocket connect closed. Try reconnect"); + try { + Request.service.open(); + LogHelper.debug("Connect to %s", Launcher.getConfig().address); + } catch (Exception e) { + LogHelper.error(e); + throw new RequestException(String.format("Connect error: %s", e.getMessage() != null ? e.getMessage() : "null")); + } + try { + RestoreSessionRequest request1 = new RestoreSessionRequest(Request.getSession()); + request1.request(); + } catch (Exception e) { + LogHelper.error(e); + } + }; + AuthService.username = params.playerProfile.username; + AuthService.uuid = params.playerProfile.uuid; + ClientService.classLoader = classLoader; + ClientService.nativePath = classLoader.nativePath; + classLoader.addURL(IOHelper.getCodeSource(ClientLauncher.class).toUri().toURL()); + //classForName(classLoader, "com.google.common.collect.ForwardingMultimap"); + ClientService.baseURLs = classLoader.getURLs(); + LogHelper.debug("Starting JVM and client WatchService"); + FileNameMatcher assetMatcher = profile.getAssetUpdateMatcher(); + FileNameMatcher clientMatcher = profile.getClientUpdateMatcher(); + try (DirWatcher assetWatcher = new DirWatcher(assetDir, params.assetHDir, assetMatcher, digest); + DirWatcher clientWatcher = new DirWatcher(clientDir, params.clientHDir, clientMatcher, digest)) { + // Verify current state of all dirs + //verifyHDir(IOHelper.JVM_DIR, jvmHDir.object, null, digest); + //for (OptionalFile s : Launcher.profile.getOptional()) { + // if (params.updateOptional.contains(s)) s.mark = true; + // else hdir.removeR(s.file); + //} + Launcher.profile.pushOptionalFile(params.clientHDir, false); + // Start WatchService, and only then client + CommonHelper.newThread("Asset Directory Watcher", true, assetWatcher).start(); + CommonHelper.newThread("Client Directory Watcher", true, clientWatcher).start(); + verifyHDir(assetDir, params.assetHDir, assetMatcher, digest); + verifyHDir(clientDir, params.clientHDir, clientMatcher, digest); + launch(profile, params); + } + } + private static void initGson(ClientModuleManager moduleManager) { + Launcher.gsonManager = new ClientGsonManager(moduleManager); + Launcher.gsonManager.initGson(); + } + public static void verifyHDir(Path dir, HashedDir hdir, FileNameMatcher matcher, boolean digest) throws IOException { + //if (matcher != null) + // matcher = matcher.verifyOnly(); + + // Hash directory and compare (ignore update-only matcher entries, it will break offline-mode) + HashedDir currentHDir = new HashedDir(dir, matcher, true, digest); + HashedDir.Diff diff = hdir.diff(currentHDir, matcher); + if (!diff.isSame()) { + /*AtomicBoolean isFoundFile = new AtomicBoolean(false); + diff.extra.walk(File.separator, (e,k,v) -> { + if(v.getType().equals(HashedEntry.Type.FILE)) { LogHelper.error("Extra file %s", e); isFoundFile.set(true); } + else LogHelper.error("Extra %s", e); + }); + diff.mismatch.walk(File.separator, (e,k,v) -> { + if(v.getType().equals(HashedEntry.Type.FILE)) { LogHelper.error("Mismatch file %s", e); isFoundFile.set(true); } + else LogHelper.error("Mismatch %s", e); + }); + if(isFoundFile.get())*/ + throw new SecurityException(String.format("Forbidden modification: '%s'", IOHelper.getFileName(dir))); + } + } + public static void checkJVMBitsAndVersion() { + if (JVMHelper.JVM_BITS != JVMHelper.OS_BITS) { + String error = String.format("У Вас установлена Java %d, но Ваша система определена как %d. Установите Java правильной разрядности", JVMHelper.JVM_BITS, JVMHelper.OS_BITS); + LogHelper.error(error); + if (Launcher.getConfig().isWarningMissArchJava) + JOptionPane.showMessageDialog(null, error); + } + String jvmVersion = JVMHelper.RUNTIME_MXBEAN.getVmVersion(); + LogHelper.info(jvmVersion); + if (jvmVersion.startsWith("10.") || jvmVersion.startsWith("9.") || jvmVersion.startsWith("11.")) { + String error = String.format("У Вас установлена Java %s. Для правильной работы необходима Java 8", JVMHelper.RUNTIME_MXBEAN.getVmVersion()); + LogHelper.error(error); + if (Launcher.getConfig().isWarningMissArchJava) + JOptionPane.showMessageDialog(null, error); + } + } + private static LinkedList resolveClassPathList(Path clientDir, String... classPath) throws IOException { + return resolveClassPathStream(clientDir, classPath).collect(Collectors.toCollection(LinkedList::new)); + } + + private static Stream resolveClassPathStream(Path clientDir, String... classPath) throws IOException { + Stream.Builder builder = Stream.builder(); + for (String classPathEntry : classPath) { + Path path = clientDir.resolve(IOHelper.toPath(classPathEntry.replace(IOHelper.CROSS_SEPARATOR, IOHelper.PLATFORM_SEPARATOR))); + if (IOHelper.isDir(path)) { // Recursive walking and adding + IOHelper.walk(path, new ClassPathFileVisitor(builder), false); + continue; + } + builder.accept(path); + } + return builder.build(); + } + private static final class ClassPathFileVisitor extends SimpleFileVisitor { + private final Stream.Builder result; + + private ClassPathFileVisitor(Stream.Builder result) { + this.result = result; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (IOHelper.hasExtension(file, "jar") || IOHelper.hasExtension(file, "zip")) + result.accept(file); + return super.visitFile(file, attrs); + } + } + private static void launch(ClientProfile profile, ClientLauncherProcess.ClientParams params) throws Throwable { + // Add client args + Collection args = new LinkedList<>(); + if (profile.getVersion().compareTo(ClientProfile.Version.MC164) >= 0) + params.addClientArgs(args); + else { + params.addClientLegacyArgs(args); + System.setProperty("minecraft.applet.TargetDirectory", params.clientDir); + } + Collections.addAll(args, profile.getClientArgs()); + List copy = new ArrayList<>(args); + for (int i = 0, l = copy.size(); i < l; i++) { + String s = copy.get(i); + if (i + 1 < l && ("--accessToken".equals(s) || "--session".equals(s))) { + copy.set(i + 1, "censored"); + } + } + LogHelper.debug("Args: " + copy); + // Resolve main class and method + Class mainClass = classLoader.loadClass(profile.getMainClass()); + for(URL u : classLoader.getURLs()) + { + LogHelper.info("ClassLoader URL: %s", u.toString()); + } + FMLPatcher.apply(); + MethodHandle mainMethod = MethodHandles.publicLookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class)).asFixedArity(); + Launcher.LAUNCHED.set(true); + JVMHelper.fullGC(); + // Invoke main method + try { + mainMethod.invokeWithArguments((Object) args.toArray(new String[0])); + LogHelper.debug("Main exit successful"); + } catch (Throwable e) { + LogHelper.error(e); + throw e; + } finally { + LauncherEngine.exitLauncher(0); + } + + } +} diff --git a/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherProcess.java b/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherProcess.java new file mode 100644 index 00000000..ce35f58a --- /dev/null +++ b/Launcher/src/main/java/pro/gravit/launcher/client/ClientLauncherProcess.java @@ -0,0 +1,257 @@ +package pro.gravit.launcher.client; + +import pro.gravit.launcher.Launcher; +import pro.gravit.launcher.guard.LauncherGuardManager; +import pro.gravit.launcher.hasher.HashedDir; +import pro.gravit.launcher.profiles.ClientProfile; +import pro.gravit.launcher.profiles.PlayerProfile; +import pro.gravit.launcher.request.Request; +import pro.gravit.launcher.serialize.HOutput; +import pro.gravit.utils.Version; +import pro.gravit.utils.helper.EnvHelper; +import pro.gravit.utils.helper.IOHelper; +import pro.gravit.utils.helper.JVMHelper; +import pro.gravit.utils.helper.SecurityHelper; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +public class ClientLauncherProcess { + private transient Process process; + private final transient Boolean[] waitWriteParams = new Boolean[1]; + public final Path executeFile; + public final Path workDir; + public final Path javaDir; + public final ClientParams params = new ClientParams(); + public final List jvmArgs = new LinkedList<>(); + public final List systemClientArgs = new LinkedList<>(); + public final List systemClassPath = new LinkedList<>(); + public final Map systemEnv = new HashMap<>(); + public final String mainClass; + public boolean isStarted; + + public ClientLauncherProcess(Path executeFile, Path workDir, Path javaDir, String mainClass) { + this.executeFile = executeFile; + this.workDir = workDir; + this.javaDir = javaDir; + this.mainClass = mainClass; + } + + public ClientLauncherProcess(Path clientDir, Path assetDir, + ClientProfile profile, PlayerProfile playerProfile, String accessToken, + HashedDir clientHDir, HashedDir assetHDir, HashedDir jvmHDir) { + this(clientDir, assetDir, clientDir.resolve("resourcepacks"), profile, playerProfile, accessToken, clientHDir, assetHDir, jvmHDir); + } + + public ClientLauncherProcess(Path clientDir, Path assetDir, Path resourcePackDir, + ClientProfile profile, PlayerProfile playerProfile, String accessToken, + HashedDir clientHDir, HashedDir assetHDir, HashedDir jvmHDir) { + this.executeFile = LauncherGuardManager.getGuardJavaBinPath(); + this.workDir = clientDir.toAbsolutePath(); + this.javaDir = Paths.get(System.getProperty("java.home")); + this.mainClass = ClientLauncherEntryPoint.class.getName(); + this.params.clientDir = this.workDir.toString(); + this.params.resourcePackDir = resourcePackDir.toAbsolutePath().toString(); + this.params.assetDir = assetDir.toAbsolutePath().toString(); + this.params.profile = profile; + this.params.playerProfile = playerProfile; + this.params.accessToken = accessToken; + this.params.assetHDir = assetHDir; + this.params.clientHDir = clientHDir; + this.params.javaHDir = jvmHDir; + applyClientProfile(); + } + private void applyClientProfile() + { + this.systemClassPath.add(IOHelper.getCodeSource(ClientLauncherEntryPoint.class).toAbsolutePath().toString()); + Collections.addAll(this.jvmArgs, this.params.profile.getJvmArgs()); + this.systemEnv.put("JAVA_HOME", javaDir.toString()); + Collections.addAll(this.systemClassPath, this.params.profile.getAlternativeClassPath()); + if (params.ram > 0) { + this.jvmArgs.add("-Xms" + params.ram + 'M'); + this.jvmArgs.add("-Xmx" + params.ram + 'M'); + } + this.params.session = Request.getSession(); + } + + + public static class ClientParams + { + public String assetDir; + + public String clientDir; + + public String resourcePackDir; + + // Client params + + public PlayerProfile playerProfile; + + public ClientProfile profile; + + public String accessToken; + + //==Minecraft params== + + public boolean autoEnter; + + public boolean fullScreen; + + public int ram; + + public int width; + + public int height; + + //======== + + public long session; + + public transient HashedDir assetHDir; + + public transient HashedDir clientHDir; + + public transient HashedDir javaHDir; + + public void addClientArgs(Collection args) + { + if (profile.getVersion().compareTo(ClientProfile.Version.MC164) >= 0) + addModernClientArgs(args); + else + addClientLegacyArgs(args); + } + + + public void addClientLegacyArgs(Collection args) { + args.add(playerProfile.username); + args.add(accessToken); + + // Add args for tweaker + Collections.addAll(args, "--version", profile.getVersion().name); + Collections.addAll(args, "--gameDir", clientDir.toString()); + Collections.addAll(args, "--assetsDir", assetDir.toString()); + } + + private void addModernClientArgs(Collection args) { + + // Add version-dependent args + ClientProfile.Version version = profile.getVersion(); + Collections.addAll(args, "--username", playerProfile.username); + if (version.compareTo(ClientProfile.Version.MC172) >= 0) { + Collections.addAll(args, "--uuid", Launcher.toHash(playerProfile.uuid)); + Collections.addAll(args, "--accessToken", accessToken); + + // Add 1.7.10+ args (user properties, asset index) + if (version.compareTo(ClientProfile.Version.MC1710) >= 0) { + // Add user properties + Collections.addAll(args, "--userType", "mojang"); + ClientLauncher.ClientUserProperties properties = new ClientLauncher.ClientUserProperties(); + if (playerProfile.skin != null) { + properties.skinURL = new String[]{playerProfile.skin.url}; + properties.skinDigest = new String[]{SecurityHelper.toHex(playerProfile.skin.digest)}; + } + if (playerProfile.cloak != null) { + properties.cloakURL = new String[]{playerProfile.cloak.url}; + properties.cloakDigest = new String[]{SecurityHelper.toHex(playerProfile.cloak.digest)}; + } + Collections.addAll(args, "--userProperties", Launcher.gsonManager.gson.toJson(properties)); + + // Add asset index + Collections.addAll(args, "--assetIndex", profile.getAssetIndex()); + } + } else + Collections.addAll(args, "--session", accessToken); + + // Add version and dirs args + Collections.addAll(args, "--version", profile.getVersion().name); + Collections.addAll(args, "--gameDir", clientDir); + Collections.addAll(args, "--assetsDir", assetDir); + Collections.addAll(args, "--resourcePackDir", resourcePackDir); + if (version.compareTo(ClientProfile.Version.MC194) >= 0) + Collections.addAll(args, "--versionType", "Launcher v" + Version.getVersion().getVersionString()); + + // Add server args + if (autoEnter) { + Collections.addAll(args, "--server", profile.getServerAddress()); + Collections.addAll(args, "--port", Integer.toString(profile.getServerPort())); + } + profile.pushOptionalClientArgs(args); + // Add window size args + if (fullScreen) + Collections.addAll(args, "--fullscreen", Boolean.toString(true)); + if (width > 0 && height > 0) { + Collections.addAll(args, "--width", Integer.toString(width)); + Collections.addAll(args, "--height", Integer.toString(height)); + } + } + } + public void start(boolean pipeOutput) throws IOException, InterruptedException { + if(isStarted) throw new IllegalStateException("Process already started"); + List processArgs = new LinkedList<>(); + processArgs.add(executeFile.toString()); + processArgs.addAll(jvmArgs); + processArgs.add("-cp"); + //ADD CLASSPATH + processArgs.add(String.join(getPathSeparator(), systemClassPath)); + processArgs.addAll(systemClientArgs); + synchronized (waitWriteParams) + { + if(!waitWriteParams[0]) + { + waitWriteParams.wait(1000); + } + } + ProcessBuilder processBuilder = new ProcessBuilder(processArgs); + EnvHelper.addEnv(processBuilder); + processBuilder.environment().putAll(systemEnv); + processBuilder.directory(workDir.toFile()); + processBuilder.inheritIO(); + if (pipeOutput) { + processBuilder.redirectErrorStream(true); + processBuilder.redirectOutput(ProcessBuilder.Redirect.PIPE); + } + process = processBuilder.start(); + isStarted = true; + } + public void runWriteParams(SocketAddress address) throws IOException + { + try(ServerSocket serverSocket = new ServerSocket()) + { + serverSocket.bind(address); + synchronized (waitWriteParams) + { + waitWriteParams[0] = true; + waitWriteParams.notifyAll(); + } + Socket socket = serverSocket.accept(); + try(HOutput output = new HOutput(socket.getOutputStream())) + { + byte[] serializedMainParams = Launcher.gsonManager.gson.toJson(params).getBytes(IOHelper.UNICODE_CHARSET); + output.writeByteArray(serializedMainParams, 0); + params.clientHDir.write(output); + params.assetHDir.write(output); + params.javaHDir.write(output); + } + } + } + + public Process getProcess() { + return process; + } + + public static String getPathSeparator() + { + if(JVMHelper.OS_TYPE == JVMHelper.OS.MUSTDIE) + return ";"; + else + return ":"; + } +} diff --git a/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/ClientProfile.java b/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/ClientProfile.java index b954fab0..8f1ce9a3 100644 --- a/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/ClientProfile.java +++ b/LauncherAPI/src/main/java/pro/gravit/launcher/profiles/ClientProfile.java @@ -128,6 +128,8 @@ public enum ClassLoaderConfig @LauncherNetworkAPI private final List classPath = new ArrayList<>(); @LauncherNetworkAPI + private final List altClassPath = new ArrayList<>(); + @LauncherNetworkAPI private final List clientArgs = new ArrayList<>(); @LauncherNetworkAPI public SecurityManagerConfig securityManagerConfig = SecurityManagerConfig.CLIENT; @@ -154,6 +156,10 @@ public String[] getClassPath() { return classPath.toArray(new String[0]); } + public String[] getAlternativeClassPath() { + return classPath.toArray(new String[0]); + } + public String[] getClientArgs() { return clientArgs.toArray(new String[0]); @@ -196,6 +202,7 @@ public String[] getJvmArgs() { } + public String getMainClass() { return mainClass; }