Ядро
Основное ядро, модуль - trambda-core
.
<dependency> <groupId>xyz.cofe</groupId> <artifactId>trambda-core</artifactId> <!-- Актуальную версию лучше поискать на https://oss.sonatype.org/ на https://search.maven.org/ --> <version>1.0-SNAPSHOT</version> </dependency>
Задачи модуля
- Получить байт-код лямбды переданной в качестве параметра
- (Де)Сериализация байт-кода
- Генерация класса JVM для сериализованного представления
- Функции проверки безопасности байт-кода
Получение байт-кода и его сериализация
Данной функций занимается класс xyz.cofe.trambda.AsmQuery
.
Данный класс реализует интерфейс xyz.cofe.trambda.Query<ENV>
:
/** * Общий интерфейс для вызова лямбды на сервере * @param <ENV> Сервис предоставляемый на сервере */ public interface Query<ENV> { /** * Вызов лямбды на сервере * @param fn лямбда * @param <RES> Сервис предоставляемый на сервере * @return результат вычисления на сервере */ public <RES> RES apply( Fn<ENV,RES> fn ); }
Fn
- Это функция от одного параметра:
package xyz.cofe.trambda; import java.io.Serializable; import java.util.function.Function; /** * Лямбда передаваемая на сервер * @param <A> Тип сервиса который доступен ра сервере * @param <Z> Тип результата возвращаемый с сервера */ public interface Fn<A,Z> extends Serializable, Function<A,Z> { /** * Вызов лямбды * @param a сервис передаваемый в лямбду * @return результата возвращаемый с сервера */ public Z apply(A a); }
Для того что бы получить байт-код лямбды, необходимо создать потомка от AsmQuery
public class AsmQuery<ENV> implements Query<ENV> { ... /** * Сериализация и вызов лямбды * @param fn лямбда * @param <RES> результат вызова * @return результат вызова */ @Override public <RES> RES apply(Fn<ENV, RES> fn){ ... } ... /** * Реализация вызова лямбды * @param fn лямбда * @param sl лямбда - сериализация * @param mdef байт-код лямбды * @param <RES> результат вызова * @return результат вызова */ protected <RES> RES call( Fn<ENV, RES> fn, SerializedLambda sl, MethodDef mdef ){ return null; } ... }
Это клас по сути является абстрактным, и для реализации конечной функциональности требуется переопределить метод call(Fn, SerializedLambda, MethodDef)
Данный класс по сути выполняет следующие функции:
- Метода
apply(Fn)
- получает байт код fn - Полученный байт код передает в
call(Fn, SerializedLambda, MethodDef)
AtomicReference<MethodDef> mdefRef = new AtomicReference<>(); var res = new AsmQuery<IEnv>(){ @Override protected RES call(Fn<IEnv, RES> fn, SerializedLambda sl, MethodDef mdef){ // Сохранение представления байт кода mdefRef.set(mdef); // ... return netClient.call(fn, sl, mdef); } }.apply( env0 -> env0.getUsers().filter( u -> u.getName().contains("Petrov") ) );
MethodDef
xyz.cofe.trambda.bc.MethodDef
- Это сериализованное представление байт-кода.
Генерация класса JVM для сериализованного представления
Для восстановления байт-кода из сериализованного представления используется класс xyz.cofe.trambda.MethodDefRestore
var byteCode = new MethodRestore() // Имя целевого класса .className("xyz.cofe.trambda.buildMethodTest.Build1") // Имя целевого метода .methodName("lambda1") // Сериализованная лямбда .methodDef(mdef) // Генерация байт кода .generate();
Затем можно для этого байткода, через собственный загрузчик классов, получить ссылку (java.lang.reflect.Method) на лямбду:
// Создаем свой Classloader, // через который будем загружать наш сгененированый класс ClassLoader cl = new ClassLoader(ClassLoader.getSystemClassLoader()) { @Override protected Class<?> findClass(String name) throws ClassNotFoundException{ if( name!=null && name.equals("xyz.cofe.trambda.buildMethodTest.Build1") ){ // Передача байт-кода в JVM return defineClass(name,byteCode,0,byteCode.length); } return super.findClass(name); } }; Class c = null; try{ // Загрузка класса из Classloader c = Class.forName("xyz.cofe.trambda.buildMethodTest.Build1",true,cl); } catch( ClassNotFoundException e ) { e.printStackTrace(); return; } // Ищем целевой метод, он по умолчанию должен быть static Method m = null; for( var delMeth : c.getDeclaredMethods() ){ if( delMeth.getName().equals("lambda1") ){ // Найден метод который реализует лямбду m = delMeth; } }
Функции проверки безопасности байт-кода
Реализована следующая проверка
- Проверка вызова метода
- Проверка чтения/записи в поля класса (field)
import xyz.cofe.trambda.sec.SecurityFilters; import xyz.cofe.trambda.sec.SecurityFilter; import xyz.cofe.trambda.sec.SecurAccess; var secAcc = SecurAccess.inspect(mdef); var sfilters = SecurityFilters.create() // Разрешаем вызовы .allow(a -> { // Java компилятор генерирует в байт коде вызовы // методов указанных классов a.invoke("Java compiler", call -> call.getOwner().equals("java.lang.invoke.StringConcatFactory")); a.invoke("Java compiler", call -> call.getOwner().equals("java.lang.invoke.LambdaMetafactory")); }) // Разрешаем вызовы .allow(a -> { // Доступ к stdio, для чтения a.field("System stdio", f -> f.getOwner().equals("java.lang.System") && f.isReadAccess()); // Доступ к java stream, java collection a.invoke("Java Streams", f -> f.getOwner().matches("java\\.io\\.[\\w\\d]*(Stream|Writer)[\\w\\d]*")); // Доступ к методам класса java.lang.String a.invoke("api Java lang", c -> c.getOwner().matches("java.lang.String")); }) // Запрещаем вызовы .deny(b -> { // Запрещаем изменять поля out, err, in класса System b.field("deny System field write", f -> f.getOwner().equals("java.lang.System") && f.isWriteAccess()); // Запрещаем вызывать чувствительные методы класса System b.invoke("deny call System method", f -> f.getOwner().equals("java.lang.System") && f.getMethodName().matches( "(?i)gc|exit|console|clear.*|" + "getSecurity.*|inherited.*|load.*|map.*|run.*|set.*|wait.*")); }) // Разрешаем вызовы нашего сервиса .allow( a -> { a.invoke("api by xyz.cofe", c -> c.getOwner().matches("xyz.cofe.iter.[\\w\\d]+")); a.invoke("api by xyz.cofe", c -> c.getOwner().matches("xyz.cofe.[\\w\\d]+")); a.invoke("api by trambda", c -> c.getOwner().matches("xyz.cofe.trambda.[\\w\\d]+")); }) // Запрещаем все остальные вызовы .deny().any("Deny by default") .build(); // Выполняем проверку байт-кода sfilters.validate(secAcc).forEach(sm -> System.out.println( "allow="+sm.isAllow()+ " message="+sm.getMessage()+ " access="+sm.getAccess() ));