Newer
Older
KeeperJerry_Launcher / Launcher / source / Launcher.java
@KeeperJerry KeeperJerry on 24 Mar 2021 14 KB Hotfix protocol 1.7.2.2
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.2.2";
    @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.warning("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
        long start = System.currentTimeMillis();
        try
        {
            new Launcher().start(args);
        }
        catch (Throwable exc)
        {
            LogHelper.error(exc);
            return;
        }
        long end = System.currentTimeMillis();
        LogHelper.debug("Launcher started in %dms", end - start);
    }

    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);
            }
        }
    }
}