понедельник, 6 августа 2012 г.

Внедрение HornetQ JMS 2.2.5 сервера и клиента в приложение

В этой статье рассказывается, как внедрить в свое приложение замечательный HornetQ сервер. И это оказалось проще, чем я предполагал вначале.
Мне требовалась асинхронность обработки сообщений, потокобезопастность и возможность мониторинга, и желательно не писать много своего кода. Так что я остановился на HornetQ как уже знакомой мне реализации, которая удовлетворяем моим запросам.
Возьмем версию HornetQ 2.2.5 как последнюю стабильную на данный момент.
Зависимоcти для maven:
<dependency>
   <groupId>org.hornetq</groupId>
   <artifactId>hornetq-core</artifactId>
   <version>2.2.5.Final</version>
   <scope>compile</scope>
</dependency>
   <dependency>
   <groupId>org.hornetq</groupId>
   <artifactId>hornetq-jms</artifactId>
   <version>2.2.5.Final</version>
   <scope>compile</scope>
</dependency>
<dependency>
   <groupId>org.hornetq</groupId>
   <artifactId>hornetq-logging</artifactId>
   <version>2.2.5.Final</version>
   <scope>compile</scope>
</dependency>
<dependency>
   <groupId>org.jboss.netty</groupId>
   <artifactId>netty</artifactId>
   <version>3.2.3.Final</version>
</dependency>
<dependency>
   <groupId>org.jboss.spec.javax.jms</groupId>
   <artifactId>jboss-jms-api_1.1_spec</artifactId>
   <version>1.0.0.Final</version>
   <scope>compile</scope>
</dependency>
Конфигурация сервера и очередей.

В classpath должны находиться файлы hornetq-configuration.xml, содержащий настройки сервера, hornetq-jms.xml с настройками очередей, и hornetq-users.xml, содержащий настройки пользователей.
hornetq-configuration.xml

<configuration xmlns="urn:hornetq"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="urn:hornetq /schema/hornetq-configuration.xsd">

    <persistence-enabled>false</persistence-enabled>
    <!-- Connectors -->

    <connectors>
        <connector name="in-vm">
            <factory-class>org.hornetq.core.remoting.impl.invm.InVMConnectorFactory</factory-class>
        </connector>
    </connectors>

    <acceptors>
        <acceptor name="in-vm">
            <factory-class>org.hornetq.core.remoting.impl.invm.InVMAcceptorFactory</factory-class>
        </acceptor>
    </acceptors>

    <!-- Other config -->

    <security-settings>
        <!--security for example queue-->
        <security-setting match="#">
            <permission type="createDurableQueue" roles="guest"/>
            <permission type="deleteDurableQueue" roles="guest"/>
            <permission type="createNonDurableQueue" roles="guest"/>
            <permission type="deleteNonDurableQueue" roles="guest"/>
            <permission type="consume" roles="guest"/>
            <permission type="send" roles="guest"/>
        </security-setting>
    </security-settings>
</configuration>

hornetq-jms.xml 
<configuration xmlns="urn:hornetq"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="urn:hornetq /schema/hornetq-jms.xsd">

    <connection-factory name="ConnectionFactory">
        <connectors>
            <connector-ref connector-name="in-vm"/>
        </connectors>
        <entries>
            <entry name="ConnectionFactory"/>
        </entries>
        <consumer-window-size>0</consumer-window-size>
        <retry-interval>1000</retry-interval>
        <retry-interval-multiplier>1.5</retry-interval-multiplier>
        <max-retry-interval>60000</max-retry-interval>
        <reconnect-attempts>1000</reconnect-attempts>
    </connection-factory>

    <!--the queue used by the example-->
    <queue name="paymentQueue">
        <entry name="queue/paymentQueue"/>
    </queue>
    <queue name="statusQueue">
        <entry name="queue/statusQueue"/>
    </queue>

</configuration>

hornetq-jms.xml
<configuration xmlns="urn:hornetq" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="urn:hornetq /schema/hornetq-users.xsd">
    <!-- the default user.  this is used where username is null-->
    <defaultuser name="guest" password="guest">
        <role name="guest"/>
    </defaultuser>
</configuration>


Старт встроенного сервера очень прост:
  
import org.hornetq.jms.server.embedded.EmbeddedJMS;

  EmbeddedJMS server = new EmbeddedJMS();
  server.start();
Далее создаем нужное число потребителей сообщений:
 
            QueueConnectionFactory cf = (QueueConnectionFactory) server.lookup("ConnectionFactory");
            Queue queue = (Queue) server.lookup("queue/paymentQueue");
            QueueConnection conn = cf.createQueueConnection("guest", "guest");
            for(int i = 0; i< Config.getConsumerCount(); i++){
                Session consumerSession = conn.createSession(false, Session.CLIENT_ACKNOWLEDGE);
                MessageConsumer consumer = consumerSession.createConsumer(queue);
                consumer.setMessageListener(new PaymentSender());
            }
