как запустить groovy скрипт
Использование возможностей Groovy DSL для конфигурации Java-приложения
Предыстория
Всем привет! Я хотел бы рассказать историю о страшных конфигах и как их удалось причесать и сделать вменяемыми. Я работаю над довольно большим и относительно старым проектом, который постоянно допиливается и разрастается. Конфигурация задается с помощью маппинга xml-файлов на java-бины. Не самое лучшее решение, но оно имеет свои плюсы — например, при создании сервиса можно передать ему бин с конфигурацией, отвечающий за его раздел. Однако, есть и минусы. Самый существенный из них — нет нормального наследования профилей конфигурации. В какой-то момент я осознал, что для того, чтобы поменять одну настройку, я должен отредактировать около 30 xml-файлов, по одному для каждого из профилей. Так больше продолжаться не могло, и было принято волевое решение все переписать.
Требования
Хотелось бы, чтобы конфиг выглядел примерно так:
Как я этого добился — под катом.
Может, для этого уже есть библиотека?
Скорее всего, да. Однако, из тех, что я нашел и посмотрел, мне ничего не подошло. Большинство из них рассчитаны на чтение конфигов, объединение их в один большой и затем работу с полученным конфигом через отдельные проперти. Маппинг на бины почти никто не умеет, а писать несколько десятков адаптеров-конвертеров слишком долго. Самой перспективной показалась lightbend config, с ее симпатичным форматом HOCON и наследованием/переопределением из коробки. И она даже почти смогла заполнить java-бин, но, как оказалось, она не умеет map-ы и очень плохо расширяется. Пока я с ней экспериментировал, на получившиеся конфиги посмотрел коллега и сказал: «Чем-то это напоминает Groovy DSL». Так было принято решение использовать именно его.
Что это такое?
DSL (domain-specific language, предметно-ориентированный язык) — язык, «заточенный» под определенную область применения, в нашем случае — под конфигурацию конкретно нашего приложения. Пример можно посмотреть в спойлере перед катом.
Запускать groovy-скрипты из java-приложения легко. Нужно всего лишь добавить groovy в зависимости, например, Gradle
и использовать GroovyShell
Как это работает?
Вся магия основывается на двух вещах.
Делегирование
Вместо этого мы можем создать скрипт специального типа — DelegatingScript. Его особенность в том, что ему можно передать объект-делегат, и все вызовы методов и работа с полями будут делегироваться ему. В документации по ссылке есть пример использования.
Создадим класс, который будет содержать наш конфиг
@Data — аннотация из библиотеки lombok: добавляет геттеры и сеттеры к полям и реализует toString, equals и hashCode. Благодаря ей POJO превращается в бин.
GroovyObjectSupport — базовый класс для «java-объектов, которые хотят казаться groovy-объектами» (как написано в документации). Позже я покажу, для чего именно он нужен. На данном этапе можно обойтись без него, но пусть будет сразу.
Теперь создадим скрипт, который будет заполнять его поля.
Тут все очевидно. Пока, как вы видите, мы не используем каких-то фич DSL, о них я расскажу позже.
И, наконец, запустим его из джавы
ServerConfig(name=MyTestServer, description=Apache Tomcat) — результат lombok-овской реализации toString().
Как видите, все довольно просто. Конфиг — настоящий исполняемый groovy-код, в нем можно использовать все фичи языка, например, подстановки
вернет нам ServerConfig(name=MyTest server, description=Apache Tomcat server)
И в этом скрипте даже можно ставить брейкпоинты и дебажить!
Вызов методов
Теперь перейдем к собственно DSL. Допустим, мы хотим добавить в наш конфиг настройки коннекторов. Выглядят они примерно так:
Добавим поля для двух коннекторов, http и https, в наш конфиг сервера:
Мы можем задать коннекторы из скрипта с помощью вот такого groovy-кода
ServerConfig(name=MyTest, description=Apache Tomcat, http=Connector(port=80, secure=false), https=null)
Как видите, это сработало, но, конечно же, для конфигурации такой синтаксис совершенно не подходит. Перепишем конфиг так, как хотелось бы, чтобы он выглядел:
И аналогичный — для https. На этот раз все хорошо:
ServerConfig(name=MyTest, description=Apache Tomcat, http=Connector(port=80, secure=false), https=Connector(port=443, secure=true))
означает, что при обращении к полям или методам groovy будет сначала смотреть на делегат, и только потом, если не найдет ничего подходящего — на замыкание. Для скрипта эта стратегия используется по умолчанию, для замыкания ее надо устанавливать вручную.
Библиотека logback, имеющая возможность конфигурации через groovy, использует именно такой подход. Они явным образом реализовали все методы, которые используются в их DSL.
В принципе, у нас уже есть некий DSL, но он далек от идеального. Во-первых, хотелось бы избежать ручного написания кода для установки каждого поля, а во-вторых, хотелось бы избежать дублирования кода для всех классов бинов, которые используются у нас в конфиге. И здесь нам на помощь приходит второй компонент магии groovy DSL.
methodMissing()
Каждый раз, когда groovy встречает вызов метода, отсутствующего у объекта, он пытается вызвать methodMissing(). В качестве параметров туда передается имя метода, который попытались вызвать, и список его аргументов. Уберем из класса ServerConfig методы http и https и объявим вместо них следующее:
То, что нужно! Осталось только развернуть аргументы и в зависимости от типа параметра устанавливать значения полей. В нашем случае туда передается массив из одного элемента класса Closure. Сделаем, например, вот так:
Я опускаю почти все проверки и ловлю исключений, чтобы не захламлять код. В реальном проекте, естественно, прямо так делать нельзя.
Здесь мы видим сразу несколько вызовов, специфичных для groovy-объектов.
Пока что мы добавили methodMissing и все dsl-плюшки только для одного класса, ServerConfig. Мы могли бы реализовать тот же метод для Connection, но зачем дублировать код? Создадим какой-нибудь базовый для всех наших конфиг-бинов класс, скажем, GroovyConfigurable, перенесем methodMissing в него, а ServerConfig и Connector унаследуем.
Это все работает, даже при том, что GroovyConfigurable ничего не знает о полях своих наследников!
Наследование
Следующий шаг — сделать возможность включать в конфиг некий родительский конфиг и переопределять какие-то отдельные поля. Выглядеть это должно примерно так.
Groovy позволяет импортировать классы, но не скрипты. Самый простой способ — реализовать в нашем классе GroovyConfigurable метод include. Добавим туда путь к самому скрипту и пару методов:
Сделаем конфиг parent.groovy, в котором опишем некий базовый конфиг:
В config.groovy оставим только то, что мы хотим переопределить:
ServerConfig(name=MyTest, description=PARENT DESCRIPTION, http=Connector(port=80, secure=false), https=Connector(port=8080, secure=true))
Как видите, name переопределилось, как и поле port в https. Поле secure в нем осталось от родительского конфига.
Можно пойти еще дальше и сделать возможность инклюдить не весь конфиг, а его отдельные части! Для этого в methodMissing надо добавить проверку на то, что устанавливаемое поле тоже GroovyConfigurable и задать ему путь к родительскому скрипту.
Это позволит нам инклюдить не только весь скрипт, но и его части! Например, так
где http.groovy это
Это уже отличный результат, но есть небольшая проблема.
Generics
Скажем, мы хотим добавить в конфиг нашего сервера маппинги и их статус.
Запуск скрипта из Groovy
чтобы моя настройка была немного ближе к «развертыванию одним щелчком мыши», я хотел бы использовать скрипты groovy для запуска/остановки других процессов, управляемых сценариями bat, работающих в разных частях файловой системы и даже на разных машинах.
Как выполнить эти скрипты и как это сделать с их соответствующий рабочий каталог?
однако есть много проблем с этим, и я задавался вопросом, Был ли Groovy что-то вроде стенографии для этого?
4 ответов
Groovy добавил метод execute () в обычную старую строку, поэтому попробуйте следующее:
метод execute () можно использовать для изменения каталогов, если вы префиксируете его командой «cmd /c», а затем используете амперсанд (при условии Windows) для объединения команд.
пример, предполагая, что вы хотите перейти в подкаталог подкаталог и запустите оттуда пару пакетных файлов:
Не уверен, что нет лучшего способа, но это работает.
вы также можете использовать ProcessBuilder, который является удивительно удобным Java-классом, представленным в java 5.
создать новый процесс с:
def proc= «cmd».execute ()
после этого вы можете использовать «consumeProcessOutput()» для управления входом и выходом «proc». Все, что вы отправляете ему, будет действовать так, как если бы вы ввели его в оболочку, и все выходные данные этой оболочки будут доступны для вас.
я завернул все это в закрытие, чтобы вы могли это сделать:
чтобы получить дисплей, который показывает только autoexec.линия летучая мышь. Обратите внимание, что пока вы не вернете true из закрытия, stdin этого процесса доступен, чтобы вы могли отправлять больше строк текста и взаимодействовать с ним бесконечно.
получите меня быстрый список каталогов с легкостью.
Groovy: часть 2 — Hello World и базовый синтаксис
Hello, Groovy
Самый простой способо выполнить «Hello, World» — используя Groovy shell :
И выполнить println(‘Hello World’) :
Либо — в виде скрипта:
Оператор Import
Оператор import используется (внезапно) для импорта библиотек для использования в вашем коде.
В примере ниже — выполняется импорт класса MarkupBuilder :
По умолчанию — Groovy выполняет импорт следющих библиотек:
Токены
Токеном (token) может являться ключевое слово, идентификатор, константа, строковый литерал или просто символ.
Комментарии в Groovy
В Groovy имеются как однострочные так и многострочные комментарии.
Однострочный комментарий начинается с символов // и могжет быть помещен в любом месте кода:
Точка с запятой
В оригинале говорится «[…] it is required to have semicolons […] Groovy» — на самом деле их не обязательно использовать каждый раз.
Может — в более старых версиях Groovy они требовались? http://groovy-lang.org/semantics.html: «In Groovy semicolons at the end of the line can be omitted, if the line contains only a single statement.»
Пример кода с использованием точки с запятой:
Оба примера будут работать одинаково:
Однако при использовании нескольких операторов в одной строке — их необходимо разделять:
Иначе — возникнет ошибка:
Идентификаторы
def тут — ключевое слово, используемое в Groovy для определения идентификатора.
Пример использования идентификатора в нашей Hello, World программе:
Ключевые слова
Ключевые слова — это специальные слова, зарезирвированные в Groovy для самого языка:
as | assert | break | case |
catch | class | const | continue |
def | default | do | else |
enum | extends | false | Finally |
for | goto | if | implements |
import | in | instanceof | interface |
new | pull | package | return |
super | switch | this | throw |
throws | trait | true | try |
while |
Пробелы
Пробелы в Groovy отделяют одну часть выражения от другой, позволяя компилятору различать отдельные элементы выражения.
В следующем примере — переменные отделяют ключевое слово def от переменной x :
Литералы
Литерал — обозначение для представления фиксированного значения в Groovy.
В Groovy имеются целочисленные (integer), символьные и строковые литералы, например:
Groovy как скриптовый язык и DSL для Java
Зачем?
При разработке разного рода ПО, например, задач мониторинга или обслуживания, я сталкиваюсь с необходимостью поддержки в программе выполнения сценариев, описывающих или выполняющих некоторый набор действий. И специфика такова, что добавление или изменение таких сценариев в ПО не должно требовать пересборки или перезапуска ПО.
Наверное, самыми простыми примерами таких сценариев, с которыми все сталкивались в том или ином виде, могут служить обычные пакетные файлы — bat или sh.
В своей практике, я иногда использую XML для описания сценариев. В случае, четко определенного заранее набора действий и их простого плоского набора без ветвлений — XML не плох для использования. Тут и фиксированная структура, позволяющая валидировать файл сценария, и простота самого языка XML, позволяющая с ним работать не только программистам. Однако реализовывать ветвления или условную логику в XML, на мой взгляд, очень накладно и сродни написанию своего мини языка. Расширение набора действий чаще всего возможно только при изменении исходного кода программы, да и изначально реализация поддержки XML сценария трудозатратна.
В общем, в поисках более простого и функционального инструмента, для описания сценариев, взор был обращен к скриптовым языкам. Так как платформа Java, то хотелось иметь возможности интеграции скриптового языка и Java. В итоге, выбор пал на Groovy – динамический язык на базе JVM, легко интегрируемый с Java, простой и выразительный, имеющий много полезных возможностей для нашей задачи.
Скрипты
Я не буду рассказывать основы Groovy, так как материалов в сети даже на русском уже предостаточно. Остановлюсь лишь на некоторых ключевых для нас моментах.
Groovy позволяет нам из Java кода выполнять не скомпилированный исходный Groovy код, что позволяет нам выполнять сценарии добавленные или измененные в runtime.
Рассмотрим пример выполнения Groovy скрипта в Java. Для поддержки Groovy в Вашем Java проекте нужно подключить в зависимости лишь одну библиотеку «groovy» нужной Вам версии.
Напишем следующий Groovy код в файле x:\GroovyScript.groovy:
Код для выполнения данного скрипта в Вашем Java код может быть таким:
В итоге выполнения, в консоль выведется 2 строки, первая из скрипта, вторая — из Java, с результатом выполнения скрипта:
Groovy script
result=16
Пример не несет функциональной нагрузки в скрипте, однако показывает возможности динамической загрузки и выполнения – всего две строчки кода и мы получаем возможность выполнения скриптов.
Немного о Java коде. GroovyShell – это класс предоставляемый Groovy для выполнения groovy скриптов. Существуют и другие способы выполнения groovy скриптов, подробнее смотрите тут
DSL – domain-specific language или предметно ориентированный язык. Язык, позволяющий использовать основные операции предметной области через набор простых и понятных высокоуровневых функций, скрывающий от пользователя их реализацию.
В приведенном выше примере код достаточно простой, однако в реальном сценарии он может быть очень большим и сложным. И работать с такими скриптами смогут только groovy-разработчики, избежать ошибок без тестирования будет сложно. В случае заранее известных операций в сценариях, всю бизнес логику можно вынести в код (java или groovy — неважно), и предоставить возможность использовать ее через набор функций.
Рассмотрим небольшой пример. Требуется написать скрипт, который будет выполнять архиваций, распаковку архивов, удаление, некоторые проверки и уведомления.
Один их кусочков сценария мог бы быть таким – проверить состояние процесса и в случае его завершения заархивировать каталог и послать уведомление:
//import
…
//check state
Process p = getProcess ( … )
int state = p. getCompleteState ( … )
if ( state == 1 ) <
//doSomeLogicForArchive
Zip z = new Zip ( … )
z. makeZip ( … )
> else <
//doAnotherLogic
return
>
//doSomeLogicForSendNotify
Smtp smtp = new Smtp ( … )
Message m = new Message ( … )
smtp. send ( to,m. )
Код получается достаточно большим, и он будет понятен, в основном, лишь программистам. Давайте его упростим и вынесем три указанных действия в класс ArchiveScript со статическими методами. Скрипт после вынесения методов:
Уже лучше? — Лучше, но все еще есть артефакты – импорт и названия класса, которые тоже стоило бы убрать. И в Groovy есть подобная возможность – возможность задания базового класса для скрипта вне самого скрипта. Класс ArchiveScript для этого должен наследоваться от Script и методы могут быть не статическими. Код скрипта при этом еще упрощается – исчезает импорт и префикс класса:
Уже достаточно хорошо. В случае если код внутри блока ветвления условия однострочный, можно отказаться и от фигурных скобок. А в случае Groovy часто и от скобок справа от имени метода. Код выполнения скрипта немного усложняется — нужно создать объект CompilerConfiguration, установить значение scriptBaseClass равное имени созданного нами класса ArchiveScript и передать этот объект в GroovyShell:
CompilerConfiguration conf = new CompilerConfiguration ( ) ;
conf. setScriptBaseClass ( «package.ArchiveScript» ) ;
GroovyShell shell = new GroovyShell ( conf ) ;
Далее, давайте рассмотрим, как задаются параметры методов в скрипте при вызове. В случае определения в классе ArchiveScript метода makeArchive в таком виде:
def makeArchive ( sourcePath, destPath, deleteSource )
В скрипте вызов должен был бы выглядеть так:
И если говорить о наглядности и даже удобстве Groovy позволяет нам сделать передачу параметров через именованные параметры, так:
Однако в этом случае параметры будут передаваться внутри HashMap и, соответственно, получение параметров в makeArchive в классе ArchiveScript должно быть таким:
Если применить преобразование и для других вызовов, то в конечном итоге наш скрипт мог бы выглядеть так:
И это уже не слишком сложный и достаточно читаемый код.
Таким образом, мы получили свой мини-DSL с несколькими предопределенными функциями, специфичными для нашей задачи. А также все еще имеем возможность использовать всю мощь исходного языка.
Замечу, что я рассмотрел лишь малую толику разработки DSL. В Groovy есть более широкая поддержка разработки своего DSL, а так же DSL-поддержка для Eclipse и для IntelliJ Idea
Тестирование
Хотелось бы сказать несколько слов о тестировании сценариев. Каким бы простым не был скрипт, ошибки могут быть и в нем. Даже если Вы пишите сценарии в IDE, полноценной проверки на корректность синтаксиса Вы можете не получить. Это возможно лишь при его выполнении. Так же необходимо проверять поведение скрипта.
Так как нам не хотелось бы выполнять реальные действия при выполнении тестирования сценария, то нужно каким-то образом заменить реальную логику на имитацию. Groovy позволяет нам это сделать многими путями. Покажу несколько из них.
Замена базового скрипта
Создаем новый класс ArchiveSciptMock который имеет интерфейс аналогичный ArchiveScript, и реализующий нужное нам поведение (или ничего не делающий). При создании объекта конфигурации CompilerConfiguration передаем его имя вместо оригинала.
CompilerConfiguration conf = new CompilerConfiguration ( ) ;
conf. setScriptBaseClass ( «package.ArchiveScriptMock» ) ;
Замена методов в базовом классе скрипта
Другим вариантом без создания дополнительного mock класса может быть замена методов на mock в самом ArchiveScript. В groovy это можно сделать, например, таким способом:
Недостатком и первого и второго способа я бы считал необходимость написания дублирующей логики по проверки правильности передаваемых параметров. Так как если в ArchiveScriptMock метод makeArchive будет таким:
def makeArchive ( params ) <
//makeArchiveInternal params.sourcePath, params.destPath, params.deleteSource
>
То мы не проверим все ли параметры были переданы. Нужно будет писать что-то похожее на это:
Я бы предложил сделать небольшой рефакторинг ArchiveScript — сделать ArchiveScript фасадом и всю логику перенести в другой класс. Например, в Java класс Archive.
Рефакторинг — не только для целей тестирования, но и из других соображений, например, для отделения поведения от способа выполнения (нет зависимости от Script). В итоге, после изменения, ArchiveScript примет такой вид:
Теперь, можно тестировать логику и сценарий в отдельности. Заменим Archive на его mock и выполним скрипт:
Естественно поведение Archive можно заменить и с помощью java mock фреймворков, однако, нам пока достаточно и этого.
Я считаю, что получил достаточно гибкий инструмент для написания сценариев, без недостатков, озвученных в начале текста, а также достаточно простотой в использовании. Контроль корректности сценария так же не был потерян — использование mock поведения позволяет нам тестировать их в достаточной мере, перед реальным выполнением.
Проект с исходными кодами проекта — groovydsl. Компилируется Gradle через враппер.
Некоторые идеи взяты из книги Groovy for Domain-Specific Languiages, Fergal Dearle, 2010
Улучшенный sandboxing для Groovy скриптов
От переводчика: При разработке CUBA Platform мы заложили в этот фреймворк возможность исполнения пользовательских скриптов для более гибкой настройки бизнес-логики приложений. О том, хороша или плоха эта возможность (и мы говорим не только о CUBA), ведутся долгие споры, но то, что контроль исполнения пользовательских сценариев необходим — это ни у кого не вызывает вопросов. Одна из полезных возможностей Groovy для управления исполнением пользовательских скриптов представлена в этом переводе статьи Cédric Champeau. Несмотря на то, что он недавно покинул команду разработки Groovy, сообщество программистов, по видимому, еще долгое время будем пользоваться плодами его трудов.
Один из наиболее часто используемых способов использования Groovy — это скриптинг, поскольку Groovy позволяет легко исполнять код динамически, в рантайме. В зависимости от приложения, скрипты могут находиться в различных местах: файловой системе, БД, удаленных сервисах… но самое важное — разработчик приложения, исполняющего скрипты, не обязательно сам их пишет. Более того, скрипты могут работать в ограниченном окружении (ограниченный объем памяти, лимит на количество дескрипторов файлов, время исполнения…), или вы можете захотеть запретить пользователю использовать все возможности языка в скрипте.
Этот пост вам расскажет
Например, представьте себе, что вам нужно сделать так, чтобы пользователь мог вычислять математические выражения. Один из вариантов реализации — встроить внутренний DSL, создать парсер и, наконец, интерпретатор для этих выражений. Для этого, понятное дело, придется поработать, но если вам нужно повысить производительность, например, с помощью генерации байт-кода для выражений вместо их вычисления в интерпретаторе или использовать кэширование классов, генерируемых в рантайме, тогда Groovy — отличный вариант.
Есть множество вариантов, описанных в документации, но самый простой пример — это просто использование класса Eval :
Код 1+1 парсится, компилируется в байт-код, загружается и исполняется Groovy в рантайме. Конечно, в этом образце код очень простой, и вам понадобится добавить параметры, но идея в том, что исполняемый код может быть произвольным. И это, возможно, не совсем то, что вам нужно. В калькуляторе вам нужно разрешать примерно такие выражения:
Именно здесь и начинаются трудности, а также становится понятно, что нам нужно решить несколько задач:
Пример с калькулятором довольно прост, но для более сложных DSL люди могут не заметить, что пишут проблемный код, особенно если DSL настолько прост, что его могут использовать не разработчики.
Несколько лет назад я был в такой ситуации. Я разработал движок, который исполнял “сценарии” Groovy, написанные лингвистами. Одной из проблем, например, было то, что они могли непреднамеренно создать бесконечный цикл. Код исполнялся на сервере, и там появлялся поток, пожирающий 100% CPU, после чего приходилось перезапускать сервер приложений. Пришлось искать способ решить проблему, не затрагивая DSL, инструменты или производительность приложения.
На самом деле, у многих людей схожие потребности. За последние 4 года я разговаривал с множеством людей, у которых был один и тот же вопрос: Как сделать, чтобы пользователи не могли натворить ерунды в скриптах Groovy?
Кастомайзеры компиляции
На тот момент у меня уже было свое решение и я знал, что другие люди тоже разработали что-то похожее. В конце концов, Гийом Лафорж (Guillaume Laforge) предложил мне создать в ядре Groovy механизм, который поможет решить эти проблемы. Он появился в Groovy 1.8.0 в виде кастомайзеров компиляции.
Кастомайзеры компиляции — это набор классов, которые модифицируют процесс компиляции скриптов Groovy. Вы можете написать и свой кастомайзер, но Groovy поставляет:
Мне стоит извиниться за это. Тогда я не смог придумать лучшего названия. Самая важная часть в названии “SecureASTCustomizer” — это AST. Целью создания этого механизма было ограничение доступа к некоторым функциям AST. Слово «secure» в названии вообще лишнее, и я объясню почему. Есть даже пост в блоге всем известного по Jenkins Косукэ Кавагути, под названием “Губительный Groovy SecureASTCustomizer”. И там все очень правильно написано. SecureASTCustomizer создавался без расчета на sandboxing. Он был создан для ограничения языка во время компиляции, но не исполнения. Сейчас я думаю, что лучшим названием было бы GrammarCustomizer. Но, как вам безусловно известно, в информатике есть три трудности: инвалидация кэша, придумывание имен и ошибка на единицу.
А теперь представьте себе, что вы рассматриваете secure AST customizer как средство для обеспечения безопасности вашего скрипта, и ваша задача — не позволить пользователю вызвать System.exit из скрипта. В документации сказано, что вызовы можно запретить в специальных ресиверах, создавая черные или белые списки. Если нужна безопасность, я всегда рекомендую белые списки, строго оговаривающие, что разрешено, но не черные списки, запрещающие что-либо. Потому что хакеры всегда думают о том, чего вы могли не учесть. Приведу пример.
Если вы запустите этот класс, то во время выполнения скрипта вылетит ошибка:
SecureASTCustomizer взломан!
Защита, говорите? А что если я сделаю так:
Вообще есть множество способов обойти различные конфигурации, созданные на secure AST customizer. Вот несколько прикольных:
и их может быть намного больше. Динамическая природа Groovy исключает возможность устранить эти проблемы во время компиляции. Однако решение существует. Один из вариантов — положиться на стандартный диспетчер безопасности JVM. Однако это тяжеловесное и объемное решение сразу для всей системы, и это равносильно стрельбе из пушки по воробьям. К тому же, оно работает не во всех случаях, например, если вы хотите запретить чтение файлов, но не создание…
Это ограничение — для многих из нас скорее огорчение — привело к созданию решения на основе проверок во время исполнения. У этого вида проверок нет таких проблем. Например, потому что вам будет известен фактический тип ресивера сообщения до начала проверки допустимости вызова метода. Особый интерес представляют следующие реализации:
Однако, ни одна из этих реализаций не является абсолютно надежной и безопасной. Например, версия Косукэ основана на хаке внутренней реализации кэширования call site. Проблема в том, что она не совместима с invokedynamic-версией Groovy, и этих внутренних классов не будет в будущих версиях Groovy. Версия Саймона, с другой стороны, основана на AST-трансформациях, но оставляет много возможных дыр.
В итоге, я и мои друзья Коринн Криш, Фабриция Матрат и Себастьян Блан решили создать новый механизм sandboxing’а в рантайме, у которого не будет таких проблем, как у этих проектов. Мы начали реализовывать его на хакатоне в Ницце, а на конференции Greach в прошлом году делали об этом доклад. Этот механизм основан на AST-трансформациях и существенно переписывает код, чтобы производить проверку перед каждым вызовом метода, попыткой доступа к полю класса, приращением переменной, бинарным выражением… Эта реализация все еще не готова, и над ней было проделано не очень много работы, так как я осознал, что проблема с методами и параметрами, вызываемыми через «implicit this», еще не решена, как, например, в билдерах:
На сегодняшний день я все еще не нашел способ решить эту проблему из-за архитектуры протокола мета-объектов в Groovy, которая основана на том, что ресивер выбрасывает исключение, когда не может найти метод, до того, как переключиться на другой ресивер. Вкратце, это значит, что вы не можете узнать тип ресивера до фактического вызова метода. А если вызов прошел, то уже слишком поздно…
А ещё до недавнего времени у меня не было оптимального решения этой проблемы для случая, в котором исполняемый скрипт использует динамические свойства языка. Но теперь настало время объяснить, как можно существенно улучшить ситуацию, если вы готовы немного пожертвовать динамичностью языка.
Проверка типов
Вернемся к основной проблеме с SecureASTCustomizer: он работает с абстрактным синтаксическим деревом и не обладает информацией о конкретных типах и ресиверах сообщений. Но с версии Groovy 2 у Groovy появилась дополнительная компиляция, а в Groovy 2.1 мы добавили расширения для проверки типов.
Расширения для проверки типов — очень мощная штука: они позволяют разработчику Groovy DSL помочь компилятору с выводом типов, а также позволяют генерировать ошибки компиляции в случаях, когда они обычно не возникают. Эти расширения используются внутри Groovy для поддержки статического компилятора, например при реализации типажей (traits) или движка шаблонов разметки.
Что если вместо использования результатов работы парсера мы могли бы полагаться на информацию от механизма проверки типов? Возьмем код, который попытался написать наш хакер:
Если активировать проверки типов, код не скомпилируется:
Итак, этот код больше не компилируется. А что если взять такой код:
Как вы можете убедиться, он проходит проверку типа, будучи обернутым в метод и выполненным с помощью команды groovy :
Простое расширение для проверки типов
Поиск расширения будет происходить в classpath’е в формате исходного кода (можно сделать предкомпилированные расширения для проверки типов, но в этой статье мы их не рассматриваем):
Вот и все, что нужно. Теперь снова запустите код, и вы увидите ошибку компиляции!
Предположим, ваше приложение вычисляет некие показатели для документа и позволяет пользователям их настраивать. В таком случае DSL:
Образец пользовательского скрипта:
Такой DSL легко настроить. Это вариант того, что мы определили выше:
Есть способы кэшировать скрипты вместо того, чтобы парсить и компилировать их каждый раз. За подробностями обратитесь к документации.
Итак, наш скрипт работает, но ничто не мешает хакеру запустить вредоносный код. Так как мы планируем использовать проверку типов, я бы порекомендовал использовать трансформацию @CompileStatic :
Неявно добавить аннотацию @CompileStatic в скрипты довольно просто. Нужно только обновить конфигурацию компилятора:
Теперь если вы снова попытаетесь запустить скрипт, вы увидите ошибку компиляции:
Поэтому можно немного изменить способ, которым добавляется аннотация @CompileStatic :
Первое завершенное «secure» расширение
Не забудьте обновить конфигурацию в вашем Java-классе, чтобы использовать новое расширение для проверки типов:
Запустите код снова — он все еще работает. Теперь попробуйте сделать вот что:
Компиляция скрипта вылетит с ошибкой:
Поздравляем, вы только что написали первое расширение для проверки типов, которое предотвращает запуск вредоносного кода!
Улучшение конфигурации расширения
В первую очередь мы сделаем список переменных настраиваемым. Так будет выглядеть код со стороны Java:
А вот как расширение для проверки типов может это использовать:
Теперь нам нужно найти способ явно объявить список разрешенных вызовов методов. Изобрести подходящий формат для этого было немного сложнее, но вот что мы в итоге придумали.
Конфигурация белого списка методов
Посмотрим, как обновить интеграцию с Java, чтобы встроить эту конфигурацию:
Затем на стороне расширения для проверки типов:
И если вы снова запустите код, получите очень классную ошибку:
Собираем все вместе
Разобраться со свойствами немного сложнее, потому что у type checker’а не средств для «property selection», такого, как для методов. Для этого можно найти обходное решение, и если вам интересно взглянуть на код этого решения, смотрите ниже. Это расширение для проверки типов написано не совсем так, как остальные в этом посте, потому что в этом случае цель — предкомпиляция для улучшения производительности. Но идея та же самая.
Заключение
Этот пост исследует вопрос использования Groovy в качестве платформы для скриптинга на JVM. Он рассказывает о различных механизмах интеграции и показывает, что за это приходится платить безопасностью. Однако, мы обозначили несколько концепций, таких как кастомайзеры компиляции, которые упрощают изоляцию среды выполнения скриптов. Существующие кастомайзеры, доступные в дистрибутиве Groovy, и доступные готовые проекты по sandboxing’у в общем случае не могут гарантировать безопасность запуска скриптов (конечно, это зависит от пользователей и от того, откуда пришел скрипт).
Мы показали, как можно правильно обойти эти ограничения с помощью расширений для проверки типов, если вы готовы заплатить за это некоторыми динамическими свойствами языка. Расширения для проверки типов настолько функциональны, что вы можете создать собственные сообщения об ошибке во время компиляции скриптов. Более того, сделав все это и закэшировав скрипты, вы получите поразительное повышение производительности при исполнении скриптов.
Последнее, но не менее важное: решение, описанное мной здесь, неполно. В ядре Groovy оно не доступно. Поскольку у меня все меньше времени для работы над Groovy, я буду рад, если кто-нибудь улучшит это решение и сделает pull request, чтобы у нас было хоть какое-то полноценное решение!