package launcher;
import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URL;
import java.nio.file.NoSuchFileException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import launcher.client.ClientLauncher;
import launcher.client.ClientProfile;
import launcher.client.PlayerProfile;
import launcher.client.ServerPinger;
import launcher.hasher.FileNameMatcher;
import launcher.hasher.HashedDir;
import launcher.hasher.HashedEntry;
import launcher.hasher.HashedFile;
import launcher.helper.CommonHelper;
import launcher.helper.IOHelper;
import launcher.helper.JVMHelper;
import launcher.helper.LogHelper;
import launcher.helper.SecurityHelper;
import launcher.helper.VerifyHelper;
import launcher.helper.js.JSApplication;
import launcher.request.CustomRequest;
import launcher.request.PingRequest;
import launcher.request.Request;
import launcher.request.RequestException;
import launcher.request.auth.AuthRequest;
import launcher.request.auth.CheckServerRequest;
import launcher.request.auth.JoinServerRequest;
import launcher.request.update.LauncherRequest;
import launcher.request.update.UpdateRequest;
import launcher.request.uuid.BatchProfileByUsernameRequest;
import launcher.request.uuid.ProfileByUUIDRequest;
import launcher.request.uuid.ProfileByUsernameRequest;
import launcher.serialize.HInput;
import launcher.serialize.HOutput;
import launcher.serialize.config.ConfigObject;
import launcher.serialize.config.TextConfigReader;
import launcher.serialize.config.TextConfigWriter;
import launcher.serialize.config.entry.BlockConfigEntry;
import launcher.serialize.config.entry.BooleanConfigEntry;
import launcher.serialize.config.entry.ConfigEntry;
import launcher.serialize.config.entry.IntegerConfigEntry;
import launcher.serialize.config.entry.ListConfigEntry;
import launcher.serialize.config.entry.StringConfigEntry;
import launcher.serialize.signed.SignedBytesHolder;
import launcher.serialize.signed.SignedObjectHolder;
import launcher.serialize.stream.EnumSerializer;
import launcher.serialize.stream.StreamObject;
public final class Launcher {
private static final AtomicReference<Config> CONFIG = new AtomicReference<>();
// Version info
@LauncherAPI public static final String VERSION = "15.1";
@LauncherAPI public static final String BUILD = readBuildNumber();
@LauncherAPI public static final int PROTOCOL_MAGIC = 0x724724_16;
// Constants
@LauncherAPI public static final String RUNTIME_DIR = "runtime";
@LauncherAPI public static final String CONFIG_FILE = "config.bin";
@LauncherAPI public static final String INIT_SCRIPT_FILE = "init.js";
// Instance
private final AtomicBoolean started = new AtomicBoolean(false);
private final ScriptEngine engine = CommonHelper.newScriptEngine();
private Launcher() {
setScriptBindings();
}
@LauncherAPI
public Object loadScript(URL url) throws IOException, ScriptException {
LogHelper.debug("Loading script: '%s'", url);
try (BufferedReader reader = IOHelper.newReader(url)) {
return engine.eval(reader);
}
}
@LauncherAPI
public void start(String... args) throws Throwable {
Objects.requireNonNull(args, "args");
if (started.getAndSet(true)) {
throw new IllegalStateException("Launcher has been already started");
}
// Load init.js script
loadScript(getResourceURL(INIT_SCRIPT_FILE));
LogHelper.info("Invoking start() function");
((Invocable) engine).invokeFunction("start", (Object) args);
}
private void setScriptBindings() {
LogHelper.info("Setting up script engine bindings");
Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
bindings.put("launcher", this);
// Add launcher class bindings
addLauncherClassBindings(bindings);
}
@LauncherAPI
public static void addLauncherClassBindings(Map<String, Object> bindings) {
bindings.put("LauncherClass", Launcher.class);
// Set client class bindings
bindings.put("PlayerProfileClass", PlayerProfile.class);
bindings.put("PlayerProfileTextureClass", PlayerProfile.Texture.class);
bindings.put("ClientProfileClass", ClientProfile.class);
bindings.put("ClientProfileVersionClass", ClientProfile.Version.class);
bindings.put("ClientLauncherClass", ClientLauncher.class);
bindings.put("ClientLauncherParamsClass", ClientLauncher.Params.class);
bindings.put("ServerPingerClass", ServerPinger.class);
// Set request class bindings
bindings.put("RequestClass", Request.class);
bindings.put("RequestTypeClass", Request.Type.class);
bindings.put("RequestExceptionClass", RequestException.class);
bindings.put("CustomRequestClass", CustomRequest.class);
bindings.put("PingRequestClass", PingRequest.class);
bindings.put("AuthRequestClass", AuthRequest.class);
bindings.put("JoinServerRequestClass", JoinServerRequest.class);
bindings.put("CheckServerRequestClass", CheckServerRequest.class);
bindings.put("UpdateRequestClass", UpdateRequest.class);
bindings.put("LauncherRequestClass", LauncherRequest.class);
bindings.put("ProfileByUsernameRequestClass", ProfileByUsernameRequest.class);
bindings.put("ProfileByUUIDRequestClass", ProfileByUUIDRequest.class);
bindings.put("BatchProfileByUsernameRequestClass", BatchProfileByUsernameRequest.class);
// Set hasher class bindings
bindings.put("FileNameMatcherClass", FileNameMatcher.class);
bindings.put("HashedDirClass", HashedDir.class);
bindings.put("HashedFileClass", HashedFile.class);
bindings.put("HashedEntryTypeClass", HashedEntry.Type.class);
// Set serialization class bindings
bindings.put("HInputClass", HInput.class);
bindings.put("HOutputClass", HOutput.class);
bindings.put("StreamObjectClass", StreamObject.class);
bindings.put("StreamObjectAdapterClass", StreamObject.Adapter.class);
bindings.put("SignedBytesHolderClass", SignedBytesHolder.class);
bindings.put("SignedObjectHolderClass", SignedObjectHolder.class);
bindings.put("EnumSerializerClass", EnumSerializer.class);
// Set config serialization class bindings
bindings.put("ConfigObjectClass", ConfigObject.class);
bindings.put("ConfigObjectAdapterClass", ConfigObject.Adapter.class);
bindings.put("BlockConfigEntryClass", BlockConfigEntry.class);
bindings.put("BooleanConfigEntryClass", BooleanConfigEntry.class);
bindings.put("IntegerConfigEntryClass", IntegerConfigEntry.class);
bindings.put("ListConfigEntryClass", ListConfigEntry.class);
bindings.put("StringConfigEntryClass", StringConfigEntry.class);
bindings.put("ConfigEntryTypeClass", ConfigEntry.Type.class);
bindings.put("TextConfigReaderClass", TextConfigReader.class);
bindings.put("TextConfigWriterClass", TextConfigWriter.class);
// Set helper class bindings
bindings.put("CommonHelperClass", CommonHelper.class);
bindings.put("IOHelperClass", IOHelper.class);
bindings.put("JVMHelperClass", JVMHelper.class);
bindings.put("JVMHelperOSClass", JVMHelper.OS.class);
bindings.put("LogHelperClass", LogHelper.class);
bindings.put("LogHelperOutputClass", LogHelper.Output.class);
bindings.put("SecurityHelperClass", SecurityHelper.class);
bindings.put("DigestAlgorithmClass", SecurityHelper.DigestAlgorithm.class);
bindings.put("VerifyHelperClass", VerifyHelper.class);
// Load JS API if available
try {
Class.forName("javafx.application.Application");
bindings.put("JSApplicationClass", JSApplication.class);
} catch (ClassNotFoundException e) {
LogHelper.warning("JavaFX API isn't available");
}
}
@LauncherAPI
public static Config getConfig() {
Config config = CONFIG.get();
if (config == null) {
try (HInput input = new HInput(IOHelper.newInput(IOHelper.getResourceURL(CONFIG_FILE)))) {
config = new Config(input);
} catch (IOException | InvalidKeySpecException e) {
throw new SecurityException(e);
}
CONFIG.set(config);
}
return config;
}
@LauncherAPI
public static URL getResourceURL(String name) throws IOException {
Config config = getConfig();
byte[] validDigest = config.runtime.get(name);
if (validDigest == null) { // No such resource digest
throw new NoSuchFileException(name);
}
// Resolve URL and verify digest
URL url = IOHelper.getResourceURL(RUNTIME_DIR + '/' + name);
if (!Arrays.equals(validDigest, SecurityHelper.digest(SecurityHelper.DigestAlgorithm.MD5, url))) {
throw new NoSuchFileException(name); // Digest mismatch
}
// Return verified URL
return url;
}
@LauncherAPI
@SuppressWarnings({ "SameReturnValue", "MethodReturnAlwaysConstant" })
public static String getVersion() {
return VERSION; // Because Java constants are known at compile-time
}
public static void main(String... args) throws Throwable {
JVMHelper.verifySystemProperties(Launcher.class);
SecurityHelper.verifyCertificates(Launcher.class);
LogHelper.printVersion("Launcher");
// Start Launcher
Instant start = Instant.now();
try {
new Launcher().start(args);
} catch (Exception e) {
LogHelper.error(e);
return;
}
Instant end = Instant.now();
LogHelper.debug("Launcher started in %dms", Duration.between(start, end).toMillis());
}
private static String readBuildNumber() {
try {
return IOHelper.request(IOHelper.getResourceURL("buildnumber"));
} catch (IOException ignored) {
return "dev"; // Maybe dev env?
}
}
public static final class Config extends StreamObject {
private static final String ADDRESS_OVERRIDE = System.getProperty("launcher.addressOverride", null);
// Instance
@LauncherAPI public final InetSocketAddress address;
@LauncherAPI public final RSAPublicKey publicKey;
@LauncherAPI public final Map<String, byte[]> runtime;
@LauncherAPI
@SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
public Config(String address, int port, RSAPublicKey publicKey, Map<String, byte[]> runtime) {
this.address = InetSocketAddress.createUnresolved(address, port);
this.publicKey = Objects.requireNonNull(publicKey, "publicKey");
this.runtime = Collections.unmodifiableMap(new HashMap<>(runtime));
}
@LauncherAPI
public Config(HInput input) throws IOException, InvalidKeySpecException {
String localAddress = input.readASCII(255);
address = InetSocketAddress.createUnresolved(
ADDRESS_OVERRIDE == null ? localAddress : ADDRESS_OVERRIDE, input.readLength(65535));
publicKey = SecurityHelper.toPublicRSAKey(input.readByteArray(SecurityHelper.CRYPTO_MAX_LENGTH));
// Read signed runtime
int count = input.readLength(0);
Map<String, byte[]> localResources = new HashMap<>(count);
for (int i = 0; i < count; i++) {
String name = input.readString(255);
VerifyHelper.putIfAbsent(localResources, name,
input.readByteArray(SecurityHelper.CRYPTO_MAX_LENGTH),
String.format("Duplicate runtime resource: '%s'", name));
}
runtime = Collections.unmodifiableMap(localResources);
// Print warning if address override is enabled
if (ADDRESS_OVERRIDE != null) {
LogHelper.warning("Address override is enabled: '%s'", ADDRESS_OVERRIDE);
}
}
@Override
public void write(HOutput output) throws IOException {
output.writeASCII(address.getHostString(), 255);
output.writeLength(address.getPort(), 65535);
output.writeByteArray(publicKey.getEncoded(), SecurityHelper.CRYPTO_MAX_LENGTH);
// Write signed runtime
Set<Map.Entry<String, byte[]>> entrySet = runtime.entrySet();
output.writeLength(entrySet.size(), 0);
for (Map.Entry<String, byte[]> entry : runtime.entrySet()) {
output.writeString(entry.getKey(), 255);
output.writeByteArray(entry.getValue(), SecurityHelper.CRYPTO_MAX_LENGTH);
}
}
}
}