Класс PaymentSender должен реализовывать интерфейс javax.jms.MessageListener
Запускаем обработчики:
 
conn.start();
Теперь нужно создать класс для отсылки сообщений.
 
    private static QueueConnectionFactory qconFactory = null;
    private static QueueConnection qcon = null;


    public static void init() throws JMSException {
        qconFactory = (QueueConnectionFactory) HornetqListener.server.lookup("ConnectionFactory");
        qcon = qconFactory.createQueueConnection("guest", "guest");

    }

    public static void destroy() throws JMSException {
        qcon.close();
    }

    public static void addPaymentToQueue(Transaction t) throws JMSException {

        QueueSession qsession = qcon.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
        Queue queue = (Queue) HornetqListener.server.lookup("queue/paymentQueue");
        QueueSender qsender = qsession.createSender(queue);
        qcon.start();
        TextMessage msg = qsession.createTextMessage();
        msg.setText(String.valueOf(t.getId()));
        logger.debug("message {} putted in queue 'queue/paymentQueue'", msg.getText());
        qsender.send(msg);
        qsession.close();
    }
HornetqListener.server - это статическая переменная, в которую мы сохранили созданный экземпляр объекта EmbeddedJMS
Мониторинг очередей реализуем через JMX
private static int max_queue = 200;

QueueView paymentqueue = jmxQuery("paymentQueue");
if(paymentqueue.getMessagesInQueue() > max_queue)
  max_queue = (int) paymentqueue.getMessagesInQueue();
String percentPayment = (new Double(paymentqueue.getMessagesInQueue()))/max_queue * 100d + "%";

private QueueView jmxQuery(String queueName) throws Exception {
        MBeanServer mbeanServer = java.lang.management.ManagementFactory.getPlatformMBeanServer();
        QueueView queueView = new QueueView();

        ObjectName name = new ObjectName("org.hornetq:module=JMS,type=Queue,name=\""+ queueName +"\"");

        queueView.setConsumersCount((Integer) mbeanServer.getAttribute(name, "ConsumerCount"));
        queueView.setMessagesDelivering((Integer) mbeanServer.getAttribute(name, "DeliveringCount"));
        queueView.setMessagesInQueue((Long) mbeanServer.getAttribute(name, "MessageCount"));
        queueView.setMessagesAdded((Long) mbeanServer.getAttribute(name, "MessagesAdded"));
        queueView.setName((String) mbeanServer.getAttribute(name, "Name"));

        name = new ObjectName("org.hornetq:module=Core,type=Server");

        queueView.setConnectionCount((Integer) mbeanServer.getAttribute(name, "ConnectionCount"));
        queueView.setThreadPoolMaxSize((Integer) mbeanServer.getAttribute(name, "ThreadPoolMaxSize"));
        queueView.setServerVersion((String) mbeanServer.getAttribute(name, "Version"));
        return queueView;
    }
Конфигурационные файлы взяты отсюда

четверг, 2 августа 2012 г.

Загрузка диапазонов номеров мобильных операторов с сайта Россвязи

На сайте Россвязи есть замечательный, периодически обновляющийся список, содержащий номерные ёмкости и телефонные компании, к которым эти диапазоны относятся. В данной статье описывается процедура получения и разбора этого файла с помощью апачевского HttpClient и обычного SAX парсера.

  • Скачиваем html со списком кодов

Для этого вполне подойдет org.apache.http.impl.client.DefaultHttpClient

DefaultHttpClient client = new DefaultHttpClient();
HttpGet httpget = new HttpGet("http://rossvyaz.ru/docs/articles/DEF-9x.html");

HttpResponse resp = client.execute(httpget);
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
    InputStream is = resp.getEntity().getContent();
    List registry = new ArrayList();
    parseHTML(is, registry);
}

Но вот незадача, контент сжат gzip'ом. В принципе это даже неплохо, уменьшается время загрузки и сетевой трафик, так что просто добавим поддержку gzip в HttpClient. Делается это путем добавления перехватчиков на запрос и ответ:

private DefaultHttpClient prepareHttpClient() {
        DefaultHttpClient client = new DefaultHttpClient();
        client.addRequestInterceptor(new HttpRequestInterceptor() {

            public void process(
                    final HttpRequest request,
                    final HttpContext context) throws HttpException, IOException {
                if (!request.containsHeader("Accept-Encoding")) {
                    request.addHeader("Accept-Encoding", "gzip");
                }
            }

        });

        client.addResponseInterceptor(new HttpResponseInterceptor() {
            public void process(
                    final HttpResponse response,
                    final HttpContext context) throws HttpException, IOException {
                HttpEntity entity = response.getEntity();
                if (entity != null) {
                    Header ceheader = entity.getContentEncoding();
                    if (ceheader != null) {
                        HeaderElement[] codecs = ceheader.getElements();
                        for (int i = 0; i < codecs.length; i++) {
                            if (codecs[i].getName().equalsIgnoreCase("gzip")) {
                                response.setEntity(
                                        new GzipDecompressingEntity(response.getEntity()));
                                return;
                            }
                        }
                    }
                }
            }

        });
        return client;
    }

