package ru.gravit.launchserver; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import ru.gravit.launcher.Launcher; import ru.gravit.launcher.LauncherConfig; import ru.gravit.launcher.hasher.HashedDir; import ru.gravit.launcher.managers.GarbageManager; import ru.gravit.launcher.profiles.ClientProfile; import ru.gravit.launcher.serialize.signed.SignedObjectHolder; import ru.gravit.launchserver.auth.AuthLimiter; import ru.gravit.launchserver.auth.handler.AuthHandler; import ru.gravit.launchserver.auth.handler.MemoryAuthHandler; import ru.gravit.launchserver.auth.hwid.AcceptHWIDHandler; import ru.gravit.launchserver.auth.hwid.HWIDHandler; import ru.gravit.launchserver.auth.permissions.JsonFilePermissionsHandler; import ru.gravit.launchserver.auth.permissions.PermissionsHandler; import ru.gravit.launchserver.auth.provider.AuthProvider; import ru.gravit.launchserver.auth.provider.RejectAuthProvider; import ru.gravit.launchserver.binary.*; import ru.gravit.launchserver.command.handler.CommandHandler; import ru.gravit.launchserver.command.handler.JLineCommandHandler; import ru.gravit.launchserver.command.handler.StdCommandHandler; import ru.gravit.launchserver.config.*; import ru.gravit.launchserver.manangers.*; import ru.gravit.launchserver.manangers.hook.AuthHookManager; import ru.gravit.launchserver.manangers.hook.BuildHookManager; import ru.gravit.launchserver.manangers.hook.SocketHookManager; import ru.gravit.launchserver.response.Response; import ru.gravit.launchserver.socket.NettyServerSocketHandler; import ru.gravit.launchserver.socket.ServerSocketHandler; import ru.gravit.launchserver.texture.RequestTextureProvider; import ru.gravit.launchserver.texture.TextureProvider; import ru.gravit.utils.helper.*; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.lang.ProcessBuilder.Redirect; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.security.KeyPair; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; import java.time.Duration; import java.time.Instant; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicBoolean; import java.util.zip.CRC32; public final class LaunchServer implements Runnable { public static final class Config { public int port; private String address; private String bindAddress; public String projectName; public String[] mirrors; public String binaryName; public LauncherConfig.LauncherEnvironment env; // Handlers & Providers public AuthProvider[] authProvider; public AuthHandler authHandler; public PermissionsHandler permissionsHandler; public TextureProvider textureProvider; public HWIDHandler hwidHandler; // Misc options public int threadCount; public int threadCoreCount; public ExeConf launch4j; public NettyConfig netty; public GuardLicenseConf guardLicense; public boolean compress; public int authRateLimit; public int authRateLimitMilis; public String[] authLimitExclusions; public String authRejectString; public String whitelistRejectString; public boolean genMappings; public boolean isUsingWrapper; public boolean isDownloadJava; public boolean isWarningMissArchJava; public boolean enabledProGuard; public boolean stripLineNumbers; public boolean deleteTempFiles; public boolean enableRcon; public String startScript; public boolean updatesNotify = true; // Defaultly to true public String getAddress() { return address; } public String getBindAddress() { return bindAddress; } public void setProjectName(String projectName) { this.projectName = projectName; } public void setBinaryName(String binaryName) { this.binaryName = binaryName; } public void setEnv(LauncherConfig.LauncherEnvironment env) { this.env = env; } public SocketAddress getSocketAddress() { return new InetSocketAddress(bindAddress, port); } public void setAddress(String address) { this.address = address; } public void verify() { VerifyHelper.verify(getAddress(), VerifyHelper.NOT_EMPTY, "LaunchServer address can't be empty"); if (authHandler == null) { throw new NullPointerException("AuthHandler must not be null"); } if (authProvider == null || authProvider[0] == null) { throw new NullPointerException("AuthProvider must not be null"); } if (textureProvider == null) { throw new NullPointerException("TextureProvider must not be null"); } if (permissionsHandler == null) { throw new NullPointerException("PermissionsHandler must not be null"); } if (env == null) { throw new NullPointerException("Env must not be null"); } } } public static class ExeConf { public boolean enabled; public String productName; public String productVer; public String fileDesc; public String fileVer; public String internalName; public String copyright; public String trademarks; public String txtFileVersion; public String txtProductVersion; } public class NettyConfig { public String bindAddress; public int port; public boolean clientEnabled; public String launcherURL; public String launcherEXEURL; public String address; } public class GuardLicenseConf { public String name; public String key; public String encryptKey; } private final class ProfilesFileVisitor extends SimpleFileVisitor { private final Collection result; private ProfilesFileVisitor(Collection result) { this.result = result; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { LogHelper.info("Syncing '%s' profile", IOHelper.getFileName(file)); // Read profile ClientProfile profile; try (BufferedReader reader = IOHelper.newReader(file)) { profile = Launcher.gson.fromJson(reader, ClientProfile.class); } profile.verify(); // Add SIGNED profile to result list result.add(profile); return super.visitFile(file, attrs); } } public static void main(String... args) throws Throwable { JVMHelper.checkStackTrace(LaunchServer.class); JVMHelper.verifySystemProperties(LaunchServer.class, true); LogHelper.addOutput(IOHelper.WORKING_DIR.resolve("LaunchServer.log")); LogHelper.printVersion("LaunchServer"); LogHelper.printLicense("LaunchServer"); // Start LaunchServer Instant start = Instant.now(); try { LaunchServer launchserver = new LaunchServer(IOHelper.WORKING_DIR, args); if(args.length == 0) launchserver.run(); else { //Обработка команды launchserver.commandHandler.eval(args,false); } } catch (Throwable exc) { LogHelper.error(exc); return; } Instant end = Instant.now(); LogHelper.debug("LaunchServer started in %dms", Duration.between(start, end).toMillis()); } // Constant paths public final Path dir; public final Path launcherLibraries; public final List args; public final Path configFile; public final Path publicKeyFile; public final Path privateKeyFile; public final Path updatesDir; public static LaunchServer server = null; public final Path profilesDir; // Server config public final Config config; public final RSAPublicKey publicKey; public final RSAPrivateKey privateKey; // Launcher binary public final JARLauncherBinary launcherBinary; public final LauncherBinary launcherEXEBinary; // HWID ban + anti-brutforce public final AuthLimiter limiter; public final SessionManager sessionManager; public final SocketHookManager socketHookManager; public final AuthHookManager authHookManager; // Server public final ModulesManager modulesManager; public final MirrorManager mirrorManager; public final ReloadManager reloadManager; public final ReconfigurableManager reconfigurableManager; public final BuildHookManager buildHookManager; public final ProguardConf proguardConf; public final CommandHandler commandHandler; public final ServerSocketHandler serverSocketHandler; public final NettyServerSocketHandler nettyServerSocketHandler; private final AtomicBoolean started = new AtomicBoolean(false); // Updates and profiles private volatile List profilesList; public volatile Map> updatesDirMap; public final Timer taskPool; public final Updater updater; public static Gson gson; public static GsonBuilder gsonBuilder; public LaunchServer(Path dir, String[] args) throws IOException, InvalidKeySpecException { this.dir = dir; taskPool = new Timer("Timered task worker thread", true); launcherLibraries = dir.resolve("launcher-libraries"); if (!Files.isDirectory(launcherLibraries)) { Files.deleteIfExists(launcherLibraries); Files.createDirectory(launcherLibraries); } this.args = Arrays.asList(args); configFile = dir.resolve("LaunchServer.conf"); publicKeyFile = dir.resolve("public.key"); privateKeyFile = dir.resolve("private.key"); updatesDir = dir.resolve("updates"); profilesDir = dir.resolve("profiles"); //Registration handlers and providers AuthHandler.registerHandlers(); AuthProvider.registerProviders(); TextureProvider.registerProviders(); HWIDHandler.registerHandlers(); PermissionsHandler.registerHandlers(); Response.registerResponses(); LaunchServer.server = this; // Set command handler CommandHandler localCommandHandler; try { Class.forName("jline.Terminal"); // JLine2 available localCommandHandler = new JLineCommandHandler(this); LogHelper.info("JLine2 terminal enabled"); } catch (ClassNotFoundException ignored) { localCommandHandler = new StdCommandHandler(this, true); LogHelper.warning("JLine2 isn't in classpath, using std"); } commandHandler = localCommandHandler; // Set key pair if (IOHelper.isFile(publicKeyFile) && IOHelper.isFile(privateKeyFile)) { LogHelper.info("Reading RSA keypair"); publicKey = SecurityHelper.toPublicRSAKey(IOHelper.read(publicKeyFile)); privateKey = SecurityHelper.toPrivateRSAKey(IOHelper.read(privateKeyFile)); if (!publicKey.getModulus().equals(privateKey.getModulus())) throw new IOException("Private and public key modulus mismatch"); } else { LogHelper.info("Generating RSA keypair"); KeyPair pair = SecurityHelper.genRSAKeyPair(); publicKey = (RSAPublicKey) pair.getPublic(); privateKey = (RSAPrivateKey) pair.getPrivate(); // Write key pair list LogHelper.info("Writing RSA keypair list"); IOHelper.write(publicKeyFile, publicKey.getEncoded()); IOHelper.write(privateKeyFile, privateKey.getEncoded()); } // Print keypair fingerprints CRC32 crc = new CRC32(); crc.update(publicKey.getModulus().toByteArray()); // IDEA говорит, что это Java 9 API. WTF? LogHelper.subInfo("Modulus CRC32: 0x%08x", crc.getValue()); // pre init modules modulesManager = new ModulesManager(this); modulesManager.autoload(dir.resolve("modules")); modulesManager.preInitModules(); initGson(); // Read LaunchServer config generateConfigIfNotExists(); LogHelper.info("Reading LaunchServer config file"); try (BufferedReader reader = IOHelper.newReader(configFile)) { config = Launcher.gson.fromJson(reader, Config.class); } config.verify(); Launcher.applyLauncherEnv(config.env); for (AuthProvider provider : config.authProvider) { provider.init(); } config.authHandler.init(); // build hooks, anti-brutforce and other buildHookManager = new BuildHookManager(); limiter = new AuthLimiter(this); proguardConf = new ProguardConf(this); sessionManager = new SessionManager(); mirrorManager = new MirrorManager(); reloadManager = new ReloadManager(); reconfigurableManager = new ReconfigurableManager(); socketHookManager = new SocketHookManager(); authHookManager = new AuthHookManager(); GarbageManager.registerNeedGC(sessionManager); GarbageManager.registerNeedGC(limiter); if (config.permissionsHandler instanceof Reloadable) reloadManager.registerReloadable("permissionsHandler", (Reloadable) config.permissionsHandler); if (config.authHandler instanceof Reloadable) reloadManager.registerReloadable("authHandler", (Reloadable) config.authHandler); for (int i = 0; i < config.authProvider.length; ++i) { AuthProvider provider = config.authProvider[i]; if (provider instanceof Reloadable) reloadManager.registerReloadable("authHandler".concat(String.valueOf(i)), (Reloadable) provider); } if (config.textureProvider instanceof Reloadable) reloadManager.registerReloadable("textureProvider", (Reloadable) config.textureProvider); Arrays.stream(config.mirrors).forEach(mirrorManager::addMirror); if (config.permissionsHandler instanceof Reconfigurable) reconfigurableManager.registerReconfigurable("permissionsHandler", (Reconfigurable) config.permissionsHandler); if (config.authHandler instanceof Reconfigurable) reconfigurableManager.registerReconfigurable("authHandler", (Reconfigurable) config.authHandler); for (int i = 0; i < config.authProvider.length; ++i) { AuthProvider provider = config.authProvider[i]; if (provider instanceof Reconfigurable) reconfigurableManager.registerReconfigurable("authHandler".concat(String.valueOf(i)), (Reconfigurable) provider); } if (config.textureProvider instanceof Reconfigurable) reconfigurableManager.registerReconfigurable("textureProvider", (Reconfigurable) config.textureProvider); Arrays.stream(config.mirrors).forEach(mirrorManager::addMirror); // init modules modulesManager.initModules(); // Set launcher EXE binary launcherBinary = new JARLauncherBinary(this); launcherEXEBinary = binary(); launcherBinary.init(); launcherEXEBinary.init(); syncLauncherBinaries(); // Sync updates dir if (!IOHelper.isDir(updatesDir)) Files.createDirectory(updatesDir); syncUpdatesDir(null); // Sync profiles dir if (!IOHelper.isDir(profilesDir)) Files.createDirectory(profilesDir); syncProfilesDir(); // Set server socket thread serverSocketHandler = new ServerSocketHandler(this, sessionManager); // post init modules modulesManager.postInitModules(); // start updater this.updater = new Updater(this); if(config.netty != null) nettyServerSocketHandler = new NettyServerSocketHandler(this); else nettyServerSocketHandler = null; } public static void initGson() { if (Launcher.gson != null) return; Launcher.gsonBuilder = new GsonBuilder(); Launcher.gsonBuilder.registerTypeAdapter(AuthProvider.class, new AuthProviderAdapter()); Launcher.gsonBuilder.registerTypeAdapter(TextureProvider.class, new TextureProviderAdapter()); Launcher.gsonBuilder.registerTypeAdapter(AuthHandler.class, new AuthHandlerAdapter()); Launcher.gsonBuilder.registerTypeAdapter(PermissionsHandler.class, new PermissionsHandlerAdapter()); Launcher.gsonBuilder.registerTypeAdapter(HWIDHandler.class, new HWIDHandlerAdapter()); Launcher.gson = Launcher.gsonBuilder.create(); //Human readable LaunchServer.gsonBuilder = new GsonBuilder(); LaunchServer.gsonBuilder.setPrettyPrinting(); LaunchServer.gsonBuilder.registerTypeAdapter(AuthProvider.class, new AuthProviderAdapter()); LaunchServer.gsonBuilder.registerTypeAdapter(TextureProvider.class, new TextureProviderAdapter()); LaunchServer.gsonBuilder.registerTypeAdapter(AuthHandler.class, new AuthHandlerAdapter()); LaunchServer.gsonBuilder.registerTypeAdapter(PermissionsHandler.class, new PermissionsHandlerAdapter()); LaunchServer.gsonBuilder.registerTypeAdapter(HWIDHandler.class, new HWIDHandlerAdapter()); LaunchServer.gson = LaunchServer.gsonBuilder.create(); } private LauncherBinary binary() { try { Class.forName("net.sf.launch4j.Builder"); if (config.launch4j.enabled) return new EXEL4JLauncherBinary(this); } catch (ClassNotFoundException ignored) { LogHelper.warning("Launch4J isn't in classpath."); } return new EXELauncherBinary(this); } public void buildLauncherBinaries() throws IOException { launcherBinary.build(); launcherEXEBinary.build(); } public void close() { serverSocketHandler.close(); // Close handlers & providers try { config.authHandler.close(); } catch (IOException e) { LogHelper.error(e); } try { for (AuthProvider p : config.authProvider) p.close(); } catch (IOException e) { LogHelper.error(e); } try { config.textureProvider.close(); } catch (IOException e) { LogHelper.error(e); } config.hwidHandler.close(); modulesManager.close(); // Print last message before death :( LogHelper.info("LaunchServer stopped"); } private void generateConfigIfNotExists() throws IOException { if (IOHelper.isFile(configFile)) return; // Create new config LogHelper.info("Creating LaunchServer config"); Config newConfig = new Config(); newConfig.mirrors = new String[]{"http://mirror.gravitlauncher.ml/"}; newConfig.launch4j = new ExeConf(); newConfig.launch4j.copyright = "© GravitLauncher Team"; newConfig.launch4j.fileDesc = "GravitLauncher ".concat(Launcher.getVersion().getVersionString()); newConfig.launch4j.fileVer = Launcher.getVersion().getVersionString().concat(".").concat(String.valueOf(Launcher.getVersion().patch)); newConfig.launch4j.internalName = "Launcher"; newConfig.launch4j.trademarks = "This product is licensed under GPLv3"; newConfig.launch4j.txtFileVersion = "%s, build %d"; newConfig.launch4j.txtProductVersion = "%s, build %d"; newConfig.launch4j.productName = "GravitLauncher"; newConfig.launch4j.productVer = newConfig.launch4j.fileVer; newConfig.env = LauncherConfig.LauncherEnvironment.STD; newConfig.startScript = JVMHelper.OS_TYPE.equals(JVMHelper.OS.MUSTDIE) ? "." + File.separator + "start.bat" : "." + File.separator + "start.sh"; newConfig.authHandler = new MemoryAuthHandler(); newConfig.hwidHandler = new AcceptHWIDHandler(); newConfig.authProvider = new AuthProvider[]{new RejectAuthProvider("Настройте authProvider")}; newConfig.textureProvider = new RequestTextureProvider("http://example.com/skins/%username%.png", "http://example.com/cloaks/%username%.png"); newConfig.permissionsHandler = new JsonFilePermissionsHandler(); newConfig.port = 7240; newConfig.bindAddress = "0.0.0.0"; newConfig.authRejectString = "Превышен лимит авторизаций"; newConfig.binaryName = "Launcher"; newConfig.whitelistRejectString = "Вас нет в белом списке"; newConfig.threadCoreCount = 0; // on your own newConfig.threadCount = JVMHelper.OPERATING_SYSTEM_MXBEAN.getAvailableProcessors() >= 4 ? JVMHelper.OPERATING_SYSTEM_MXBEAN.getAvailableProcessors() / 2 : JVMHelper.OPERATING_SYSTEM_MXBEAN.getAvailableProcessors(); newConfig.enabledProGuard = true; newConfig.stripLineNumbers = true; newConfig.deleteTempFiles = true; newConfig.isWarningMissArchJava = true; // Set server address System.out.println("LaunchServer address: "); newConfig.setAddress(commandHandler.readLine()); System.out.println("LaunchServer projectName: "); newConfig.setProjectName(commandHandler.readLine()); // Write LaunchServer config LogHelper.info("Writing LaunchServer config file"); try (BufferedWriter writer = IOHelper.newWriter(configFile)) { LaunchServer.gson.toJson(newConfig, writer); } } public Collection getProfiles() { return profilesList; } public SignedObjectHolder getUpdateDir(String name) { return updatesDirMap.get(name); } public Set>> getUpdateDirs() { return updatesDirMap.entrySet(); } public void rebindServerSocket() { serverSocketHandler.close(); CommonHelper.newThread("Server Socket Thread", false, serverSocketHandler).start(); } public void rebindNettyServerSocket() { nettyServerSocketHandler.close(); CommonHelper.newThread("Netty Server Socket Thread", false, nettyServerSocketHandler).start(); } @Override public void run() { if (started.getAndSet(true)) throw new IllegalStateException("LaunchServer has been already started"); // Add shutdown hook, then start LaunchServer JVMHelper.RUNTIME.addShutdownHook(CommonHelper.newThread(null, false, this::close)); CommonHelper.newThread("Command Thread", true, commandHandler).start(); rebindServerSocket(); if(config.netty != null) rebindNettyServerSocket(); modulesManager.finishModules(); } public void syncLauncherBinaries() throws IOException { LogHelper.info("Syncing launcher binaries"); // Syncing launcher binary LogHelper.info("Syncing launcher binary file"); if (!launcherBinary.sync()) LogHelper.warning("Missing launcher binary file"); // Syncing launcher EXE binary LogHelper.info("Syncing launcher EXE binary file"); if (!launcherEXEBinary.sync() && config.launch4j.enabled) LogHelper.warning("Missing launcher EXE binary file"); } public void syncProfilesDir() throws IOException { LogHelper.info("Syncing profiles dir"); List newProfies = new LinkedList<>(); IOHelper.walk(profilesDir, new ProfilesFileVisitor(newProfies), false); // Sort and set new profiles newProfies.sort(Comparator.comparing(a -> a)); profilesList = Collections.unmodifiableList(newProfies); } public void syncUpdatesDir(Collection dirs) throws IOException { LogHelper.info("Syncing updates dir"); Map> newUpdatesDirMap = new HashMap<>(16); try (DirectoryStream dirStream = Files.newDirectoryStream(updatesDir)) { for (Path updateDir : dirStream) { if (Files.isHidden(updateDir)) continue; // Skip hidden // Resolve name and verify is dir String name = IOHelper.getFileName(updateDir); if (!IOHelper.isDir(updateDir)) { LogHelper.warning("Not update dir: '%s'", name); continue; } // Add from previous map (it's guaranteed to be non-null) if (dirs != null && !dirs.contains(name)) { SignedObjectHolder hdir = updatesDirMap.get(name); if (hdir != null) { newUpdatesDirMap.put(name, hdir); continue; } } // Sync and sign update dir LogHelper.info("Syncing '%s' update dir", name); HashedDir updateHDir = new HashedDir(updateDir, null, true, true); newUpdatesDirMap.put(name, new SignedObjectHolder<>(updateHDir, privateKey)); } } updatesDirMap = Collections.unmodifiableMap(newUpdatesDirMap); } public void restart() { ProcessBuilder builder = new ProcessBuilder(); if (config.startScript != null) builder.command(Collections.singletonList(config.startScript)); else throw new IllegalArgumentException("Please create start script and link it as startScript in config."); builder.directory(this.dir.toFile()); builder.inheritIO(); builder.redirectErrorStream(true); builder.redirectOutput(Redirect.PIPE); try { builder.start(); } catch (IOException e) { LogHelper.error(e); } } public void fullyRestart() { restart(); JVMHelper.RUNTIME.exit(0); } }