244 lines
11 KiB
Java
244 lines
11 KiB
Java
package pro.gravit.launchserver.binary.tasks;
|
|
|
|
import org.apache.logging.log4j.LogManager;
|
|
import org.apache.logging.log4j.Logger;
|
|
import org.objectweb.asm.ClassReader;
|
|
import org.objectweb.asm.ClassWriter;
|
|
import org.objectweb.asm.Type;
|
|
import org.objectweb.asm.tree.AnnotationNode;
|
|
import org.objectweb.asm.tree.ClassNode;
|
|
import org.objectweb.asm.tree.FieldNode;
|
|
import pro.gravit.launcher.base.Launcher;
|
|
import pro.gravit.launcher.base.LauncherConfig;
|
|
import pro.gravit.launchserver.LaunchServer;
|
|
import pro.gravit.launchserver.asm.ClassMetadataReader;
|
|
import pro.gravit.launchserver.asm.InjectClassAcceptor;
|
|
import pro.gravit.launchserver.asm.SafeClassWriter;
|
|
import pro.gravit.launchserver.binary.BuildContext;
|
|
import pro.gravit.utils.HookException;
|
|
import pro.gravit.utils.helper.IOHelper;
|
|
import pro.gravit.utils.helper.SecurityHelper;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.file.Path;
|
|
import java.security.cert.CertificateEncodingException;
|
|
import java.util.*;
|
|
import java.util.jar.JarFile;
|
|
import java.util.stream.Collectors;
|
|
import java.util.zip.ZipOutputStream;
|
|
|
|
public class MainBuildTask implements LauncherBuildTask {
|
|
public final ClassMetadataReader reader;
|
|
public final Set<String> blacklist = new HashSet<>();
|
|
public final List<Transformer> transformers = new ArrayList<>();
|
|
public final IOHookSet<BuildContext> preBuildHook = new IOHookSet<>();
|
|
public final IOHookSet<BuildContext> postBuildHook = new IOHookSet<>();
|
|
public final Map<String, Object> properties = new HashMap<>();
|
|
private final LaunchServer server;
|
|
private transient final Logger logger = LogManager.getLogger();
|
|
|
|
public MainBuildTask(LaunchServer srv) {
|
|
server = srv;
|
|
reader = new ClassMetadataReader();
|
|
InjectClassAcceptor injectClassAcceptor = new InjectClassAcceptor(properties);
|
|
transformers.add(injectClassAcceptor);
|
|
}
|
|
|
|
@Override
|
|
public String getName() {
|
|
return "MainBuild";
|
|
}
|
|
|
|
@Override
|
|
public Path process(Path inputJar) throws IOException {
|
|
Path outputJar = server.launcherBinary.nextPath(this);
|
|
try (ZipOutputStream output = new ZipOutputStream(IOHelper.newOutput(outputJar))) {
|
|
BuildContext context = new BuildContext(output, reader.getCp(), this);
|
|
initProps();
|
|
preBuildHook.hook(context);
|
|
properties.put("launcher.legacymodules", context.legacyClientModules.stream().map(e -> Type.getObjectType(e.replace('.', '/'))).collect(Collectors.toList()));
|
|
properties.put("launcher.modules", context.clientModules.stream().map(e -> Type.getObjectType(e.replace('.', '/'))).collect(Collectors.toList()));
|
|
postInitProps();
|
|
reader.getCp().add(new JarFile(inputJar.toFile()));
|
|
for (Path e : server.launcherBinary.coreLibs) {
|
|
reader.getCp().add(new JarFile(e.toFile()));
|
|
}
|
|
context.pushJarFile(inputJar, (e) -> blacklist.contains(e.getName()) || e.getName().startsWith("pro/gravit/launcher/debug/"), (e) -> true);
|
|
|
|
// map for guard
|
|
Map<String, byte[]> runtime = new HashMap<>(256);
|
|
// Write launcher guard dir
|
|
if (server.config.launcher.encryptRuntime) {
|
|
context.pushEncryptedDir(server.launcherBinary.runtimeDir, Launcher.RUNTIME_DIR, server.runtime.runtimeEncryptKey, runtime, false);
|
|
} else {
|
|
context.pushDir(server.launcherBinary.runtimeDir, Launcher.RUNTIME_DIR, runtime, false);
|
|
}
|
|
|
|
LauncherConfig launcherConfig = new LauncherConfig(server.config.netty.address, server.keyAgreementManager.ecdsaPublicKey, server.keyAgreementManager.rsaPublicKey, runtime, server.config.projectName);
|
|
context.pushFile(Launcher.CONFIG_FILE, launcherConfig);
|
|
postBuildHook.hook(context);
|
|
}
|
|
reader.close();
|
|
return outputJar;
|
|
}
|
|
|
|
protected void postInitProps() {
|
|
List<byte[]> certificates = Arrays.stream(server.certificateManager.trustManager.getTrusted()).map(e -> {
|
|
try {
|
|
return e.getEncoded();
|
|
} catch (CertificateEncodingException e2) {
|
|
logger.error("Certificate encoding failed", e2);
|
|
return new byte[0];
|
|
}
|
|
}).collect(Collectors.toList());
|
|
if (!server.config.sign.enabled) {
|
|
CertificateAutogenTask task = server.launcherBinary.getTaskByClass(CertificateAutogenTask.class).get();
|
|
try {
|
|
certificates.add(task.certificate.getEncoded());
|
|
} catch (CertificateEncodingException e) {
|
|
throw new InternalError(e);
|
|
}
|
|
}
|
|
properties.put("launchercore.certificates", certificates);
|
|
}
|
|
|
|
protected void initProps() {
|
|
properties.clear();
|
|
properties.put("launcher.address", server.config.netty.address);
|
|
properties.put("launcher.projectName", server.config.projectName);
|
|
properties.put("runtimeconfig.secretKeyClient", SecurityHelper.randomStringAESKey());
|
|
properties.put("launcher.port", 32148 + SecurityHelper.newRandom().nextInt(512));
|
|
properties.put("launchercore.env", server.config.env);
|
|
properties.put("launcher.memory", server.config.launcher.memoryLimit);
|
|
properties.put("launcher.customJvmOptions", server.config.launcher.customJvmOptions);
|
|
if (server.config.launcher.encryptRuntime) {
|
|
if (server.runtime.runtimeEncryptKey == null)
|
|
server.runtime.runtimeEncryptKey = SecurityHelper.randomStringToken();
|
|
properties.put("runtimeconfig.runtimeEncryptKey", server.runtime.runtimeEncryptKey);
|
|
}
|
|
properties.put("launcher.certificatePinning", server.config.launcher.certificatePinning);
|
|
properties.put("runtimeconfig.passwordEncryptKey", server.runtime.passwordEncryptKey);
|
|
String launcherSalt = SecurityHelper.randomStringToken();
|
|
byte[] launcherSecureHash = SecurityHelper.digest(SecurityHelper.DigestAlgorithm.SHA256,
|
|
server.runtime.clientCheckSecret.concat(".").concat(launcherSalt));
|
|
properties.put("runtimeconfig.secureCheckHash", Base64.getEncoder().encodeToString(launcherSecureHash));
|
|
properties.put("runtimeconfig.secureCheckSalt", launcherSalt);
|
|
if (server.runtime.unlockSecret == null) server.runtime.unlockSecret = SecurityHelper.randomStringToken();
|
|
properties.put("runtimeconfig.unlockSecret", server.runtime.unlockSecret);
|
|
server.runtime.buildNumber++;
|
|
properties.put("runtimeconfig.buildNumber", server.runtime.buildNumber);
|
|
}
|
|
|
|
public byte[] transformClass(byte[] bytes, String classname, BuildContext context) {
|
|
byte[] result = bytes;
|
|
ClassWriter writer;
|
|
ClassNode cn = null;
|
|
for (Transformer t : transformers) {
|
|
if (t instanceof ASMTransformer asmTransformer) {
|
|
if (cn == null) {
|
|
ClassReader cr = new ClassReader(result);
|
|
cn = new ClassNode();
|
|
cr.accept(cn, 0);
|
|
}
|
|
asmTransformer.transform(cn, classname, context);
|
|
continue;
|
|
} else if (cn != null) {
|
|
writer = new SafeClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
|
cn.accept(writer);
|
|
result = writer.toByteArray();
|
|
}
|
|
byte[] old_result = result;
|
|
result = t.transform(result, classname, context);
|
|
if (old_result != result) {
|
|
cn = null;
|
|
}
|
|
}
|
|
if (cn != null) {
|
|
writer = new SafeClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
|
cn.accept(writer);
|
|
result = writer.toByteArray();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@FunctionalInterface
|
|
public interface Transformer {
|
|
byte[] transform(byte[] input, String classname, BuildContext context);
|
|
}
|
|
|
|
public interface ASMTransformer extends Transformer {
|
|
default byte[] transform(byte[] input, String classname, BuildContext context) {
|
|
ClassReader reader = new ClassReader(input);
|
|
ClassNode cn = new ClassNode();
|
|
reader.accept(cn, 0);
|
|
transform(cn, classname, context);
|
|
SafeClassWriter writer = new SafeClassWriter(context.task.reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
|
cn.accept(writer);
|
|
return writer.toByteArray();
|
|
}
|
|
|
|
void transform(ClassNode cn, String classname, BuildContext context);
|
|
}
|
|
|
|
public static class IOHookSet<R> {
|
|
public final Set<IOHook<R>> list = new HashSet<>();
|
|
|
|
public void registerHook(IOHook<R> hook) {
|
|
list.add(hook);
|
|
}
|
|
|
|
public boolean unregisterHook(IOHook<R> hook) {
|
|
return list.remove(hook);
|
|
}
|
|
|
|
/**
|
|
* @param context custom param
|
|
* False to continue
|
|
* @throws HookException The hook may return the error text throwing this exception
|
|
*/
|
|
public void hook(R context) throws HookException, IOException {
|
|
for (IOHook<R> hook : list) {
|
|
hook.hook(context);
|
|
}
|
|
}
|
|
|
|
@FunctionalInterface
|
|
public interface IOHook<R> {
|
|
/**
|
|
* @param context custom param
|
|
* False to continue processing hook
|
|
* @throws HookException The hook may return the error text throwing this exception
|
|
*/
|
|
void hook(R context) throws HookException, IOException;
|
|
}
|
|
}
|
|
|
|
public abstract static class ASMAnnotationFieldProcessor implements ASMTransformer {
|
|
private final String desc;
|
|
|
|
protected ASMAnnotationFieldProcessor(String desc) {
|
|
this.desc = desc;
|
|
}
|
|
|
|
@Override
|
|
public void transform(ClassNode cn, String classname, BuildContext context) {
|
|
for (FieldNode fn : cn.fields) {
|
|
if (fn.invisibleAnnotations == null || fn.invisibleAnnotations.isEmpty()) continue;
|
|
AnnotationNode found = null;
|
|
for (AnnotationNode an : fn.invisibleAnnotations) {
|
|
if (an == null) continue;
|
|
if (desc.equals(an.desc)) {
|
|
found = an;
|
|
break;
|
|
}
|
|
}
|
|
if (found != null) {
|
|
transformField(found, fn, cn, classname, context);
|
|
}
|
|
}
|
|
}
|
|
|
|
abstract public void transformField(AnnotationNode an, FieldNode fn, ClassNode cn, String classname, BuildContext context);
|
|
}
|
|
}
|