Теперь осталось разобрать полученный html и вытащить из него DEF коды.

  • Парсим полученный html

Воспользуется стандартным потоковым java SAX парсером (javax.xml.stream.XMLEventReader), поскольку полученный файл большой, и к тому же является невалидным XML файлом, так что придется произвести небольшие ухищрения, чтобы его разобрать. Разбор производится "на лету", то есть начинается уже в процессе скачивания html, за счет чего достигается приличная скорость при небольшом потреблении памяти. Весь процесс занимает порядка трех секунд.


static final String TR = "tr";
static final String TD = "td";

...

private void parseHTML(InputStream is, List<DEFCode> result) {
        try {
            XMLInputFactory inputFactory = XMLInputFactory.newInstance();
            XMLEventReader eventReader = inputFactory.createXMLEventReader(is, "windows-1251");
            String[] buff = new String[6];
            int count = 0;
            int ind = 0;
            while (eventReader.hasNext()) {
                try {
                    XMLEvent event = eventReader.nextEvent();

                    if (event.isStartElement()) {
                        StartElement startElement = event.asStartElement();
                        // start new row
                        if (TR.equals(startElement.getName().getLocalPart())) {
                            buff = new String[6];
                            count++;
                            ind = 0;
                        }

                        if (TD.equals(event.asStartElement().getName().getLocalPart())) {
                            event = eventReader.nextEvent();
                            buff[ind++] = event.asCharacters().getData();
                            continue;
                        }
                    }

                    if (event.isEndElement()) {
                        EndElement endElement = event.asEndElement();
                        if (TR.equals(endElement.getName().getLocalPart())) {
                            if(count != 1){ // пропускаем первую строку с заголовком
                                result.add(validateRow(count, buff));
                            }
                        }
                    }
                } catch (XMLStreamException e) { // вероятнее всего это незакрытый тег, игнорируем ошибку
                    logger.error("skip error");
                } catch(Exception ex){ // вероятнее всего файл просто закончился, завершаем обработку
                    logger.error("skip error, break");
                    break;
                }
            }
            eventReader.close();

        } catch (XMLStreamException e) { // что-то не так с кодировкой или структурой файла
            e.printStackTrace();
        }
    }

private DEFCode validateRow(int counter, String[] nextLine) throws ValidationException {
        // здесь проводится проверка строки на валидность и если 
        // строка не валидна, выбрасывается эксепшн с номером строки 
    }

вторник, 24 июля 2012 г.

Настройка очередей в JBoss 7

В этой статье описывается настройка очередей в JBoss AS 7.1.1

Последний раз мне приходилось работать с JBoss 5 и с тех пор есть определенный прогресс. Сервер запускается и останавливается быстро, появились средства конфигурации и мониторинга. В 7й ветке есть тенденция отказа от отдельных xml файлов для настроек очередей и источников данных, и рекомендованный способ - добавлять их непосредственно в конфигурационный файл сервера. Хотя старый способ все еще работает, добавленные таким образом очереди не будут видны в админской консоли как очереди и по ним нельзя будет посмотреть статистику.

Итак, для начала нам нужно просто подключить поддержку очередей. По умолчанию в standalone.xml она не подключена. Для включения поддержки копируем из standalone-full.xml все, связанное с messaging, а именно:

...

<extension module="org.jboss.as.messaging"/>

...

<subsystem xmlns="urn:jboss:domain:messaging:1.1">
            <hornetq-server>
                <persistence-enabled>true</persistence-enabled>
                <journal-file-size>102400</journal-file-size>
                <journal-min-files>2</journal-min-files>

                <connectors>
                    <netty-connector name="netty" socket-binding="messaging"/>
                    <netty-connector name="netty-throughput" socket-binding="messaging-throughput">
                        <param key="batch-delay" value="50"/>
                    </netty-connector>
                    <in-vm-connector name="in-vm" server-id="0"/>
                </connectors>

                <acceptors>
                    <netty-acceptor name="netty" socket-binding="messaging"/>
                    <netty-acceptor name="netty-throughput" socket-binding="messaging-throughput">
                        <param key="batch-delay" value="50"/>
                        <param key="direct-deliver" value="false"/>
                    </netty-acceptor>
                    <in-vm-acceptor name="in-vm" server-id="0"/>
                </acceptors>
                <security-settings>
                    <security-setting match="#">
                        <permission type="send" roles="guest"/>
                        <permission type="consume" roles="guest"/>
                        <permission type="createNonDurableQueue" roles="guest"/>
                        <permission type="deleteNonDurableQueue" roles="guest"/>
                    </security-setting>
                </security-settings>

                <address-settings>
                    <address-setting match="#">
                        <dead-letter-address>jms.queue.DLQ</dead-letter-address>
                        <expiry-address>jms.queue.ExpiryQueue</expiry-address>
                        <redelivery-delay>1000</redelivery-delay>
                        <max-size-bytes>10485760</max-size-bytes>
                        <address-full-policy>BLOCK</address-full-policy>
                        <message-counter-history-day-limit>10</message-counter-history-day-limit>
                    </address-setting>
                </address-settings>
               <jms-connection-factories>
                    <connection-factory name="InVmConnectionFactory">
                        <connectors>
                            <connector-ref connector-name="in-vm"/>
                        </connectors>
                        <entries>
                            <entry name="java:/ConnectionFactory"/>
                        </entries>
                    </connection-factory>
                    <connection-factory name="RemoteConnectionFactory">
                        <connectors>
                            <connector-ref connector-name="netty"/>
                        </connectors>
                        <entries>
                            <entry name="RemoteConnectionFactory"/>
                            <entry name="java:jboss/exported/jms/RemoteConnectionFactory"/>
                        </entries>
                    </connection-factory>
                    <pooled-connection-factory name="hornetq-ra">
                        <transaction mode="xa"/>
                        <connectors>
                            <connector-ref connector-name="in-vm"/>
                        </connectors>
                        <entries>
                            <entry name="java:/JmsXA"/>
                        </entries>
                    </pooled-connection-factory>
                </jms-connection-factories>
          <jms-destinations>
                    <jms-queue name="testQueue">
                        <entry name="queue/test"/>
                        <entry name="java:jboss/exported/jms/queue/test"/>
                    </jms-queue>
                    <jms-topic name="testTopic">
                        <entry name="topic/test"/>
                        <entry name="java:jboss/exported/jms/topic/test"/>
                    </jms-topic>
          </jms-destinations>
