[FEATURE] LauncherAgent переделан. (#213)

* [REFACTOR] Рефакторинг.

* [FEATURE] LauncherAgent переделан.

* [FIX] Подача нужных агенту аргументов.

* [FEATURE] Вы можете поставить листы ClientProfile модулем.

* [FIX] Скрипты gradle, для работы LauncherTest.

* [FEATURE] Система тестов - начало.

* [FIX] Perms handler для тестовю
This commit is contained in:
Zaxar163 2019-04-07 10:53:39 +03:00 committed by Gravit
parent f747ef55d1
commit 0094c3b613
17 changed files with 296 additions and 98 deletions

View file

@ -15,6 +15,7 @@
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.DefaultPermissionsHandler;
import ru.gravit.launchserver.auth.permissions.JsonFilePermissionsHandler;
import ru.gravit.launchserver.auth.permissions.PermissionsHandler;
import ru.gravit.launchserver.auth.protect.NoProtectHandler;
@ -45,6 +46,7 @@
import java.io.File;
import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;
import java.lang.reflect.InvocationTargetException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.file.*;
@ -308,7 +310,7 @@ public static void main(String... args) throws Throwable {
long startTime = System.currentTimeMillis();
try {
@SuppressWarnings("resource")
LaunchServer launchserver = new LaunchServer(IOHelper.WORKING_DIR, args);
LaunchServer launchserver = new LaunchServer(IOHelper.WORKING_DIR, false, args);
if (args.length == 0) launchserver.run();
else { //Обработка команды
launchserver.commandHandler.eval(args, false);
@ -325,6 +327,8 @@ public static void main(String... args) throws Throwable {
public final Path dir;
public final boolean testEnv;
public final Path launcherLibraries;
public final Path launcherLibrariesCompile;
@ -354,6 +358,8 @@ public static void main(String... args) throws Throwable {
public final JARLauncherBinary launcherBinary;
public Class<? extends LauncherBinary> launcherEXEBinaryClass;
public final LauncherBinary launcherEXEBinary;
// HWID ban + anti-brutforce
@ -397,8 +403,11 @@ public static void main(String... args) throws Throwable {
public static Gson gson;
public static GsonBuilder gsonBuilder;
public LaunchServer(Path dir, String[] args) throws IOException, InvalidKeySpecException {
public static Class<? extends LauncherBinary> defaultLauncherEXEBinaryClass = null;
public LaunchServer(Path dir, boolean testEnv, String[] args) throws IOException, InvalidKeySpecException {
this.dir = dir;
this.testEnv = testEnv;
taskPool = new Timer("Timered task worker thread", true);
launcherLibraries = dir.resolve("launcher-libraries");
launcherLibrariesCompile = dir.resolve("launcher-libraries-compile");
@ -422,16 +431,19 @@ public LaunchServer(Path dir, String[] args) throws IOException, InvalidKeySpecE
// Set command handler
CommandHandler localCommandHandler;
try {
Class.forName("jline.Terminal");
if (testEnv)
localCommandHandler = new StdCommandHandler(false);
else
try {
Class.forName("jline.Terminal");
// JLine2 available
localCommandHandler = new JLineCommandHandler();
LogHelper.info("JLine2 terminal enabled");
} catch (ClassNotFoundException ignored) {
localCommandHandler = new StdCommandHandler(true);
LogHelper.warning("JLine2 isn't in classpath, using std");
}
// JLine2 available
localCommandHandler = new JLineCommandHandler();
LogHelper.info("JLine2 terminal enabled");
} catch (ClassNotFoundException ignored) {
localCommandHandler = new StdCommandHandler(true);
LogHelper.warning("JLine2 isn't in classpath, using std");
}
ru.gravit.launchserver.command.handler.CommandHandler.registerCommands(localCommandHandler);
commandHandler = localCommandHandler;
@ -459,6 +471,9 @@ public LaunchServer(Path dir, String[] args) throws IOException, InvalidKeySpecE
crc.update(publicKey.getModulus().toByteArray()); // IDEA говорит, что это Java 9 API. WTF?
LogHelper.subInfo("Modulus CRC32: 0x%08x", crc.getValue());
// Load class bindings.
launcherEXEBinaryClass = defaultLauncherEXEBinaryClass;
// pre init modules
modulesManager = new ModulesManager(this);
modulesManager.autoload(dir.resolve("modules"));
@ -466,7 +481,7 @@ public LaunchServer(Path dir, String[] args) throws IOException, InvalidKeySpecE
initGson();
// Read LaunchServer config
generateConfigIfNotExists();
generateConfigIfNotExists(testEnv);
LogHelper.info("Reading LaunchServer config file");
try (BufferedReader reader = IOHelper.newReader(configFile)) {
config = Launcher.gson.fromJson(reader, Config.class);
@ -589,6 +604,14 @@ public static void initGson() {
}
private LauncherBinary binary() {
if (launcherEXEBinaryClass != null) {
try {
return (LauncherBinary)launcherEXEBinaryClass.getConstructor(LaunchServer.class).newInstance(this);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
LogHelper.error(e);
}
}
try {
Class.forName("net.sf.launch4j.Builder");
if (config.launch4j.enabled) return new EXEL4JLauncherBinary(this);
@ -614,7 +637,7 @@ public void close() {
LogHelper.info("LaunchServer stopped");
}
private void generateConfigIfNotExists() throws IOException {
private void generateConfigIfNotExists(boolean testEnv) throws IOException {
if (IOHelper.isFile(configFile))
return;
@ -640,7 +663,8 @@ private void generateConfigIfNotExists() throws IOException {
new RequestTextureProvider("http://example.com/skins/%username%.png", "http://example.com/cloaks/%username%.png")
, "std")};
newConfig.protectHandler = new NoProtectHandler();
newConfig.permissionsHandler = new JsonFilePermissionsHandler();
if (testEnv) newConfig.permissionsHandler = new DefaultPermissionsHandler();
else newConfig.permissionsHandler = new JsonFilePermissionsHandler();
newConfig.legacyPort = 7240;
newConfig.legacyBindAddress = "0.0.0.0";
newConfig.binaryName = "Launcher";
@ -674,10 +698,15 @@ private void generateConfigIfNotExists() throws IOException {
newConfig.components.put("authLimiter", authLimiterComponent);
// Set server address
System.out.println("LaunchServer address: ");
newConfig.setLegacyAddress(commandHandler.readLine());
System.out.println("LaunchServer projectName: ");
newConfig.setProjectName(commandHandler.readLine());
if (testEnv) {
newConfig.setLegacyAddress("localhost");
newConfig.setProjectName("test");
} else {
System.out.println("LaunchServer address: ");
newConfig.setLegacyAddress(commandHandler.readLine());
System.out.println("LaunchServer projectName: ");
newConfig.setProjectName(commandHandler.readLine());
}
// Write LaunchServer config
LogHelper.info("Writing LaunchServer config file");
@ -686,10 +715,13 @@ private void generateConfigIfNotExists() throws IOException {
}
}
public Collection<ClientProfile> getProfiles() {
public List<ClientProfile> getProfiles() {
return profilesList;
}
public void setProfiles(List<ClientProfile> profilesList) {
this.profilesList = Collections.unmodifiableList(profilesList);
}
public SignedObjectHolder<HashedDir> getUpdateDir(String name) {
return updatesDirMap.get(name);
@ -717,8 +749,10 @@ public void run() {
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();
if (!this.testEnv) {
JVMHelper.RUNTIME.addShutdownHook(CommonHelper.newThread(null, false, this::close));
CommonHelper.newThread("Command Thread", true, commandHandler).start();
}
rebindServerSocket();
if (config.netty != null)
rebindNettyServerSocket();

View file

@ -28,6 +28,7 @@
dependencies {
pack project(':LauncherAPI') // Not error on obf.
bundle 'com.github.oshi:oshi-core:3.13.0'
bundle 'org.ow2.asm:asm-tree:7.1'
}
task genRuntimeJS(type: Zip) {

View file

@ -40,7 +40,7 @@ public static void main(String[] arguments) throws IOException, InterruptedExcep
JVMHelper.addSystemPropertyToArgs(args, DirBridge.USE_OPTDIR_PROPERTY);
Collections.addAll(args, MAGIC_ARG);
Collections.addAll(args, "-XX:+DisableAttachMechanism");
Collections.addAll(args, "-javaagent:".concat(pathLauncher));
Collections.addAll(args, "-javaagent:".concat(pathLauncher).concat("=pr"));
Collections.addAll(args, "-cp");
Collections.addAll(args, pathLauncher);
Collections.addAll(args, LauncherEngine.class.getName());

View file

@ -0,0 +1,159 @@
package ru.gravit.launcher;
import ru.gravit.utils.helper.LogHelper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.Instrumentation;
import java.util.jar.JarFile;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.MethodNode;
import static org.objectweb.asm.Opcodes.*;
@LauncherAPI
public final class LauncherAgent {
private static boolean isAgentStarted = false;
public static Instrumentation inst;
public static void addJVMClassPath(String path) throws IOException {
LogHelper.debug("Launcher Agent addJVMClassPath");
inst.appendToSystemClassLoaderSearch(new JarFile(path));
}
public boolean isAgentStarted() {
return isAgentStarted;
}
public static void premain(String agentArgument, Instrumentation instrumentation) {
System.out.println("Launcher Agent");
inst = instrumentation;
isAgentStarted = true;
boolean pb = true;
boolean rt = true;
if (agentArgument != null) {
String trimmedArg = agentArgument.trim();
if (!trimmedArg.isEmpty()) {
if (trimmedArg.contains("p")) pb = false;
if (trimmedArg.contains("r")) rt = false;
}
}
replaceClasses(pb, rt);
}
public static boolean isStarted() {
return isAgentStarted;
}
/**
* @author https://github.com/Konloch/JVM-Sandbox
* Replaces the Runtime class via instrumentation, transforms the class via ASM
*/
public static void replaceClasses(boolean pb, boolean rt) {
for(Class<?> c : inst.getAllLoadedClasses()) {
if(rt && c.getName().equals("java.lang.Runtime")) {
try {
inst.redefineClasses(new java.lang.instrument.ClassDefinition(java.lang.Runtime.class, transformClass(c.getName(), getClassFile(c))));
} catch(Exception e) {
e.printStackTrace();
}
}
if(pb && c.getName().equals("java.lang.ProcessBuilder")) {
try {
inst.redefineClasses(new java.lang.instrument.ClassDefinition(java.lang.ProcessBuilder.class, transformClass(c.getName(), getClassFile(c))));
} catch(Exception e) {
e.printStackTrace();
}
}
if(c.getName().equals("java.awt.Robot")) {
try {
inst.redefineClasses(new java.lang.instrument.ClassDefinition(java.lang.ProcessBuilder.class, transformClass(c.getName(), getClassFile(c))));
} catch(Exception e) {
e.printStackTrace();
}
}
}
}
/**
* @author https://github.com/Konloch/JVM-Sandbox
* Use ASM to modify the byte array
*/
public static byte[] transformClass(String className, byte[] classBytes) {
if (className.equals("java.lang.Runtime")) {
ClassReader cr=new ClassReader(classBytes);
ClassNode cn=new ClassNode();
cr.accept(cn,ClassReader.EXPAND_FRAMES);
for (Object o : cn.methods.toArray()) {
MethodNode m = (MethodNode) o;
if(m.name.equals("exec")) {
m.instructions.insert(new InsnNode(ARETURN));
m.instructions.insert(new InsnNode(ACONST_NULL));
}
}
ClassWriter cw=new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
cn.accept(cw);
return cw.toByteArray();
} else if (className.equals("java.lang.ProcessBuilder")) {
ClassReader cr=new ClassReader(classBytes);
ClassNode cn=new ClassNode();
cr.accept(cn,ClassReader.EXPAND_FRAMES);
for (Object o : cn.methods.toArray()) {
MethodNode m = (MethodNode) o;
if(m.name.equals("start")) {
m.instructions.insert(new InsnNode(ARETURN));
m.instructions.insert(new InsnNode(ACONST_NULL));
}
}
ClassWriter cw=new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
cn.accept(cw);
return cw.toByteArray();
} else if (className.equals("java.awt.Robot")) {
ClassReader cr=new ClassReader(classBytes);
ClassNode cn=new ClassNode();
cr.accept(cn,ClassReader.EXPAND_FRAMES);
for (Object o : cn.methods.toArray()) {
MethodNode m = (MethodNode) o;
if( m.name.equals("createScreenCapture") || m.name.equals("getPixelColor") ||
m.name.equals("keyPress") || m.name.equals("keyRelease") ||
m.name.equals("mouseMove") || m.name.equals("mousePress") ||
m.name.equals("mouseWheel"))
{
m.instructions.insert(new InsnNode(ARETURN));
m.instructions.insert(new InsnNode(ACONST_NULL));
}
}
ClassWriter cw=new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
cn.accept(cw);
return cw.toByteArray();
}
return classBytes;
}
/**
* @author https://github.com/Konloch/JVM-Sandbox
* Do not remove this method. Do not to cause classloading!
* Grab the byte array from the loaded Class object
* @param clazz
* @return array, respending this class in bytecode.
* @throws IOException
*/
public static byte[] getClassFile(Class<?> clazz) throws IOException {
InputStream is = clazz.getResourceAsStream( "/" + clazz.getName().replace('.', '/') + ".class");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int r = 0;
byte[] buffer = new byte[8192];
while((r=is.read(buffer))>=0) {
baos.write(buffer, 0, r);
}
return baos.toByteArray();
}
}

View file

@ -14,7 +14,6 @@
import ru.gravit.launcher.request.websockets.RequestInterface;
import ru.gravit.launcher.serialize.signed.SignedObjectHolder;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

View file

@ -8,12 +8,10 @@
import ru.gravit.launcher.request.websockets.RequestInterface;
public class SetProfileRequest extends Request<SetProfileRequestEvent> implements RequestInterface {
private transient ClientProfile profile;
@LauncherNetworkAPI
public String client;
public SetProfileRequest(ClientProfile profile) {
this.profile = profile;
this.client = profile.getTitle();
}

View file

@ -10,7 +10,6 @@
import ru.gravit.launcher.hasher.HashedEntry;
import ru.gravit.launcher.hasher.HashedFile;
import ru.gravit.launcher.request.Request;
import ru.gravit.launcher.request.UpdateAction;
import ru.gravit.launcher.request.update.UpdateRequest.State.Callback;
import ru.gravit.launcher.request.websockets.LegacyRequestBridge;
import ru.gravit.launcher.request.websockets.RequestInterface;
@ -26,7 +25,6 @@
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Queue;
public final class UpdateRequest extends Request<UpdateRequestEvent> implements RequestInterface {
@ -168,26 +166,6 @@ public double getTotalSizeMiB() {
}
}
private static void fillActionsQueue(Queue<UpdateAction> queue, HashedDir mismatch) {
for (Entry<String, HashedEntry> mapEntry : mismatch.map().entrySet()) {
String name = mapEntry.getKey();
HashedEntry entry = mapEntry.getValue();
HashedEntry.Type entryType = entry.getType();
switch (entryType) {
case DIR: // cd - get - cd ..
queue.add(new UpdateAction(UpdateAction.Type.CD, name, entry));
fillActionsQueue(queue, (HashedDir) entry);
queue.add(UpdateAction.CD_BACK);
break;
case FILE: // get
queue.add(new UpdateAction(UpdateAction.Type.GET, name, entry));
break;
default:
throw new AssertionError("Unsupported hashed entry type: " + entryType.name());
}
}
}
@Override
public UpdateRequestEvent requestDo() throws Exception {
LogHelper.debug("Start update request");

View file

@ -0,0 +1,23 @@
package ru.gravit.launcher.test.utils;
import ru.gravit.launchserver.LaunchServer;
import ru.gravit.launchserver.binary.LauncherBinary;
import ru.gravit.utils.helper.IOHelper;
import java.io.IOException;
import java.nio.file.Files;
public class EXENonWarningLauncherBinary extends LauncherBinary {
public EXENonWarningLauncherBinary(LaunchServer server) {
super(server, server.dir.resolve(server.config.binaryName + ".exe"));
}
@Override
public void build() throws IOException {
if (IOHelper.isFile(syncBinaryFile)) {
Files.delete(syncBinaryFile);
}
}
}

View file

@ -0,0 +1,36 @@
package ru.gravit.launcher;
import java.io.IOException;
import java.nio.file.Path;
import java.security.spec.InvalidKeySpecException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import ru.gravit.launcher.test.utils.EXENonWarningLauncherBinary;
import ru.gravit.launchserver.LaunchServer;
import ru.gravit.utils.helper.LogHelper;
public class StartTest {
@TempDir
public Path dir;
@BeforeAll
public static void prepare() {
LogHelper.removeStdOutput();
LaunchServer.defaultLauncherEXEBinaryClass = EXENonWarningLauncherBinary.class;
}
@Test
public void checkLaunchServerStarts() {
try {
LaunchServer srv = new LaunchServer(dir, true, new String[] { "checkInstall" });
srv.run();
srv.commandHandler.eval(new String[] { "checkInstall" }, false);
srv.close();
} catch (InvalidKeySpecException | IOException e) {
throw new RuntimeException(e);
}
}
}

2
Radon

@ -1 +1 @@
Subproject commit e1f7548f97132fa29b01f68bd57ffab32de7b6e7
Subproject commit e6974532efc7228a8b826a8cd9e1f1f7b609659f

View file

@ -37,7 +37,7 @@ public class ServerWrapper extends JsonConfigurable<ServerWrapper.Config> {
public ClassLoader loader;
public ClientPermissions permissions;
public static ServerWrapper wrapper;
private static Gson gson;
public static Gson gson;
private static GsonBuilder gsonBuiler;
public static Path modulesDir = Paths.get(System.getProperty("serverwrapper.modulesDir", "modules"));

View file

@ -1,6 +1,5 @@
package ru.gravit.launcher.server.setup;
import ru.gravit.launcher.LauncherConfig;
import ru.gravit.launcher.server.ServerWrapper;
import ru.gravit.utils.PublicURLClassLoader;
import ru.gravit.utils.helper.IOHelper;
@ -24,20 +23,22 @@ public void run() throws IOException {
System.out.println("Print jar filename:");
String jarName = commands.commandHandler.readLine();
Path jarPath = Paths.get(jarName);
JarFile file = new JarFile(jarPath.toFile());
URL jarURL = jarPath.toUri().toURL();
urlClassLoader = new PublicURLClassLoader(new URL[]{jarURL});
LogHelper.info("Check jar MainClass");
String mainClassName = file.getManifest().getMainAttributes().getValue("Main-Class");
if (mainClassName == null) {
LogHelper.error("Main-Class not found in MANIFEST");
return;
}
try {
Class mainClass = Class.forName(mainClassName, false, urlClassLoader);
} catch (ClassNotFoundException e) {
LogHelper.error(e);
return;
String mainClassName = null;
try (JarFile file = new JarFile(jarPath.toFile())) {
URL jarURL = jarPath.toUri().toURL();
urlClassLoader = new PublicURLClassLoader(new URL[]{jarURL});
LogHelper.info("Check jar MainClass");
mainClassName = file.getManifest().getMainAttributes().getValue("Main-Class");
if (mainClassName == null) {
LogHelper.error("Main-Class not found in MANIFEST");
return;
}
try {
Class.forName(mainClassName, false, urlClassLoader);
} catch (ClassNotFoundException e) {
LogHelper.error(e);
return;
}
}
LogHelper.info("Found MainClass %s", mainClassName);
System.out.println("Print launchserver websocket host:");
@ -66,7 +67,6 @@ public void run() throws IOException {
wrapper.config.password = password;
wrapper.config.title = title;
wrapper.config.stopOnError = false;
LauncherConfig cfg = null;
if (wrapper.auth()) {
break;

View file

@ -1,32 +0,0 @@
package ru.gravit.launcher;
import ru.gravit.utils.helper.LogHelper;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.util.jar.JarFile;
@LauncherAPI
public final class LauncherAgent {
private static boolean isAgentStarted = false;
public static Instrumentation inst;
public static void addJVMClassPath(String path) throws IOException {
LogHelper.debug("Launcher Agent addJVMClassPath");
inst.appendToSystemClassLoaderSearch(new JarFile(path));
}
public boolean isAgentStarted() {
return isAgentStarted;
}
public static void premain(String agentArgument, Instrumentation instrumentation) {
System.out.println("Launcher Agent");
inst = instrumentation;
isAgentStarted = true;
}
public static boolean isStarted() {
return isAgentStarted;
}
}

View file

@ -9,8 +9,9 @@
import java.util.HashMap;
import java.util.Objects;
@SuppressWarnings("rawtypes")
public class ConfigManager {
private final HashMap<String, JsonConfigurable> CONFIGURABLE = new HashMap<>();
private final HashMap<String, JsonConfigurable> CONFIGURABLE = new HashMap<>();
public void registerConfigurable(String name, JsonConfigurable reconfigurable) {
VerifyHelper.putIfAbsent(CONFIGURABLE, name.toLowerCase(), Objects.requireNonNull(reconfigurable, "adapter"),

View file

@ -7,6 +7,7 @@
include 'ServerWrapper'
include 'LaunchServer'
include 'LaunchServerConsole'
include 'LauncherTest'
include 'modules'
file('modules').eachDir { sub ->
if (sub.name.endsWith('_module')) include 'modules:' + sub.name