Ядро
Основное ядро, модуль - 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()
));