</hornetq-server>
...
        <socket-binding name="messaging" port="5445"/>
        <socket-binding name="messaging-throughput" port="5455"/>

После этого можно добавить непосредственно сами очереди:

<jms-destinations>
                    <jms-queue name="defaultQueue">
                        <entry name="queue/default">
                        <entry name="java:jboss/defaultQueue">
                    </entry></entry></jms-queue>
                    <jms-queue name="beelineQueue">
                        <entry name="queue/beeline">
                        <entry name="java:jboss/beelineQueue">
                    </entry></entry></jms-queue>
                    <jms-queue name="mtsQueue">
                        <entry name="queue/mts">
                        <entry name="java:jboss/mtsQueue">
                    </entry></entry></jms-queue>
                    <jms-queue name="mfQueue">
                        <entry name="queue/mf">
                        <entry name="java:jboss/mfQueue">
                    </entry></entry></jms-queue>
                    <jms-queue name="ExpiryQueue">
                        <entry name="queue/expiry">
                        <entry name="java:jboss/ExpiryQueue">
                    </entry></entry></jms-queue>
                    <jms-queue name="DLQ">
                        <entry name="queue/dead">
                        <entry name="java:jboss/deadQueue">
                    </entry></entry></jms-queue>
                </jms-destinations>

Как можно заметить, в конфигурации есть отдельные очереди для "мертвых" и просроченных сообщений. Сообщение попадает в "мертвую" очередь после того, как его доставка окончилась неудачей определенное количество раз. Этот параметр можно настроить, как и интервал между повторными попытками отправки сообщения.

Добавляем в конфигурацию описание этих очередей. Тут есть магия с префиксами jms.queue. где-то их нужно указывать, где-то нет.

После этого можно приступать к настройке обработчиков сообщений. Это обычные Message Driving Bean (MDB). Можно описать их с помощью аннотаций:

@MessageDriven(activationConfig = {
        @ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Queue"),
        @ActivationConfigProperty(propertyName="destination", propertyValue="java:jboss/defaultQueue"),
        @ActivationConfigProperty(propertyName = "maxSession", propertyValue = "10")
        })
public class DefaultQueueHandler implements MessageListener {

При деплое они подцепятся автоматически. В принципе это уже рабочая конфигурация, но если используется spring или hibernate может потребоваться создание контекста, еще до того, как очереди начнут обрабатывать сообщения. В этом случае можно поступить следующим образом: создать EJB, ответственный за инициализацию контекста, перед инициализацией остановить обработку сообщений во всех очередях, а после инициализации возобновить ее. Это позволить избежать включения отдельного war модуля только для того, чтобы поднять контекст.

@Startup
@Singleton(name = "InitContextEJB")
public class InitContextEJB{

    private String jconsolePrefix = "jboss.as:subsystem=messaging,hornetq-server=default,jms-queue=";

    @PostConstruct
    public void atStartup() {
        stopQueues();
        System.out.println("+++++++++++++++++++ CONTEXT +++++++++++++++++++");
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
                "applicationContext.xml",
                "data-source.xml"
                );
        startQueues();
    }

    @PreDestroy
    public void atDestroy(){
        stopQueues();
    }

    private void startQueues() {
        try {
            setPaused(new ObjectName(jconsolePrefix + "defaultQueue"), false);
            setPaused(new ObjectName(jconsolePrefix + "beelineQueue"), false);
            setPaused(new ObjectName(jconsolePrefix + "mtsQueue"), false);
            setPaused(new ObjectName(jconsolePrefix + "mfQueue"), false);
        } catch (MalformedObjectNameException e) {
            e.printStackTrace();
        }

    }

