package launcher.client;
import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.WriterConfig;
import launcher.Launcher;
import launcher.Launcher.Config;
import launcher.LauncherAPI;
import launcher.client.ClientProfile.Version;
import launcher.hasher.DirWatcher;
import launcher.hasher.FileNameMatcher;
import launcher.hasher.HashedDir;
import launcher.helper.*;
import launcher.helper.JVMHelper.OS;
import launcher.request.update.LauncherRequest;
import launcher.serialize.HInput;
import launcher.serialize.HOutput;
import launcher.serialize.signed.SignedObjectHolder;
import launcher.serialize.stream.StreamObject;
import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.net.URL;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.security.interfaces.RSAPublicKey;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
public final class ClientLauncher
{
// Authlib constants
@LauncherAPI
public static final String SKIN_URL_PROPERTY = "skinURL";
@LauncherAPI
public static final String SKIN_DIGEST_PROPERTY = "skinDigest";
@LauncherAPI
public static final String CLOAK_URL_PROPERTY = "cloakURL";
@LauncherAPI
public static final String CLOAK_DIGEST_PROPERTY = "cloakDigest";
private static final String[] EMPTY_ARRAY = new String[0];
private static final String MAGICAL_INTEL_OPTION = "-XX:HeapDumpPath=ThisTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump";
private static final Set<PosixFilePermission> BIN_POSIX_PERMISSIONS = Collections.unmodifiableSet(EnumSet.of(
PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, // Owner
PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_EXECUTE, // Group
PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_EXECUTE // Others
));
// Constants
private static final Path NATIVES_DIR = IOHelper.toPath("natives");
private static final Path RESOURCEPACKS_DIR = IOHelper.toPath("resourcepacks");
private static final Pattern UUID_PATTERN = Pattern.compile("-", Pattern.LITERAL);
// Used to determine from clientside is launched from launcher
private static final AtomicBoolean LAUNCHED = new AtomicBoolean(false);
private ClientLauncher()
{
}
@LauncherAPI
public static boolean isLaunched()
{
return LAUNCHED.get();
}
public static String jvmProperty(String name, String value)
{
return String.format("-D%s=%s", name, value);
}
@LauncherAPI
public static Process launch(Path jvmDir, SignedObjectHolder<HashedDir> jvmHDir,
SignedObjectHolder<HashedDir> assetHDir, SignedObjectHolder<HashedDir> clientHDir,
SignedObjectHolder<ClientProfile> profile, Params params, boolean pipeOutput) throws Throwable
{
// Write params file (instead of CLI; Mustdie32 API can't handle command line > 32767 chars)
LogHelper.debug("Writing ClientLauncher params file");
Path paramsFile = Files.createTempFile("ClientLauncherParams", ".bin");
try (HOutput output = new HOutput(IOHelper.newOutput(paramsFile)))
{
params.write(output);
profile.write(output);
// Write hdirs
jvmHDir.write(output);
assetHDir.write(output);
clientHDir.write(output);
}
// Resolve java bin and set permissions
LogHelper.debug("Resolving JVM binary");
Path javaBin = IOHelper.resolveJavaBin(jvmDir);
if (IOHelper.POSIX)
{
Files.setPosixFilePermissions(javaBin, BIN_POSIX_PERMISSIONS);
}
// Fill CLI arguments
List<String> args = new LinkedList<>();
args.add(javaBin.toString());
args.add(MAGICAL_INTEL_OPTION);
if (params.ram > 0 && params.ram <= JVMHelper.RAM)
{
args.add("-Xms" + params.ram + 'M');
args.add("-Xmx" + params.ram + 'M');
}
args.add(jvmProperty(LogHelper.DEBUG_PROPERTY, Boolean.toString(LogHelper.isDebugEnabled())));
if (Config.ADDRESS_OVERRIDE != null)
{
args.add(jvmProperty(Config.ADDRESS_OVERRIDE_PROPERTY, Config.ADDRESS_OVERRIDE));
}
if (JVMHelper.OS_TYPE == OS.MUSTDIE && JVMHelper.OS_VERSION.startsWith("10."))
{
LogHelper.debug("MustDie 10 fix is applied");
args.add(jvmProperty("os.name", "Windows 10"));
args.add(jvmProperty("os.version", "10.0"));
}
// A fucking shitty fix
args.add(jvmProperty(JVMHelper.JAVA_LIBRARY_PATH, params.clientDir.resolve(NATIVES_DIR).toString()));
// Add classpath and main class
Collections.addAll(args, profile.object.getJvmArgs());
String v = profile.object.getVersion();
if (Version.compare(v, "1.13") >= 0 && JVMHelper.OS_TYPE == OS.MACOSX)
Collections.addAll(args, "-XstartOnFirstThread");
Collections.addAll(args, "-classpath", IOHelper.getCodeSource(ClientLauncher.class).toString(), ClientLauncher.class.getName());
args.add(paramsFile.toString()); // Add params file path to args
// Print commandline debug message
LogHelper.debug("Commandline: " + args);
// Build client process
LogHelper.debug("Launching client instance");
ProcessBuilder builder = new ProcessBuilder(args);
builder.directory(params.clientDir.toFile());
builder.inheritIO();
Map<String, String> env = builder.environment();
env.put("_JAVA_OPTS", "");
env.put("_JAVA_OPTIONS", "");
env.put("JAVA_OPTS", "");
env.put("JAVA_OPTIONS", "");
if (pipeOutput)
{
builder.redirectErrorStream(true);
builder.redirectOutput(Redirect.PIPE);
}
// Let's rock!
return builder.start();
}
@LauncherAPI
public static void main(String... args) throws Throwable
{
SecurityHelper.verifyCertificates(ClientLauncher.class);
JVMHelper.verifySystemProperties(ClientLauncher.class, true);
LogHelper.printVersion("Client Launcher");
// Resolve params file
VerifyHelper.verifyInt(args.length, l -> l >= 1, "Missing args: <paramsFile>");
Path paramsFile = IOHelper.toPath(args[0]);
// Read and delete params file
LogHelper.debug("Reading ClientLauncher params file");
Params params;
SignedObjectHolder<ClientProfile> profile;
SignedObjectHolder<HashedDir> jvmHDir, assetHDir, clientHDir;
RSAPublicKey publicKey = Launcher.getConfig().publicKey;
try (HInput input = new HInput(IOHelper.newInput(paramsFile)))
{
params = new Params(input);
profile = new SignedObjectHolder<>(input, publicKey, ClientProfile.RO_ADAPTER);
// Read hdirs
jvmHDir = new SignedObjectHolder<>(input, publicKey, HashedDir::new);
assetHDir = new SignedObjectHolder<>(input, publicKey, HashedDir::new);
clientHDir = new SignedObjectHolder<>(input, publicKey, HashedDir::new);
}
finally
{
Files.delete(paramsFile);
}
// Verify ClientLauncher sign and classpath
LogHelper.debug("Verifying ClientLauncher sign and classpath");
SecurityHelper.verifySign(LauncherRequest.BINARY_PATH, params.launcherSign, publicKey);
URL[] classpath = JVMHelper.getClassPath();
for (URL classpathURL : classpath)
{
Path file = Paths.get(classpathURL.toURI());
if (!file.startsWith(IOHelper.JVM_DIR) && !file.equals(LauncherRequest.BINARY_PATH))
{
throw new SecurityException(String.format("Forbidden classpath entry: '%s'", file));
}
}
// Start client with WatchService monitoring
boolean digest = !profile.object.isUpdateFastCheck();
LogHelper.debug("Starting JVM and client WatchService");
FileNameMatcher assetMatcher = profile.object.getAssetUpdateMatcher();
FileNameMatcher clientMatcher = profile.object.getClientUpdateMatcher();
try (DirWatcher jvmWatcher = new DirWatcher(IOHelper.JVM_DIR, jvmHDir.object, null, digest); // JVM Watcher
DirWatcher assetWatcher = new DirWatcher(params.assetDir, assetHDir.object, assetMatcher, digest);
DirWatcher clientWatcher = new DirWatcher(params.clientDir, clientHDir.object, clientMatcher, digest))
{
// Start WatchService, and only then client
CommonHelper.newThread("JVM Directory Watcher", true, jvmWatcher).start();
CommonHelper.newThread("Asset Directory Watcher", true, assetWatcher).start();
CommonHelper.newThread("Client Directory Watcher", true, clientWatcher).start();
// Verify current state of all dirs
verifyHDir(IOHelper.JVM_DIR, jvmHDir.object, null, digest);
verifyHDir(params.assetDir, assetHDir.object, assetMatcher, digest);
verifyHDir(params.clientDir, clientHDir.object, clientMatcher, digest);
launch(profile.object, params);
}
}
@LauncherAPI
public static String toHash(UUID uuid)
{
return UUID_PATTERN.matcher(uuid.toString()).replaceAll("");
}
@LauncherAPI
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, false, digest);
if (!hdir.diff(currentHDir, matcher).isSame())
{
throw new SecurityException(String.format("Forbidden modification: '%s'", IOHelper.getFileName(dir)));
}
}
private static void addClientArgs(Collection<String> args, ClientProfile profile, Params params)
{
PlayerProfile pp = params.pp;
// Add version-dependent args
String version = profile.getVersion();
Collections.addAll(args, "--username", pp.username);
if (Version.compare(version, "1.7.2") >= 0)
{
Collections.addAll(args, "--uuid", toHash(pp.uuid));
Collections.addAll(args, "--accessToken", params.accessToken);
// Add 1.7.3+ args (user properties, asset index)
if (Version.compare(version, "1.7.3") >= 0)
{
// Add user properties
if (Version.compare(version, "1.7.4") >= 0)
{
Collections.addAll(args, "--userType", "mojang");
}
JsonObject properties = Json.object();
if (pp.skin != null)
{
properties.add(SKIN_URL_PROPERTY, Json.array(pp.skin.url));
properties.add(SKIN_DIGEST_PROPERTY, Json.array(SecurityHelper.toHex(pp.skin.digest)));
}
if (pp.cloak != null)
{
properties.add(CLOAK_URL_PROPERTY, Json.array(pp.cloak.url));
properties.add(CLOAK_DIGEST_PROPERTY, Json.array(SecurityHelper.toHex(pp.cloak.digest)));
}
Collections.addAll(args, "--userProperties", properties.toString(WriterConfig.MINIMAL));
// Add asset index
Collections.addAll(args, "--assetIndex", profile.getAssetIndex());
}
}
else
{
Collections.addAll(args, "--session", params.accessToken);
}
// Add version and dirs args
Collections.addAll(args, "--version", profile.getVersion());
Collections.addAll(args, "--gameDir", params.clientDir.toString());
Collections.addAll(args, "--assetsDir", params.assetDir.toString());
Collections.addAll(args, "--resourcePackDir", params.clientDir.resolve(RESOURCEPACKS_DIR).toString());
if (Version.compare(version, "1.9.0") >= 0)
{ // Just to show it in debug screen
Collections.addAll(args, "--versionType", "KJ-Launcher v" + Launcher.VERSION);
}
// Add server args
if (params.autoEnter)
{
Collections.addAll(args, "--server", profile.getServerAddress());
Collections.addAll(args, "--port", Integer.toString(profile.getServerPort()));
}
// Add window size args
if (params.fullScreen)
{
Collections.addAll(args, "--fullscreen", Boolean.toString(true));
}
if (params.width > 0 && params.height > 0)
{
Collections.addAll(args, "--width", Integer.toString(params.width));
Collections.addAll(args, "--height", Integer.toString(params.height));
}
}
private static void addClientLegacyArgs(Collection<String> args, ClientProfile profile, Params params)
{
args.add(params.pp.username);
args.add(params.accessToken);
// Add args for tweaker
Collections.addAll(args, "--version", profile.getVersion());
Collections.addAll(args, "--gameDir", params.clientDir.toString());
Collections.addAll(args, "--assetsDir", params.assetDir.toString());
}
private static void launch(ClientProfile profile, Params params) throws Throwable
{
// Add client args
Collection<String> args = new LinkedList<>();
if (Version.compare(profile.getVersion(), "1.6.0") >= 0)
{
addClientArgs(args, profile, params);
}
else
{
addClientLegacyArgs(args, profile, params);
}
Collections.addAll(args, profile.getClientArgs());
LogHelper.debug("Args: " + args);
// Add client classpath
URL[] classPath = resolveClassPath(params.clientDir, profile.getClassPath());
for (URL url : classPath)
{
JVMHelper.addClassPath(url);
}
// Resolve main class and method
Class<?> mainClass = Class.forName(profile.getMainClass());
MethodHandle mainMethod = JVMHelper.LOOKUP.findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class))
.asFixedArity();
// Invoke main method with exception wrapping
LAUNCHED.set(true);
JVMHelper.fullGC();
System.setProperty("minecraft.launcher.brand", "KeeperJerry's NekroLauncher");
System.setProperty("minecraft.launcher.version", Launcher.VERSION);
System.setProperty("minecraft.applet.TargetDirectory", params.clientDir.toString()); // For 1.5.2
mainMethod.invoke((Object) args.toArray(EMPTY_ARRAY));
}
private static URL[] resolveClassPath(Path clientDir, String... classPath) throws IOException
{
Collection<Path> result = new LinkedList<>();
for (String classPathEntry : classPath)
{
Path path = clientDir.resolve(IOHelper.toPath(classPathEntry));
if (IOHelper.isDir(path))
{ // Recursive walking and adding
IOHelper.walk(path, new ClassPathFileVisitor(result), false);
continue;
}
result.add(path);
}
return result.stream().map(IOHelper::toURL).toArray(URL[]::new);
}
public static final class Params extends StreamObject
{
// Client paths
@LauncherAPI
public final Path assetDir;
@LauncherAPI
public final Path clientDir;
// Client params
@LauncherAPI
public final PlayerProfile pp;
@LauncherAPI
public final String accessToken;
@LauncherAPI
public final boolean autoEnter;
@LauncherAPI
public final boolean fullScreen;
@LauncherAPI
public final int ram;
@LauncherAPI
public final int width;
@LauncherAPI
public final int height;
private final byte[] launcherSign;
@LauncherAPI
public Params(byte[] launcherSign, Path assetDir, Path clientDir, PlayerProfile pp, String accessToken,
boolean autoEnter, boolean fullScreen, int ram, int width, int height)
{
this.launcherSign = launcherSign.clone();
// Client paths
this.assetDir = assetDir;
this.clientDir = clientDir;
// Client params
this.pp = pp;
this.accessToken = SecurityHelper.verifyToken(accessToken);
this.autoEnter = autoEnter;
this.fullScreen = fullScreen;
this.ram = ram;
this.width = width;
this.height = height;
}
@LauncherAPI
public Params(HInput input) throws IOException
{
launcherSign = input.readByteArray(-SecurityHelper.RSA_KEY_LENGTH);
// Client paths
assetDir = IOHelper.toPath(input.readString(0));
clientDir = IOHelper.toPath(input.readString(0));
// Client params
pp = new PlayerProfile(input);
int length = input.readInt();
accessToken = SecurityHelper.verifyToken(input.readASCII(-length));
autoEnter = input.readBoolean();
fullScreen = input.readBoolean();
ram = input.readVarInt();
width = input.readVarInt();
height = input.readVarInt();
}
@Override
public void write(HOutput output) throws IOException
{
output.writeByteArray(launcherSign, -SecurityHelper.RSA_KEY_LENGTH);
// Client paths
output.writeString(assetDir.toString(), 0);
output.writeString(clientDir.toString(), 0);
// Client params
pp.write(output);
output.writeInt(accessToken.length());
output.writeASCII(accessToken, -accessToken.length());
output.writeBoolean(autoEnter);
output.writeBoolean(fullScreen);
output.writeVarInt(ram);
output.writeVarInt(width);
output.writeVarInt(height);
}
}
private static final class ClassPathFileVisitor extends SimpleFileVisitor<Path>
{
private final Collection<Path> result;
private ClassPathFileVisitor(Collection<Path> result)
{
this.result = result;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
{
if (IOHelper.hasExtension(file, "jar") || IOHelper.hasExtension(file, "zip"))
{
result.add(file);
}
return super.visitFile(file, attrs);
}
}
}
// Н@хуя это здесь? *facepam* И главное нахуя?