package launcher;
import javafx.application.Application;
import launcher.client.ClientLauncher;
import launcher.client.ClientLauncher.Params;
import launcher.client.ClientProfile;
import launcher.client.ClientProfile.Version;
import launcher.client.PlayerProfile;
import launcher.client.PlayerProfile.Texture;
import launcher.client.ServerPinger;
import launcher.hasher.FileNameMatcher;
import launcher.hasher.HashedDir;
import launcher.hasher.HashedEntry;
import launcher.hasher.HashedFile;
import launcher.helper.*;
import launcher.helper.JVMHelper.OS;
import launcher.helper.LogHelper.Output;
import launcher.helper.SecurityHelper.DigestAlgorithm;
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.ConfigObject.Adapter;
import launcher.serialize.config.TextConfigReader;
import launcher.serialize.config.TextConfigWriter;
import launcher.serialize.config.entry.*;
import launcher.serialize.config.entry.ConfigEntry.Type;
import launcher.serialize.signed.SignedBytesHolder;
import launcher.serialize.signed.SignedObjectHolder;
import launcher.serialize.stream.EnumSerializer;
import launcher.serialize.stream.StreamObject;
import javax.script.*;
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.util.*;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public final class Launcher
{
// Version info
@LauncherAPI
public static final String VERSION = "1.7.4.1";
@LauncherAPI
public static final String BUILD = readBuildNumber();
@LauncherAPI
public static final int PROTOCOL_MAGIC = 0x724724_00 + 23;
// 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";
private static final AtomicReference<Config> CONFIG = new AtomicReference<>();
// Instance
private final AtomicBoolean started = new AtomicBoolean(false);
private final ScriptEngine engine = CommonHelper.newScriptEngine();
private Launcher()
{
setScriptBindings();
}
@LauncherAPI
public static void addLauncherClassBindings(ScriptEngine engine, Map<String, Object> bindings)
{
addClassBinding(engine, bindings, "Launcher", Launcher.class);
addClassBinding(engine, bindings, "Config", Config.class);
// Set client class bindings
addClassBinding(engine, bindings, "PlayerProfile", PlayerProfile.class);
addClassBinding(engine, bindings, "PlayerProfileTexture", Texture.class);
addClassBinding(engine, bindings, "ClientProfile", ClientProfile.class);
addClassBinding(engine, bindings, "ClientProfileVersion", Version.class);
addClassBinding(engine, bindings, "ClientLauncher", ClientLauncher.class);
addClassBinding(engine, bindings, "ClientLauncherParams", Params.class);
addClassBinding(engine, bindings, "ServerPinger", ServerPinger.class);
// Set request class bindings
addClassBinding(engine, bindings, "Request", Request.class);
addClassBinding(engine, bindings, "RequestType", Request.Type.class);
addClassBinding(engine, bindings, "RequestException", RequestException.class);
addClassBinding(engine, bindings, "CustomRequest", CustomRequest.class);
addClassBinding(engine, bindings, "PingRequest", PingRequest.class);
addClassBinding(engine, bindings, "AuthRequest", AuthRequest.class);
addClassBinding(engine, bindings, "JoinServerRequest", JoinServerRequest.class);
addClassBinding(engine, bindings, "CheckServerRequest", CheckServerRequest.class);
addClassBinding(engine, bindings, "UpdateRequest", UpdateRequest.class);
addClassBinding(engine, bindings, "LauncherRequest", LauncherRequest.class);
addClassBinding(engine, bindings, "ProfileByUsernameRequest", ProfileByUsernameRequest.class);
addClassBinding(engine, bindings, "ProfileByUUIDRequest", ProfileByUUIDRequest.class);
addClassBinding(engine, bindings, "BatchProfileByUsernameRequest", BatchProfileByUsernameRequest.class);
// Set hasher class bindings
addClassBinding(engine, bindings, "FileNameMatcher", FileNameMatcher.class);
addClassBinding(engine, bindings, "HashedDir", HashedDir.class);
addClassBinding(engine, bindings, "HashedFile", HashedFile.class);
addClassBinding(engine, bindings, "HashedEntryType", HashedEntry.Type.class);
// Set serialization class bindings
addClassBinding(engine, bindings, "HInput", HInput.class);
addClassBinding(engine, bindings, "HOutput", HOutput.class);
addClassBinding(engine, bindings, "StreamObject", StreamObject.class);
addClassBinding(engine, bindings, "StreamObjectAdapter", StreamObject.Adapter.class);
addClassBinding(engine, bindings, "SignedBytesHolder", SignedBytesHolder.class);
addClassBinding(engine, bindings, "SignedObjectHolder", SignedObjectHolder.class);
addClassBinding(engine, bindings, "EnumSerializer", EnumSerializer.class);
// Set config serialization class bindings
addClassBinding(engine, bindings, "ConfigObject", ConfigObject.class);
addClassBinding(engine, bindings, "ConfigObjectAdapter", Adapter.class);
addClassBinding(engine, bindings, "BlockConfigEntry", BlockConfigEntry.class);
addClassBinding(engine, bindings, "BooleanConfigEntry", BooleanConfigEntry.class);
addClassBinding(engine, bindings, "IntegerConfigEntry", IntegerConfigEntry.class);
addClassBinding(engine, bindings, "ListConfigEntry", ListConfigEntry.class);
addClassBinding(engine, bindings, "StringConfigEntry", StringConfigEntry.class);
addClassBinding(engine, bindings, "ConfigEntryType", Type.class);
addClassBinding(engine, bindings, "TextConfigReader", TextConfigReader.class);
addClassBinding(engine, bindings, "TextConfigWriter", TextConfigWriter.class);
// Set helper class bindings
addClassBinding(engine, bindings, "CommonHelper", CommonHelper.class);
addClassBinding(engine, bindings, "IOHelper", IOHelper.class);
addClassBinding(engine, bindings, "JVMHelper", JVMHelper.class);
addClassBinding(engine, bindings, "JVMHelperOS", OS.class);
addClassBinding(engine, bindings, "LogHelper", LogHelper.class);
addClassBinding(engine, bindings, "LogHelperOutput", Output.class);
addClassBinding(engine, bindings, "SecurityHelper", SecurityHelper.class);
addClassBinding(engine, bindings, "DigestAlgorithm", DigestAlgorithm.class);
addClassBinding(engine, bindings, "VerifyHelper", VerifyHelper.class);
// Load JS API if available
try
{
addClassBinding(engine, bindings, "Application", Application.class);
addClassBinding(engine, bindings, "JSApplication", JSApplication.class);
}
catch (Throwable ignored)
{
LogHelper.error("JavaFX API isn't available");
}
}
@LauncherAPI
public static void addClassBinding(ScriptEngine engine, Map<String, Object> bindings, String name, Class<?> clazz)
{
bindings.put(name + "Class", clazz); // Backwards-compatibility
try
{
engine.eval("var " + name + " = " + name + "Class.static;");
}
catch (ScriptException e)
{
throw new AssertionError(e);
}
}
@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(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
{
SecurityHelper.verifyCertificates(Launcher.class);
JVMHelper.verifySystemProperties(Launcher.class, true);
LogHelper.printVersion("Launcher");
// Start Launcher
try
{
new Launcher().start(args);
}
catch (Throwable exc)
{
LogHelper.error(exc);
return;
}
}
private static String readBuildNumber()
{
try
{
return IOHelper.request(IOHelper.getResourceURL("buildnumber"));
}
catch (IOException ignored)
{
return "dev"; // Maybe dev env?
}
}
@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(engine, bindings);
}
public static final class Config extends StreamObject
{
@LauncherAPI
public static final String ADDRESS_OVERRIDE_PROPERTY = "launcher.addressOverride";
@LauncherAPI
public static final String ADDRESS_OVERRIDE = System.getProperty(ADDRESS_OVERRIDE_PROPERTY, 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<Entry<String, byte[]>> entrySet = runtime.entrySet();
output.writeLength(entrySet.size(), 0);
for (Entry<String, byte[]> entry : runtime.entrySet())
{
output.writeString(entry.getKey(), 255);
output.writeByteArray(entry.getValue(), SecurityHelper.CRYPTO_MAX_LENGTH);
}
}
}
}