    private void stopQueues() {
        try {
            setPaused(new ObjectName(jconsolePrefix + "defaultQueue"), true);
            setPaused(new ObjectName(jconsolePrefix + "beelineQueue"), true);
            setPaused(new ObjectName(jconsolePrefix + "mtsQueue"), true);
            setPaused(new ObjectName(jconsolePrefix + "mfQueue"), true);
        } catch (MalformedObjectNameException e) {
            e.printStackTrace();
        }

    }

Для остановки и запуска очередей можно вспомнить, что они по-сути MDB, а значит доступны через JMX консоль и javax.management.MBeanServer. Приведу вариант с MBeanServer, как более независимый от реализации и стандартный.

protected void setPaused(final ObjectName objectName, final Boolean method) {
        try {
            MBeanServer mbeanServer = java.lang.management.ManagementFactory.getPlatformMBeanServer();
            System.out.println(objectName + " paused before= " + mbeanServer.getAttribute(objectName, "paused"));

            mbeanServer.invoke(objectName, method ? "pause" : "resume",new Object[]{}, new String[]{});

            System.out.println(objectName + " paused after= " + mbeanServer.getAttribute(objectName, "paused"));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Перехват и глотание исключений при остановке желательно, поскольку сам HornetQ сервер может остановиться раньше (например при остановке JBoss)

Полезные ссылки:

четверг, 21 июня 2012 г.

Генерация pdf из java

Появилась недавно задача - добавить в pdf шаблон несколько строк, причем pdf был довольно сложный по составу. Немного поискав варианты решения, понял, есть два пути - использовать генератор с шаблонами, либо загружать готовый pdf документ и непосредственно выводить в него текст. Поняв, что первый вариант меня не устраивает по той причине, что шаблон уже в pdf формате, и рисовать его заново мне не хочется, я остановился на варианте с непосредственным выводом текста в pdf.

Первой мне попалась библиотека pdfbox. К сожалению, не удалось заставить ее корректно отображать русские буквы. Возможно, мне просто не повезло со шрифтом, который я пытался использовать, или не понравилась кодировка, не знаю. Возможно когда-нибудь я предоставлю ей второй шанс.

Следующая на очереди была iText. С помощью неё удалось решить поставленную задачу. Замеченные особенности:

  • Практически все тулзы для генерации pdf файлов внедряют в итоговый файл только subset'ы используемых шрифтов. Вывод - не нужно использовать внедренные в pdf шрифты, если не хотите получить слова с отсутствующими буквами.
  • Стандартные шрифты, например BaseFont.HELVETICA, содержат в себе только латинские символы и не могут быть использованы для вывода текста в кириллице.
  • Используйте Unicode шрифты, загружайте их из файла и внедряйте в итоговый pdf документ. Сделать это просто, вызвав BaseFont.createFont("Liberation-Sans.ttf", BaseFont.IDENTITY_H, true)
И пример использования:

    File tmp = File.createTempFile("pdf", null);
    FileOutputStream outStream = new FileOutputStream(tmp);
    Document pdDoc = new Document(PageSize.A4);
    PdfWriter writer = PdfWriter.getInstance(pdDoc, outStream);
    pdDoc.open();
    try{
        PdfContentByte cb = writer.getDirectContent();

        InputStream is = 
                    getClass().getClassLoader().getResourceAsStream("/templates/template.pdf");
        if (is == null) {
            throw new DocumentException("cannot load template");
        }

        String fontPath = "/templates/" + 
                          Config.getInstance().getString("templates.font");
        URL u = getClass().getClassLoader().getResource(fontPath);
        if(u == null){
            throw new  DocumentException("cannot load font");
        }
        // load font from resource
        BaseFont bf = BaseFont.createFont(u.getFile(), BaseFont.IDENTITY_H, true);
        PdfReader reader = new PdfReader(is);
        PdfImportedPage page = writer.getImportedPage(reader, 1);

        // Copy first page of existing PDF into output PDF
        pdDoc.newPage();
        cb.addTemplate(page, 0, 0);

        cb.saveState();

        // write  text
        cb.beginText();
        cb.setFontAndSize(bf, 10);
        cb.setTextMatrix(197, 676);
        cb.showText("some text");

        cb.setFontAndSize(bf, 7);

        cb.setTextMatrix(70, 645);
        cb.showText("smaller text");

        cb.endText();
        cb.restoreState();


        page = writer.getImportedPage(reader, 2);
        pdDoc.newPage();
        //add second page as is
        cb.addTemplate(page, 0, 0);


        page = writer.getImportedPage(reader, 3);
        pdDoc.newPage();
        //add third page as is
        cb.addTemplate(page, 0, 0);
    }finally{
        pdDoc.close();
    }

среда, 20 июня 2012 г.

Внедрение Groovy для вычисления простых выражений

Небольшой пример внедрения groovy для вычисления простых выражений. Не требует создания сложных тяжеловесных конструкций с Binding и создания отдельных файлов скриптов.

import groovy.util.Eval;

HashMap params = new HashMap();
params.put("field1", "10");
params.put("field2", "11");

Boolean res = (Boolean) Eval.me("param", params, "param.field1 == param.field2");
Документация на класс Eval

пятница, 15 июня 2012 г.

Использование OpenSource крипто библиотек для работы с ГОСТовыми сертификатами CryptoPro

В данной статье делается попытка ответить на вопрос: возможно ли использовать OpenSource библиотеки для работы с ГОСТовыми сертификатами?
Если коротко, то да.
Для проверки использовалась последняя на данный момент версия OpenSSL (1.0.1с), собранная из исходников с поддержкой gost engine
Для начала нужно установить КриптоПро CSP 3.6 R3, поскольку в нем реализована поддержка экспорта сертификатов в формате pfx. Эту часть работ нужно делать в операционной системе Windows. Не буду описывать подробно этот процесс, скажу лишь, что нужно сохранить приватный ключ в формате CryptoPro на флэшку и импортировать его в хранилище "Реестр". Выгрузив сертификат в файл, например store.pfx (включая приватный ключ), можно вывести приватный ключ в stdout командой
/usr/local/ssl/bin/openssl pkcs12 -info -engine gost -nodes -in ~/store.pfx 

  • -info вывести информацию о содержимом файла в формате pkcs12 (.pfx)
  • -engine использовать указанный криптографический модуль
  • -nodes не нужно шифровать приватный ключ
  • -in входной файл
Если вы получаете ошибку вида "unknown PBE algorithm"
Enter Import Password:
MAC Iteration 2000
MAC verified OK
PKCS7 Data
Shrouded Keybag: undefined, Iteration 2000
Bag Attributes
    Microsoft Local Key set: <No Values>
    localKeyID: 01 00 00 00
    friendlyName: REGISTRY\\1ba6dcf8-b953-4774-8a9e-de98de071f24
    Microsoft CSP Name: Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider
Error outputting keys and certificates
140669637789344:error:06074079:digital envelope routines:EVP_PBE_CipherInit:unknown pbe algorithm:evp_pbe.c:167:TYPE=1.2.840.113549.1.12.1.80
140669637789344:error:23077073:PKCS12 routines:PKCS12_pbe_crypt:pkcs12 algor cipherinit error:p12_decr.c:83:
140669637789344:error:2306A075:PKCS12 routines:PKCS12_item_decrypt_d2i:pkcs12 pbe crypt error:p12_decr.c:130:
То это означает, что вам потребуется другое средство для получения pfx файла, утилитка P12FromGostCSP.exe
Если вы получаете ошибку вида "unknown digest algorithm"

Enter Import Password:
MAC Iteration 2048
Mac verify error: invalid password?
140572666472096:error:2306B076:PKCS12 routines:PKCS12_gen_mac:unknown digest algorithm:p12_mutl.c:88:
140572666472096:error:2307E06D:PKCS12 routines:PKCS12_verify_mac:mac generation error:p12_mutl.c:122:

то скорее всего в вашем openssl нет поддержки ГОСТовых алгоритмов либо вы используете не ту копию openssl.

Если все прошло без ошибок, то в консоли вы получите приватный ключ и сертификат. Скопировав выведенный в консоль приватный ключ (вместе со словами -----BEGIN PRIVATE KEY----- и -----END PRIVATE KEY-----) в отдельный файл private.key мы сможем с его помощью создавать ЭЦП.

Команда для генерации примерно следующая:

/usr/local/ssl/bin/openssl cms -sign -inkey ~/private.key -in ~/file.txt -CAfile ~/CA.cer 
-signer ~/client.cer -engine gost -out ~/test.sign -outform DER -noattr -binary

  • -sign генерация подписи
  • -inkey путь к приватному ключу
  • -in подписываемый файл
  • -CAfile файл УЦ
  • -signer файл с сертификатом, которым осуществляется подпись
  • -engine использовать указанный криптографический модуль
  • -out файл, в который будет записана ЭЦП
  • -outform формат подписи
  • -noattr не добавлять дополнительных аттрибутов в подпись
  • -binary считать подписываемый файл массивом байт, а не текстом

К сожалению, Bouncy Castle (версии 1.46), хоть в нем и декларируется поддержка ГОСТовых алгоритмов, не может разобрать формат приватного ключа, так что использовать его для генерации ЭЦП на данный момент не представляется возможным.

UPD:
Bouncy Castle 1.55 уже умеет работать с такими приватными ключами

суббота, 9 июня 2012 г.

Создание ЭЦП с помощью CryptoPro

В данной статье описывается процесс создания ЭЦП c помощью библиотеки CryptoPro.
Для создания подписи необходим приватный ключ и сертификат. Рассмотрим гипотетическую ситуацию, когда приватный ключ прислан по почте, а не сгенерирован самостоятельно - порочная, но широко распространенная практика.
Предположим, что файл клиентского сертификата называется client.cer, сертификата УЦ - CA.cer, а приватный ключ находится в директории 999996.000

1. Установка CryptoPro CSP и CryptoPro JCP

Добываем каким-либо образом дистрибутивы, самый простой способ - скачать их с сайта CryptoPro, предварительно там зарегистрировавшись. Триальная версия полнофункциональна и работает 30 дней.
При установке CSP пакет cprocsp-rdr-gui скорее всего не установится, поскольку использует древние версии Motif, но он не нужен для работы, так что установка данного пакета не является обязательной.
Также нужно убедиться, что в системе есть libcurl.so (эта библиотека используется для получения CRL)
После установки необходимо проверить, что в файле /etc/opt/cprocsp/config.ini прописан правильный путь к libcurl.so, по умолчанию он ведет на /usr/local/lib/64/libcurl.so что является ошибкой.
После этого устанавливаем CryptoPro JCP (jcp_plus_jtls_1.0.53.jar)
После успешной установки нужно запустить ControlPane.sh из-под рута и поменять путь к хранилищу ключей на /var/opt/cprocsp/keys/{$user.name}

2. Установка приватного ключа и сертификатов

CryptoPro имеет свой собственный формат приватного ключа и свои собственные контейнеры для хранения ключей и сертификатов. Чтобы установить ключ и сертификаты в контейнеры, нужно проделать следующие действия:

  • Скопировать в корень дискеты или флэшки сертификат и приватный ключ. Под приватным ключом понимается директория 999996.000 и ее содержимое, файлы header.key, masks2.key, masks.key, name.key, primary2.key, primary.key
    $ cp -R /path/to/key/999996.000 /media/flashdrive/
    $ cp /path/to/cert/client.cer /media/flashdrive/
    
  • Выполнить команду по копированию ключа с флэшки на диск. Ключ попадет в пользовательское хранилище 'My'. Выполнять команду нужно под пользователем, который будет использовать данный контейнер для подписи. 999996 - название (alias) контейнера. gate@example.com - то, что прописано в поле E сертификата ( можно посмотреть командой keytool --printcert -file /path/to/cert/client.cer )
    $ csptest -keycopy -src '\\.\FLASH\gate@example.com' -dest '\\.\HDIMAGE\999996'
    Проверить, что все скопировалось, можно командой
    $ ls -al /var/opt/cprocsp/keys/<username>
  • Альтернативный путь, если нет дискеты или csptest выдает ошибку
    Error number 0x8009000f (2148073487). Object already exists.
    Руками скопировать приватный ключ в хранилище командой
    $ cp -R /path/to/key/999996.000 /var/opt/cprocsp/keys/<username>/
  • Ассоциировать сертификат с контейнером. Сертификат попадет в пользовательское хранилище 'My'
    $ certmgr -inst -file /path/to/file/client.cer -cont '\\.\HDIMAGE\999996'
  • Установить сертификат УЦ из-под пользователя root командой
    # certmgr -inst -store root -file /path/to/file/CA.cer
Контейнер должен быть готов к использованию, проверить это можно командами:
$ certmgr --list
Certmgr 0.9 prerelease (c) "CryptoPro",  2007-2010.
program for managing certificate(CRL) and stores

=============================================================================
1-------
Issuer         : DC=ru, DC=issuer, CN=EXAMPLE
Subject        : C=RU, S=RUSSIA, L=MOSCOW, O=ORGANIZATION, OU=IT, CN=GATE_DEMO, E=gate@example.com
Serial         : 0x2225000000007DF78065
PrivateKey Link: Yes. Container: HDIMAGE\\999996.000\D7BB
=============================================================================

[ErrorCode: 0x00000000]
Обратите внимание на строку "PrivateKey Link: Yes. Container: HDIMAGE\\999996.000\D7BB". Она показывает наличие связи сертификата и приватного ключа, если выводится "PrivateKey Link: No" это означает, что связь не установлена и использовать такой контейнер для подписи не удастся.
$ certmgr --list -store root
Certmgr 0.9 prerelease (c) "CryptoPro",  2007-2010.
program for managing certificate(CRL) and stores

=============================================================================
1-------
Issuer         : DC=ru, DC=issuer, CN=EXAMPLE
Subject        : DC=ru, DC=issuer, CN=EXAMPLE
Serial         : 0xE44263EF7B42044F9E20FFF14C6F1327
PrivateKey Link: No
=============================================================================

[ErrorCode: 0x00000000]

3. Генерация ЭЦП

Под ЭЦП обычно понимается отсоединенная (detached) подпись в формате pkcs#7. Т.е помимо самой подписи, в сообщение внедряется вся цепочка сертификатов. CryptoPro не предоставляет отдельного пакета CMS для легкой генерации криптографических сообщений, но в принципе в пакете JCP есть все необходимое, чтобы сформировать корректное сообщение самостоятельно. Код по большей части взят из примеров, которые идут в JCP.
    private static byte[] signWithCryptoProJcp(byte[] data) throws Exception{

        String alias = "999996";
        String caFile = "/path/to/CA.cer";
        String certFile = "/path/to/client.cer";

        //load keys for sign
        final PrivateKey[] keys = new PrivateKey[1];
        keys[0] = CMStools.loadKey(alias, null);

        //load certificates chain
        final Certificate[] certs = new Certificate[2];
// функция CMStools.loadCertificate() почему-то не работает, хотя сертификат есть в хранилище
// пришлось читать сертификат из файла
        certs[0] = CMStools.readCertificate(certFile);
        certs[1] = CMStools.readCertificate(caFile);
        return createCMS(data, keys, certs, true);
    }


    private static byte[] createCMS(byte[] data, PrivateKey[] keys,
                                   Certificate[] certs,
                                   boolean detached)
            throws Exception {
        //create CMS
//        Array.writeFile("/home/grigory/test.msg", data);
        final ContentInfo all = new ContentInfo();
        all.contentType = new Asn1ObjectIdentifier(new OID(CMStools.STR_CMS_OID_SIGNED).value);
        final SignedData cms = new SignedData();
        all.content = cms;
        cms.version = new CMSVersion(1);
        // digest
        cms.digestAlgorithms = new DigestAlgorithmIdentifiers(1);
        final DigestAlgorithmIdentifier a = new DigestAlgorithmIdentifier(
                new OID(CMStools.DIGEST_OID).value);
        a.parameters = new Asn1Null();
        cms.digestAlgorithms.elements[0] = a;
        if (detached)
            cms.encapContentInfo = new EncapsulatedContentInfo(
                    new Asn1ObjectIdentifier(
                            new OID(CMStools.STR_CMS_OID_DATA).value),
                    null);
        else
            cms.encapContentInfo =
                    new EncapsulatedContentInfo(new Asn1ObjectIdentifier(
                            new OID(CMStools.STR_CMS_OID_DATA).value),
                            new Asn1OctetString(data));
        // certificates
        final int ncerts = certs.length;
        cms.certificates = new CertificateSet(ncerts);
        cms.certificates.elements = new CertificateChoices[ncerts];
        for (int i = 0; i < cms.certificates.elements.length; i++) {
            final ru.CryptoPro.JCP.ASN.PKIX1Explicit88.Certificate certificate =
                    new ru.CryptoPro.JCP.ASN.PKIX1Explicit88.Certificate();
            final Asn1BerDecodeBuffer decodeBuffer =
                    new Asn1BerDecodeBuffer(certs[i].getEncoded());
            certificate.decode(decodeBuffer);
            cms.certificates.elements[i] = new CertificateChoices();
            cms.certificates.elements[i].set_certificate(certificate);
        }
        // Signature.getInstance
        final Signature signature =
                Signature.getInstance(JCP.GOST_EL_SIGN_NAME);
        byte[] sign;
        // signer infos
        final int nsign = keys.length;
        cms.signerInfos = new SignerInfos(nsign);
        for (int i = 0; i < cms.signerInfos.elements.length; i++) {
            signature.initSign(keys[i]);
            signature.update(data);
            sign = signature.sign();
            cms.signerInfos.elements[i] = 
                     new ru.CryptoPro.JCP.ASN.CryptographicMessageSyntax.SignerInfo();
            cms.signerInfos.elements[i].version = new CMSVersion(1);
            cms.signerInfos.elements[i].sid = new SignerIdentifier();

            final byte[] encodedName =
                    ((X509Certificate) certs[i]).getIssuerX500Principal().getEncoded();
            final Asn1BerDecodeBuffer nameBuf = new Asn1BerDecodeBuffer(encodedName);
            final Name name = new Name();
            name.decode(nameBuf);

            final CertificateSerialNumber num = new CertificateSerialNumber(
                    ((X509Certificate) certs[i]).getSerialNumber());
            cms.signerInfos.elements[i].sid.set_issuerAndSerialNumber(
                    new IssuerAndSerialNumber(name, num));
            cms.signerInfos.elements[i].digestAlgorithm =
                    new DigestAlgorithmIdentifier(new OID(CMStools.DIGEST_OID).value);
            cms.signerInfos.elements[i].digestAlgorithm.parameters = new Asn1Null();
            cms.signerInfos.elements[i].signatureAlgorithm =
                    new SignatureAlgorithmIdentifier(new OID(CMStools.SIGN_OID).value);
            cms.signerInfos.elements[i].signatureAlgorithm.parameters = new Asn1Null();
            cms.signerInfos.elements[i].signature = new SignatureValue(sign);
        }
        // encode
        final Asn1BerEncodeBuffer asnBuf = new Asn1BerEncodeBuffer();
        all.encode(asnBuf, true);
//        Array.writeFile("/home/grigory/test.signature", asnBuf.getMsgCopy());
        return asnBuf.getMsgCopy();
    }
 
У CryptoPro есть замечательный сервис проверки ЭЦП, где можно проверить, что сгенерированная подпись верна.