Дневник Шестеро Михаила

Oct. 9th, 2014

03:30 am - Преобразование HTML в XHTML на C и C++

Задача преобразования HTML-форматированного текста в XHTML появилась в связи с тем, что нужно было вставлять в XML блоки форматированного текста. (Про чтение таких блоков при SAX-разборе я написал статью: http://lj.rossia.org/users/shestero/141287.html ). Эти вставки писались вручную и часто содержали ошибки форматирования, невидные при просмотре в браузерах, но неприемлимые для строгих парсеров XML. Кроме того в исходных текстах часто были непарные теги, вроде <BR>, которые каждый раз приходилось отыскивать и исправлять вручную.

Для выполнения этой задачи на C++ я успешно приминил бесплатную библиотку Tidy. К сожалению разработка её остановилась в начале 2009, но для моей задачи она сгодилась.

Я использовал MinGW, компилировал из коммандной строки-консоли Qt 4.8.3 под Windows 7.
Так как исходники уже довольно древние, при компиляции возникают небольшие загвоздки. Вот пошаговая инструкция, как я делал:
1. Использовал последний tarbar-архив из CSV (март 2009).
2. После распаковки надо создать вручную директории obj (где Makefile) и bin c lib (в директории tidy) (сами они не создаются)
3. Я использовал Makefile для gmake, но его пришлось изрядно поправить. Также для компиляции в MinGW пришлось поправить директиву выбора блока в файле src/mappedio.c. Исправленные файлы я выложил в архив tidy-changes.7z [здесь].
4. Для сборки библиотеки запустите make из директории build\gmake.

Вот код на C++, обеспечивающий конвертацию:

// Convert HTML to XHTML and clean up using libTidy
#include <tidy.h>
#include <buffio.h>
string CleanHTML(const char *html)
{
    // Initialize a Tidy document
    TidyDoc tidyDoc = tidyCreate();
    TidyBuffer tidyOutputBuffer = {0};

    // Configure Tidy
    // The flags tell Tidy to output XML and disable showing warnings
    bool configSuccess = tidyOptSetBool(tidyDoc, TidyXmlOut, yes)
        && tidyOptSetBool(tidyDoc, TidyQuiet, yes)
        && tidyOptSetBool(tidyDoc, TidyNumEntities, yes)
        && tidyOptSetBool(tidyDoc, TidyShowWarnings, yes) // no
        ;//&& tidyOptSetValue(tidyDoc,TidyCharEncoding, "utf8");

    int tidyResponseCode = -1;

    // Parse input
    if (configSuccess)
        tidyResponseCode = tidyParseString(tidyDoc, html);

    // Process HTML
    if (tidyResponseCode >= 0)
        tidyResponseCode = tidyCleanAndRepair(tidyDoc);

    // Output the HTML to our buffer
    if (tidyResponseCode >= 0)
        tidyResponseCode = tidySaveBuffer(tidyDoc, &tidyOutputBuffer);

    // Any errors from Tidy?
    if (tidyResponseCode < 0) // Tidy encountered an error while parsing an HTML
        throw tidyResponseCode;

    // Grab the result from the buffer and then free Tidy's memory
    std::string tidyResult = (char*)tidyOutputBuffer.bp;
    tidyBufFree(&tidyOutputBuffer);
    tidyRelease(tidyDoc);

    return tidyResult;
}
// ................

        // Convert HTML to XHTML and clean it
        // see also: https://bugs.webkit.org/show_bug.cgi?id=44876
        string xhtml;
        try
        {
            xhtml = CleanHTML( html );
        }
        catch (int e)
        {
            cerr << "Clean HTML error (from libTidy): " << e << endl;
        }
Добавлю, что в моём случае обработка UTF-8 символов (кирилицы) в приложении, запущенном под Windows 7 x64 происходила не правильно (они конвертировались в кодовые &-представления побайтно). Так как мне это не было нужно в тот момент, я не стал разбираться в чём там дело, возможно просто в системных настройках локалей или в каких-то параметрах запуска библиотечных функций или компиляции. Вроде бы Tidy поддерживает UTF-8. Для обработки кириличных HTML-текстов также можно перевести их в однобайтовую кодировку, например в CP-1251.

Для C++ также существует специальная обёртка Tidy-библиотеки TidyPP: http://code.google.com/p/tidypp

Tags: , , ,
Current Mood: busy

Oct. 7th, 2014

04:29 pm - Чтение простого XML с помощью RapidXML

Я люблю хранить технологическую конфигурацию моих программ и утилит в XML-файле.
Пока я делал приложения с использованием Qt использовал небольшой класс, который читал эту конфигурацию и сохранял её в QStringMap.

Однако возникла потребность сделать простую утилиту на C++ но без Qt, XML-парсером которой я пользовался.
Типичный формат моего XML таков:

<?xml version="1.0" encoding="utf-8"?>
<registry>
    <version>1</version>
    <section id="common" >
        <parameter id="locale" >ru_RU.utf8</parameter>

        <parameter id="parameter1" >value1</parameter>
        <parameter id="parameter2" >value2</parameter>
    </section>
</registry>
Для чтения я без проблем воспользовался бесплатной библиотекой RapidXML 1.13. Сейчас она входит в Boost, но я попробовал также использовать её и отдельно (при сборке с помощью GNU C++/MinGW под Windows). Собственно библиотека эта состоит из трёх шабонных HPP-файлов, которые не требуют компиляции.

Публикую код, который читает XML и сохраняет конфигурацию в STL-коллекцию:
//#include <rapidxml.hpp>
//using namespace rapidxml;
#include <boost/property_tree/detail/rapidxml.hpp>
using namespace boost::property_tree::detail::rapidxml;

// .................

    // ---
    xml_document<char> doc;

    //cout << "Parse..." << endl;

    try {
        ifstream file( fname_xml.c_str() );
        vector<char> buffer((istreambuf_iterator<char>(file)), istreambuf_iterator<char>());
        buffer.push_back('\0');

        doc.parse<0>( &buffer[0] );
    }
    catch (parse_error e) {
        cerr << "Fail. Parse error " << e.what() << endl;
        return 0;
    }

    xml_node<> *root_node = doc.first_node();
    //cout << "Root found?" << endl;

    if (root_node!=NULL)
    {
        //cout << "Root found!" << root_node->name() << ":" << root_node->value() << endl;
        xml_node<> *node = root_node->first_node("section");
        //if (node!=NULL) cout << "section found" << endl;
        if (node) node = node->first_node("parameter");
        //if (node!=NULL) cout << "first parameter found" << endl;
        while (node!=NULL)
        {
            xml_attribute<>* a = node->first_attribute("id");
            if (a!=NULL)
            {
                config[ a->value() ] = node->value();
            }

            node = node->next_sibling();
        }
    }
    cout << "========" << endl << "Check loaded configuration:" << endl;

    typedef map<string,string>::const_iterator it;
    for (it i=config.begin(); i!=config.end(); i++)
    {
        cout << i->first << "\t= " << i->second << endl;
    }
    cout << "Configuration loaded" << endl;
    // ---
    

Стоит добавить, что когда я подставлял в doc.parse<0>( ... ) для проверки забитую в коде текстовую константу, это приводило к падению программы во время работы парсера (run-time exception). Так получалось потому, что RapidXML для скорости применяет "разрушающий" разбор - точнее в процессе работы он заменяет символы после концов символьных сущонстей XML нулевыми байтами. Таким образом они превращаются в "отдельные" строки с точки зрения C/C++ без копирования их содержимого.

Tags: , , , , ,
Current Mood: busy

Oct. 6th, 2014

03:42 pm - Сжатие потока информации PHP-HTTP-Java

Это логическое продолжение моих статей «Сжатие потока информации PHP-HTTP-Qt» и «Шифровка потока информации PHP-HTTP-Java».

Перед вами код на Java, способный конвеерно принимать запакованный (как в первой упомянутой статье) и возможно зашифрованный (как во второй) XML:


import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
// ................


public static void main(String[] args
{
  System.out.println("DecryptorTest HELLO");
  
  //Security.addProvider( new IAIK() ); // using IAIK Security Provider
  java.security.Security.addProvider(new gnu.crypto.jce.GnuCrypto());
  
  // Using Hex in Apache Commons:
  // byte[] bytes = Hex.decodeHex(key0.toCharArray());
  StringBuilder keyc = new StringBuilder();
    for (int i = 0; i < key0.length(); i+=2) {
        String str = key0.substring(i, i+2);
        keyc.append((char)Byte.parseByte(str, 16));
    }
  System.out.println("DecryptorTest: Common secret key (plain) =" + keyc);

  MD5 md5 = new MD5();
  //IMessageDigest md5 = HashFactory.getInstance("MD5");
  byte[] iv;
  try {
    iv = iv0.getBytes"UTF-8" );
  catch (UnsupportedEncodingException e1) {
    System.out.println("DecryptorTest: Error: UnsupportedEncodingException (UTF-8)");
    return;
  }
  System.out.println("DecryptorTest: Common initialization vector length (bytes) =" + iv.length);
  md5.update(iv, 0, iv.length);
  String ivh = "";
  iv = md5.digest();
  for (int i = 0; i < iv.length; i++) {
    ivh += String.format("%02x", iv[i] );
  }
  System.out.println("DecryptorTest: Common initialization vector (MD5) =" + ivh);

  // encrypt the user password and convert it to Hex
  String passwordh;
  try {
    passwordh = encryptpassword0, keyc.toString(), iv );
  catch InvalidKeyException 
      | UnsupportedEncodingException 
      | IllegalBlockSizeException 
      | BadPaddingException 
      | NoSuchAlgorithmException 
      | NoSuchProviderException 
      | NoSuchPaddingException 
      | InvalidAlgorithmParameterException e
  {
    System.out.println("DecryptorTest: Error: cannot encrypt user password! Exception="+e.toString());
    return;
  }
  System.out.println("DecryptorTest: Personal password (encripted, hex) =" + passwordh);
  
  md5.reset();
  try {
    md5.update(password0.getBytes("UTF-8")0, password0.getBytes("UTF-8").length);
  catch (UnsupportedEncodingException e) {
    System.out.println("DecryptorTest: Error: cannot prepare password (no UTF-8 encoding)");
    return;
  }
  byte[] pwd5 = md5.digest();
  //System.out.println("DecryptorTest: Personal password (MD5, hex) =" + password5);
  
  int keysize;
  keysize = 32// Cipher.getMaxAllowedKeyLength("Twofish/CBC/NoPadding");
  System.out.println("DecryptorTest: maximum key size ="+keysize );
  
  URL url;
  try {
    url = new URL(surl);
  catch (MalformedURLException e) {
    System.out.println("DecryptorTest: Error: Malformed URL");
    return;
  }
    HttpURLConnection conn;
  try {
    conn = (HttpURLConnectionurl.openConnection();
    String post;
    
    post = "user=shestero";
    post+= "&password="+passwordh;
    
    conn.setDoOutput(true)// мы будем писать POST данные
    conn.setDoInput(true);

    OutputStreamWriter out =
        new OutputStreamWriterconn.getOutputStream()"UTF-8" );
    out.write(post);
    // out.write("\r\n"); // перевод строки попадает в значения, передаваемые POST-ом
    out.close();
    
    PushbackInputStream stream = new PushbackInputStreamconn.getInputStream());
    int i0 = stream.read();
    if (i0<0)
    {
      System.out.println("DecryptorTest: Warning: empty reply!");
    }
    else
    {
      stream.unread(i0);
      
      InflaterInputStream inf = null;
      if (i0==0)
      {
        // encrypted
        stream.skip(32)// skip header
        
        System.out.println("DecryptorTest: Note: data comes encrypted!");
        
        byte[] key2 = new byte[32];
        Arrays.fill(key2,(byte)0);
        int j=0;
        for (int i=0; i<keyc.length() || i<pwd5.length; i++)
        {
          if (i<keyc.length()) key2[j++]=keyc.toString().substring(i,i+1).getBytes()[0];
          if (i<pwd5.lengthkey2[j++]=pwd5[i];
          if (j>=keysize)
            break;
        }
        System.out.println("DecryptorTest: Personal key to decript reply =["+key2+"]");

        // TODO: Fix key size
        if (j<=16j=16else if (j<=24j=24else if (j<32j=32;
        System.out.println("DecryptorTest: key2.length="+j);
        Cipher cipher = createCipherCipher.DECRYPT_MODE, key2, iv );
        
        inf = new InflaterInputStream
            new CipherInputStreamstream, cipher )
            new Inflater(true// set 'nowrap' parameter to 'true' here 
            );
        // r = new BufferedReader( new InputStreamReader( new CipherInputStream( stream, cipher ) ) );
      }
      else
      {
        // plain
        System.out.println("DecryptorTest: Warning: data comes unencrypted!");

        inf = new InflaterInputStreamstream, new Inflater(true) );
        //r = new BufferedReader( new InputStreamReader( stream ) );
      }
      BufferedReader r = new BufferedReadernew InputStreamReaderinf ) );    
      
      // Чтение строка за строкой для проверки
      System.out.println("====[REPLY FROM SERVER:]===========================");
          String inputLine;
          while ((inputLine = r.readLine()) != null
          {
              System.out.println(inputLine);
          }
          r.close();
          System.out.println("====[SUCESS]=======================================");
    }          
    catch (IOException e) {
    System.out.println("DecryptorTest: Error: IOException; URL="+surl);
  catch InvalidKeyException
      |  NoSuchAlgorithmException
      |  NoSuchProviderException
      |  NoSuchPaddingException
      |  InvalidAlgorithmParameterException e
  {
    System.out.println("DecryptorTest: Error: cannot decode: Exception="+e.toString());
  
  
  System.out.println("DecryptorTest BYE")
}
Исходник обработан: Java2html

Tags: , , ,
Current Mood: busy

Jun. 9th, 2014

03:53 pm - C++/Qt: Получение полного содержимого XML-тега (со вложенными) при SAX-разборе

Задача: при SAX-разборе перехватить полное содержимое определённых тегов в виде исходного текста, как оно есть, со всеми вложенными подтегами.
Хотя задача эта, по-моему, достаточно простая, банальная и затребованная, но по какой-то не понятной причине не нашёл избытка описания готовых её решений.
Зачем удобно перехватывать полный «сырой» текст XML? С ходу столкнулся с двумя возможными причинами:
1. Как известно и у SAX и у DOM разбора есть свои преимущества и недостатки. Возможен большой XML-документ, который по крайней мере удобно разбирать SAX-парсером, но в нём попадаются отсносительно небольшие участки, которые как раз было бы удобно и естественно с помощью DOM.
2. В определённных XML-тегах может находится XHTML-содержимое, разбирать которое не нужно вообще, его надо просто целиком сохранить, например, отправить на визуализацию в GUI-контрол.

Вероятно хватает и других задач, при которых удобно временно перевести SAX-разбор в режим «перехвата», при попадании на определённый тег.

Итак, как оказалось в Qt это сделать очень просто: достаточно перекрыть в классе QXmlInputSource публичный виртуальный метод next, который «кормит» прасер анализируемым XML посимвольно.

Вариант №1:
Обработчик содержимого (наследник QXmlDefaultHandler):
(В случае если при SAX-разборе попался один из интересующих нас «особых» тегов, мы переходим в режим «перехвата», а при закрытии тега выходим из этого режима, обрабатываем результат).

bool Report::startElement(const QString &namespaceURI, const QString &localName,
                          const QString &name, const QXmlAttributes &attrs)
{
    if (name=="information") // "information" is a special tag name
    {
        m_xmlsource->BeginIntercept();
    }

    return RemoteTable::startElement(namespaceURI, localName, name, attrs);
}
// RemoteTable is a parent class that handle XML content

bool Report::endElement(const QString &namespaceURI, const QString &localName,
                        const QString &name)
{
    if (name=="information") 
    {
        QString info = m_xmlsource->EndIntercept();

        if (m_pHeader!=NULL)
        {
            // send XHTML content into QLabel
            const QString was = m_pHeader->text();
            m_pHeader->setText( was+info );
        }
    }

    return RemoteTable::endElement(namespaceURI, localName, name);
}

Класс ExtXmlInputSource:
#ifndef EXTXMLINPUTSOURCE_H
#define EXTXMLINPUTSOURCE_H

#include <QXmlInputSource>

#include <QIODevice>


class ExtXmlInputSource : public QXmlInputSource
{
public:
    ExtXmlInputSource(QIODevice* dev);

    virtual void    BeginIntercept();
    virtual QString EndIntercept();
    virtual QChar   next();

protected:
    bool            m_interception;
    QString         m_content;
};

#endif // EXTXMLINPUTSOURCE_H

// cpp:

ExtXmlInputSource::ExtXmlInputSource(QIODevice* dev)
    : QXmlInputSource(dev)
    , m_interception(false)
{
}

void ExtXmlInputSource::BeginIntercept()
{
    m_interception = true;
}

QChar ExtXmlInputSource::next()
{
    QChar ret = QXmlInputSource::next();

    if (m_interception)
        m_content+=ret;

    return ret;
}
Таким образом в накопителе ExtXmlInputSource::m_content получится нужный блок XML-текста плюс закрывающийся тег (если открывающий тег не был вообще «самозакрытым»). Тег для симментрии убираем (если результат направляется в DOM, можно наоборот в начале приписать открывающийся тег):
QString ExtXmlInputSource::EndIntercept()
{
    m_interception = false;

    const QString res = m_content;
    m_content.clear();

    // if needed: remove the last (closing) tag
    if (!res.isEmpty()) // non-empty tag?
    {
        int pos = res.lastIndexOf("</");
        if (pos>=0)
            return res.left(pos);
    }

    return res;
}

У этого метода есть недостаток — в режиме «перехвата» парсер всё равно вызывает виртуальные методы обработчика содержимого (в данном случае класса Report, наследника QXmlDefaultHandler), что во-первых не нужно и грузит машину бесполезной работой, во-вторых вносит путаницу и является потенциальной причиной ошибок.

У меня есть несколько идей, как обойти это. Например, смелая идея (не знаю, на сколько это реализуемо) — запустить DOM-обработчик, на том же QIODevice-источнике информации.
Ниже привожу наверное самое простое решение — временную замену обработчика содержимого.

Вариант №2 (улучшенный):
Класс ExtXmlInputSource2:
#ifndef EXTXMLINPUTSOURCE2_H
#define EXTXMLINPUTSOURCE2_H

#include "extxmlinputsource.h"

#include <QXmlDefaultHandler>
#include <QXmlSimpleReader>

class ExtXmlInputSource2 : public ExtXmlInputSource, public QXmlDefaultHandler
{
public:
    ExtXmlInputSource2(QIODevice* dev);

    void Intercept(const QString& spectag, QXmlSimpleReader* reader, QXmlDefaultHandler* old);

    virtual bool endElement(const QString& , const QString& , const QString &name);

protected:
    QString m_spectag;
    QXmlSimpleReader* m_reader;
    QXmlDefaultHandler* m_old;
};

#endif // EXTXMLINPUTSOURCE2_H

// cpp:

ExtXmlInputSource2::ExtXmlInputSource2(QIODevice* dev)
    : ExtXmlInputSource(dev)
{
}

void ExtXmlInputSource2::Intercept(const QString& spectag, QXmlSimpleReader* reader, QXmlDefaultHandler* old)
{
    m_spectag = spectag;
    m_reader  = reader;
    m_old     = old;

    if (m_reader!=NULL)
        m_reader->setContentHandler(this);

    BeginIntercept();
}

bool ExtXmlInputSource2::endElement(const QString& , const QString& , const QString &name)
{
    if (name==m_spectag) // warning: no nested tags 'spectag' are expected!
    {
        if (m_old!=NULL)
            m_old->characters( EndIntercept() );

        if (m_reader!=NULL)
            m_reader->setContentHandler(m_old);
    }

    return true; // QXmlDefaultHandler::endElement(namespaceURI, localName, name); == always true
}

Вызов из основного класса-обработчика содержимого происходит с помощью метода Intercept, а приём перехваченного XML происходит как-бы обычным вызовом виртуального метода characters в рабочем обработчике.
В метод Intercept кроме названия тега передаётся указатель на класс SAX-парскра и указатель на рабочий (основной) обработчик содержимого для автоматического возврата в основной режим, после закрывающегося «особого» тега:
bool Report::startElement(const QString &namespaceURI, const QString &localName,
                          const QString &name, const QXmlAttributes &attrs)
{
    if (name=="information")
    {
        m_xmlsource->Intercept(name, m_reader, this);
    }

    return RemoteTable::startElement(namespaceURI, localName, name, attrs);
}
// RemoteTable is a parent class that handle XML content

bool Report::characters(const QString &str)
{
    if (m_name=="information") // m_name set in RemoteTable::startElement
    {
        if (m_pHeader!=NULL)
        {
            // send XHTML content into QLabel
            const QString was = m_pHeader->text();
            m_pHeader->setText( was+str );
        }
    }

    return RemoteTable::characters(str);
}

Оба эти варианты работают с предположением, что «специальные» теги, которые надо перехватывать не будут вложенными.

Приложение.
На форуме RSDN человек по имени Константин дал мне ценные указания, как это сделать технологиями Microsoft:
«MSXML позволяет это делать минимальными усилиями: класс MXXMLWriter умеет из SAX-событий делать строку с текстом, или писать XML документ в любой IStream, или, как для твоей задачи, делать DOM из SAX.
Когда в своей реализации ISAXContentHandler встретился определённый тег, создавай DOM document, создавай экземпляр COM-класса MXXMLWriter назначив ему output новый DOM document, потом нужно аккуратно форвардить события ISAXContentHandler в этот MXXMLWriter, пока определённый тег не закроется. Когда закроется, получиццо DOM-документ с содержимым тега
».

Там же мне дали ссылку по этому вопросу на Java:
http://stackoverflow.com/questions/7998733/loading-local-chunks-in-dom-while-parsing-a-large-xml-file-in-sax-java

Tags: , , , ,
Current Mood: [mood icon] satisfied

Apr. 24th, 2014

06:21 am - Сжатие потока информации PHP-HTTP-Qt

Это второе дополнение к моей предыдущей статье «Шифровка потока информации PHP-HTTP-Qt». Для того что бы разобраться с тем что здесь написано, следует сначала ознакомиться с ней.

Вопрос о возможности их сжатия потока естественно возникает в связи с постоянной передачей больших объёмов разряженных данных (в формате XML) множеству клиентов. Тем более что у конечных пользователей программ-клиантов может быть и не быстрый доступ в Интернет, и сжатие в этом случае может дать существенный выйгрыш в скорости работы.
Я делаю сжатие/разжатие сразу в паре с шифровкой/дешифровкой и так же в связи с шифровкой о нём сейчас рассказываю, тем более что это делается и на стороне сервера и на стороне клиента подобным образом. Но вы, естественно, можете попробовать сделать сжатие/разжатие и без шифровки.
Для сдатия в PHP я использовал всё поточный фильтр zlib.deflate. На стороне клиента я применил класс QtIOCompressor из подзаброшенной ныне коллекции-библиотеки Qt Solutions.
Что бы XML лучше ужался, конечно, надо сжать его до шифровки, а не после.
В моём коде сжатие происходит опционально, в зависимости от значения переменной $compress в PHP и .. в коде клиента C++.

Итак, установка сжимающего фильтра на PHP (естественно, соответствующий фильтр должен быть доступен; проверка: phpinfo() ) выглядит так:
if ($compress=="RawZip")
{
  
$compress_alg "zlib.deflate"
}
else
{
  
$compress_alg false
}
if (
$compress_alg)
{
  
stream_filter_append($f$compress_algSTREAM_FILTER_WRITE);
}

В исходнике в статье про шифрование место, где фильт вставляется у меня помечено комментарием: ...[*]....

В программе клиенте поток-декомпрессор вставляется так:

    if (use_compression()) // if use decompressor
    {   // the input stream is compressed
        m_decompressor = new QtIOCompressor(m_decrypter); // QtIOCompressor *m_decompressor;
        m_decompressor->setStreamFormat(QtIOCompressor::RawZipFormat);
        m_decompressor->open(QIODevice::ReadOnly);

        m_xmlsource = new QXmlInputSource(m_decompressor);
    }
    else
    {   // the input stream isn't compressed
        m_xmlsource = new QXmlInputSource(m_decrypter);
    }
(Соответствующее место в исходнике я помечетил комменарием // [**]).

Таким образом информация проходит всю цепочку из таблицы MySQL через MySQLi — PHP — сжатие — шифровку — сеть (HTTP) — расшифровку — распаковку — SAX-парсер до GUI-виджета таблицы конвеерно!
Слоты для «массажа» района дешифратора и распаковщика в Qt этой цепочки таковы:

void WorkplaceTab::onReadyRead()
{
    if ( m_reader==NULL || m_xmlsource==NULL )
    {
        qDebug() << "WorkplaceTab::onReadyRead() - warning";
        return;
    }

    if (m_first)
    {
        // выполняется один раз - при приёме превой порции данных
        qDebug() << "m_reader->parse(m_xmlsource, true);";
        if (!m_reader->parse(m_xmlsource, true))
            to_idle_state(); // прекращение режима приёма
        m_first = false;
    }

    long watchdog = 100;
    while ( m_decrypter!=NULL &&
           (m_decompressor!=NULL?
                m_decompressor->bytesAvailable():
                m_decrypter->bytesAvailable())>0
         )
    {
        m_xmlsource->fetchData();
        if (!m_reader->parseContinue())
            to_idle_state();    // прекращение режима приёма

        if (watchdog--<0)
            break;
    }
}

void WorkplaceTab::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
    logmessage( tr("%1 bytes and %2 rows loaded").arg(bytesReceived).arg(m_row) );

    if (bytesReceived==bytesTotal)
    {
        // выполняется один раз - после приёма последней порции данных
        qDebug() << "WorkplaceTab::onDownloadProgress LAST TIME";
        if (m_reader==NULL || m_xmlsource==NULL || m_reply==NULL)
        {
            qDebug() << "Warning: m_reader==NULL or m_xmlsource==NULL or m_reply==NULL!";
            return;
        }

        long watchdog=1000000;
        bool b=true;
        while ( b && m_xmlsource->data().length()>0 && --watchdog )
        {
            b = m_reader->parseContinue();
        }

        if (b)
        {
            m_xmlsource->next();        // и ещё раз - вхолостую -
            m_reader->parseContinue();  // что бы вызвалось QXmlInputSource::EndOfDocument
        }
    }
}

Согласитесь, это круто. Но, увы,
ложка дёгтя: если с шифрованием-расшифровкой всё идеально (уже прошло многомесячную интенсивную проверку обкатку), то с копрессия-декомпрессия очень редко, но, увы, всё же даёт сбой — в XML-е образуется мусорный участочек, парсер сигнализирует об ошибке... Происходит, правда, это редко, под нагрузкой и никогда — в начале (на маленьких данных не проявляется, обычно где то с 2000 строки), но всё же происходит... Была замечена ситуация, когда сбой был при работе клиента Windows, но не было при работе в Linux, откомпилированного из того-же исходника (правда библиотеки ZLib там были из разных исходников). В то же время сбой имеет тенденцию проявляться в одном и том же месте одного и того же длинного XML.
Так что я эту функцию сжатия в рабочих дистрибутивах сейчас выключаю, но в ряде простых задач с небольшими блоками данных вполне может прокатить. Или можно например в случае ошибок делать перезапрос с отключённым сжатием.
В чём причина не знаю, виноват скорее всего QtIOCompressor — точнее его не совершенство. Например он в принципе не умеет верно оценивать количество байт, которые он готов отдать в текущий момент.
Есть гипотеза, что ошибку можно обойти, например, усовершенствовав «массаж» или вставив между QtIOCompressor и QDecrypter-ом промежуточный буффер, скажем на 1 Kb.

Есть также открытые вопросы: какой стандартной утилитой можно распаковать данные, cгенерированные фильтром zlib.deflate в PHP?
Как этот поток распаковать на Java? (пока не дошёл до этого)?

В общем ещё есть тут над чем поразбираться!

Tags: , , , , , ,
Current Mood: [mood icon] satisfied

Apr. 22nd, 2014

08:06 pm - Шифровка потока информации PHP-HTTP-Java (GNU-JCE, IAIK-JCE)

Это дополнение к моей предыдущей статье «Шифровка потока информации PHP-HTTP-Qt». Для того что бы понять о чём тут речь, прочитайте её сначала. Здесь приводится простенькая демонстрационная программа-клиент на Java, которая делает то же самое, что я сделал там на C++/Qt, то есть обеспечивает приём и расшифровку данных по тому же протоколу. Она работает с тем же самым серверным скриптом на PHP. Принятые расшифрованные данные (в моём случае XML) выводятся в стандартный поток вывода. Вот исходный код (URL-скрипта и пароли я естесвенно убрал):

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Array;

import java.io.*;
import java.net.*;
import java.security.*;

import javax.crypto.*;
import javax.crypto.spec.*;

import gnu.crypto.Registry;

import gnu.crypto.hash.MD5;

import gnu.crypto.jce.GnuCrypto;          // GNU-JCE  провайдер алгоритма TwoFish
// import iaik.security.provider.IAIK;    // IAIK-JCE провайдер алгоритма TwoFish


public class MainClass {

  final static String surl = "http://......./xml.php"// "baseurl()+"/xml.php";
  final static String iv0  = "i.vector";
  final static String key0 = ".............."// Hex encoded
  final static String password0 = "........."// personal password in plain text
  
  
  
  // prepare cipher
  public static Cipher createCipher(int mode, byte[] key, byte[] ivBytesthrows 
      UnsupportedEncodingException, 
      NoSuchAlgorithmException, 
      NoSuchProviderException, 
      NoSuchPaddingException, 
      InvalidKeyException, 
      InvalidAlgorithmParameterException
  {
     IvParameterSpec iv = new IvParameterSpec(ivBytes);
     SecretKey secretKey = new SecretKeySpeckey, "Twofish" )
     
     // get Cipher and init it for encryption
     
     //Cipher cipher = Cipher.getInstance("Twofish/CBC/NoPadding", "IAIK");       // IAIK
     Cipher cipher = Cipher.getInstance("Twofish/CBC/NoPadding", Registry.GNU_CRYPTO);     // GNU
     
     cipher.initmode, secretKey, iv );

     return cipher;
  }

  // encrypt string
  public static String encrypt(String source, String key, byte[] iv
      throws 
      UnsupportedEncodingException, 
      InvalidKeyException, 
      IllegalBlockSizeException, 
      BadPaddingException, 
      NoSuchAlgorithmException, 
      NoSuchProviderException, 
      NoSuchPaddingException, 
      InvalidAlgorithmParameterException 
  {
     Cipher cipher = createCipherCipher.ENCRYPT_MODE, key.getBytes("UTF-8"), iv )
     
     // encrypt data
     while source.length() % cipher.getBlockSize() !=source+="\0"// padding
     byte[] cipherText = cipher.doFinalsource.getBytes("UTF-8") );

     // перевод cipherText в HEX-представление
     String encryptedString = "";
     for (int i = 0; i < cipherText.length; i++) {
      encryptedString += String.format("%02x", cipherText[i]);
     // encryptedText = Base64Coder.encodeLines(encryptedText);
     
     return encryptedString;
  }
  
  public static void main(String[] args) {
    
    System.out.println("DecryptorTest HELLO");
    
    // Security.addProvider( new IAIK() ); // using IAIK Security Provider
    java.security.Security.addProvider(new GnuCrypto());
    
    // Using Hex in Apache Commons:
    // byte[] bytes = Hex.decodeHex(key0.toCharArray());
    StringBuilder keyc = new StringBuilder();
    for (int i = 0; i < key0.length(); i+=2) {
      String str = key0.substring(i, i+2);
      keyc.append((char)Byte.parseByte(str, 16));
    }
    System.out.println("DecryptorTest: Common secret key (plain) =" + keyc);

    MD5 md5 = new MD5();
    //IMessageDigest md5 = HashFactory.getInstance("MD5");
    byte[] iv;
    try {
      iv = iv0.getBytes"UTF-8" );
    catch (UnsupportedEncodingException e1) {
      System.out.println("DecryptorTest: Error: UnsupportedEncodingException (UTF-8)");
      return;
    }
    System.out.println("DecryptorTest: Common initialization vector length (bytes) =" + iv.length);
    md5.update(iv, 0, iv.length);
    String ivh = "";
    iv = md5.digest();
    for (int i = 0; i < iv.length; i++) {
      ivh += String.format("%02x", iv[i] );
    }
    System.out.println("DecryptorTest: Common initialization vector (MD5) =" + ivh);

    // encrypt the user password and convert it to Hex
    String passwordh;
    try {
      passwordh = encryptpassword0, keyc.toString(), iv );
    catch InvalidKeyException 
        | UnsupportedEncodingException 
        | IllegalBlockSizeException 
        | BadPaddingException 
        | NoSuchAlgorithmException 
        | NoSuchProviderException 
        | NoSuchPaddingException 
        | InvalidAlgorithmParameterException e
    {
      System.out.println("DecryptorTest: Error: cannot encrypt user password! Exception="+e.toString());
      return;
    }
    System.out.println("DecryptorTest: Personal password (encripted, hex) =" + passwordh);
    
    md5.reset();
    try {
      md5.update(password0.getBytes("UTF-8")0, password0.getBytes("UTF-8").length);
    catch (UnsupportedEncodingException e) {
      System.out.println("DecryptorTest: Error: cannot prepare password (no UTF-8 encoding)");
      return;
    }
    byte[] pwd5 = md5.digest();
    //System.out.println("DecryptorTest: Personal password (MD5, hex) =" + password5);
    
    int keysize;
    keysize = 32// Cipher.getMaxAllowedKeyLength("Twofish/CBC/NoPadding");
    System.out.println("DecryptorTest: maximum key size ="+keysize );
    
    URL url;
    try {
      url = new URL(surl);
    catch (MalformedURLException e) {
      System.out.println("DecryptorTest: Error: Malformed URL");
      return;
    }
    
    HttpURLConnection conn;
    InputStream stream;
    try {
      conn = (HttpURLConnectionurl.openConnection();
      String post;
      
      post = "user=shestero";
      post+= "&password="+passwordh;
      
      conn.setDoOutput(true)// мы будем писать POST данные
      conn.setDoInput(true);

      OutputStreamWriter out =
          new OutputStreamWriterconn.getOutputStream()"UTF-8" );
      out.write(post);
      // out.write("\r\n"); // перевод строки попадает в значения, передаваемые POST-ом
      out.close();
      
      stream = conn.getInputStream();
      stream.mark(1);
      int i0 = 0;//stream.read();
      BufferedReader r;      
      if (i0==0)
      {
        stream.skip(32)// skip header
        
        // encrypted
        System.out.println("DecryptorTest: Note: data comes encrypted!");
        
        byte[] key2 = new byte[32];
        Arrays.fill(key2,(byte)0);
        int j=0;
        for (int i=0; i<keyc.length() || i<pwd5.length; i++)
        {
          if (i<keyc.length()) key2[j++]=keyc.toString().substring(i,i+1).getBytes()[0];
          if (i<pwd5.lengthkey2[j++]=pwd5[i];
          if (j>=keysize)
            break;
        }
        System.out.println("DecryptorTest: Personal key to decript reply =["+key2+"]");

        // TODO: Fix key size
        if (j<=16j=16else if (j<=24j=24else if (j<32j=32;
        System.out.println("DecryptorTest: key2.length="+j);
        Cipher cipher = createCipherCipher.DECRYPT_MODE, key2, iv );
        
        r = new BufferedReadernew InputStreamReadernew CipherInputStreamstream, cipher ) ) );
      }
      else
      {
        // plain
        stream.reset();
        r = new BufferedReadernew InputStreamReaderstream ) );
        System.out.println("DecryptorTest: Warning: data comes unencrypted!");
      }
      
      // Чтение строка за строкой для проверки
      System.out.println("====[REPLY FROM SERVER:]===========================");
          String inputLine;
          while ((inputLine = r.readLine()) != null
          {
              System.out.println(inputLine);
          }
          r.close();
          System.out.println("====[SUCESS]=======================================");
          
      catch (IOException e) {
      System.out.println("DecryptorTest: Error: IOException; URL="+surl);
    catch InvalidKeyException
        |  NoSuchAlgorithmException
        |  NoSuchProviderException
        |  NoSuchPaddingException
        |  InvalidAlgorithmParameterException e
    {
      System.out.println("DecryptorTest: Error: cannot decode: Exception="+e.toString());
    
    
    System.out.println("DecryptorTest BYE")
  }

}
Исходник обработан: Java2html

Ради алгоритма Twofish я использовал из GNU-JCE , а также пробовал коммерческий IAIK с закрытым исходным кодом.

См. также: http://www.cryptix.org , http://bouncycastle.org

Tags: , , , , , , ,

Apr. 21st, 2014

12:06 am - Шифровка потока информации PHP-HTTP-Qt. Часть 1

В этом посте расскажу о том, как поточно шифровать данные, передаваемые из PHP-скрипта в программу-клиент по HTTP. «Поточно» обозначает, что шифровка-передача-расшифровка происходит параллельно их поступлению (в данном случае из небуферизированного запроса к MySQL).
Как известно, данные передаваемые по HTTP никак не защищены и могут быть легко перехвачены и даже изменены на промежуточных узлах сети. Поэтому, если не использовать HTTPS, весьма уместно ценные данные при передаче зашифравать.

Данные я предаю в формате XML. Программа клиент сделаная на C++ и Qt расшифровав, направляет их в SAX-парсер (QXmlSimpleReader, XML-handler) и далее в GUI-таблицу QTableWidget «на лету».

Шифровка на сервере выполняется исключительно средствами PHP (поточным фильтром). Для расшифровки информации в клиенте я использовал библиотеку CryptoC++.
Я использовал древнюю Qt 4.5.0. Программа клиент разрабатывалась и компилировалась под Linux, а также (кросс-компилятором) под Windows.
Шифр я выбрал Twofish, который сейчас считается весьма стойким. Шифромание и дешифровка Twofish происходит блоками по 16 байт, причём я использовал режим сцеплания блоков шифротекста (CBC).

Я использовал два пароля — один общий для всех пользователей — вбивается в HEX-формате в настройках программы. Там же вбивается общий начальный вектор инициализации блока шифрования. Также у пользователя есть индивидуальный (относительно короткий) пароль, который он вводит каждый раз при старте программы для авторизации.
Для авторизации я посылаю POST-параметром HEX-форму индивидуального пароля пользователя, зашифрованный общим паролем (переведённым в бинарный формат из HEX-представления). На сервере происходит расшифровка его и сопоставление с хранящимся там паролем (или MD5-суммой).
Конечно, ещё стоит добавить до шифровки изменяющуюся не повотояющуюся последовательность, для того что бы нельзя было тупо перехватить и использовать это шифрованное значение. Сделаю это в будущем.
Рабочий ключ для шифровки и расшифровки данных создаю из бинарного представления MD5-суммы индивидуального пароля и из общего пароля беря оттуда и оттуда байты по очереди.
Мой серверный скрипт умеет посылать нешифрованный ответ — это удобно в случае если произошла какая-то ошибка (например, пароль не верный) и для отладки. Для того что бы отличать шифрованный ответ от не шифрованного в первом случае я в начале отправляю «заголовок» - 32 байта (два 16-байтовых блока), первый из которых является нулём. В XML-е вообще не должно быть нулевых байт, так что это — надёжный признак. Не шифрованный XML идёт обычно, без всякого заголовка.
Все эти детали не относятся прямо к теме, но, надеюсь, они помогут разобраться в моём коде, который я привожу в статье. Он уже прошёл "обкадку" и с уверенностью можно сказать, что всё работает надёжно под Linux и Windows (по крайней мере при компиляции с библиотеками, которые у меня)!

Вот важные фрагменты PHP серверной части:
date_default_timezone_set("Europe/Moscow");

@
ini_set('output_buffering'0);

header('Content-Transfer-Encoding: binary');

$alg  MCRYPT_TWOFISH// MCRYPT_TripleDES; // MCRYPT_PANAMA;
$mode MCRYPT_MODE_CBC// MCRYPT_MODE_STREAM;
$ivsize mcrypt_get_iv_size($alg,$mode); 
$z="";for($i=0;$i<$ivsize;$i++) $z.="\0"// нулевой блок
$iv=$z;

// получаем вектор инициализации из таблицы с настроечными параметрами:
$ksql "SELECT *, MD5(value) as M FROM parameters WHERE name='iv'";
$rr0a $mysqli1->query($ksql); 
if ( 
$rr0a && ($row $rr0a->fetch_assoc()) )
{
  
$ivh $row["M"]; // MD5
  // assert length($ivh)=32
  
$b pack("H*"$ivh); // OR:
  // $b = hex2bin($ivh); // request PHP 5.4 !
  
if ($b===false)
  {
    
$message "110\tCannot decode initialization vecto$ivh!";
  }
  else
  {
    
$iv $b;
  }
}


// проверка авторизации происходит так:
$password = (string)pack("H*"$password );
$password trimmcrypt_decrypt($alg$key$password$mode$iv), "\x00..\x1F" );
user_check$user$password ); // set second argument to false here to disable passwords
$pwd md5($password);



ob_flush
();
$f fopen('php://output''w'); // пишем XML-информацию так: fwrite( $f, …);
// ...[*]...

$encr = ((!$message) || $message<=""); // $message у меня сигнализирует об ошибке
//$encr = false; ← что бы отключить шифровку для отладки

if ($encr) echo $z.md5($iv,true); // write 16 zero bytes + 16 bytes of iv (признак шифрованной информации - «заголовок»)


// формирование рабочего ключа mix $key and $pwd into $key1
$pwd pack("H*"$pwd); // convert to binary
$key1="";
for (
$i=0$i<strlen($key) && $i<strlen($pwd); $i++)
$key1.=$key[$i].$pwd[$i]; }
if (
strlen($key)>strlen($pwd)) $key1.=substr($key,strlen($pwd));
if (
strlen($key)<strlen($pwd)) $key1.=substr($pwd,strlen($key));
// note that maximum key lenght may exceeded
// Twofish::MAX_KEYLENGTH
//$key1=$pwd;

$opts = array('iv'=>$iv'key'=>$key1);
if (
$encrstream_filter_append($f"mcrypt.$alg"STREAM_FILTER_WRITE$opts); // <== активируем поточный фильтр-шифровальщик
//stream_filter_append($f, 'convert.base64-encode');

fwrite($f'<?xml version="1.0" encoding="UTF-8"?>'."\n" ); // и понеслось...

Для того что бы это работало, разумеется нужна поддержка соответствующего фильтра mcrypt.* в PHP. Проверить это можно с помощью функции phpinfo().

На стороне клиента шифрование индивидуального пароля для авторизации и формирование запроса происходит так:

QByteArray Dialog_Settings::encrypt(const QString& plain_str)
{
    byte iv[ CryptoPP::Twofish::BLOCKSIZE ];
    memset(iv,0,sizeof(iv));

    CryptoPP::SecByteBlock keyblock( CryptoPP::Twofish::DEFAULT_KEYLENGTH );

    const QByteArray iva = get_iv();
    // assert iva.size()==BLOCKSIZE()==16
    for (int i=0; i<CryptoPP::Twofish::BLOCKSIZE; i++) iv[i]=iva[i];

    const QString k = get_key();
    qDebug() << "key=" << k;
    keyblock.Assign(
        (const unsigned char*)
        QByteArray::fromHex(k.toLatin1()).data(), // k.toStdString().c_str(),
        k.toStdString().length()/2
    );

    try
    {
        const std::string plain = plain_str.toStdString();
        std::string cipher;

        CryptoPP::CBC_Mode< CryptoPP::Twofish >::Encryption e(
            keyblock, keyblock.size(), iv );

        // The StreamTransformationFilter adds padding
        //  as required. ECB and CBC Mode must be padded
        //  to the block size of the cipher.
        {
            CryptoPP::StringSource ss( plain, true,
                new CryptoPP::StreamTransformationFilter(
                    e,
                    new CryptoPP::StringSink( cipher )
                ) // StreamTransformationFilter
            ); // StringSource
        }

        qDebug() << plain_str << "encryped. size=" << cipher.length();

        return QByteArray( cipher.c_str(), cipher.length() );
    }
    catch( const CryptoPP::Exception& e )
    {
        qDebug() << "Error encryption: " << e.what() << endl;

        return QByteArray();
    }
}
..........

            bool ok = false;
            QString p = QInputDialog::getText(
                    this, // NULL, // this
                    tr("Password"),
                    get_username().isEmpty()?
                        tr("Enter user passord"):
                        tr("Enter password for %1").arg(
                                QString("<b>")+get_username()+"</b>"),
                    QLineEdit::Password,
                    v,
                    &ok,
                    Qt::Window | Qt::WindowStaysOnTopHint // Qt::WType_TopLevel // obsolete
            );
            if (ok)
            {
                m_encrypted = encrypt(p);
..........

    const QString surl = baseurl()+"/xml.php?user="+user;

    QUrl url( surl );

    QUrl postData;
    foreach (const RequestItem& wh, hr.reqrec.where())
    {
        postData.addQueryItem("where[]", wh.where());
    }
    foreach (const QString& col, m_pTableWidget->m_hidden[hr.table])
    {
        postData.addQueryItem("skip[]", col);
        qDebug() << "at table" << hr.table << " skipped " << col;
    }
    qDebug() << "sending password...";
    qDebug() << m_pSettings->get_enc().toHex();
    postData.addQueryItem("password", m_pSettings->get_enc().toHex());  // ← m_pSettings->m_encrypted

    QNetworkRequest request(url);
    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
так как шифрование происходит блоками, нужно что бы входная информация была дополнена до размера кратного длине блока («padding»). Для этого служит класс StreamTransformationFilter, дополнение происходит нулями. Кстати, поточный фильтр mcrypt.TwoFish в PHP тоже для этого использует нулевые байты (и это вроде нигде не написано). Т.к. мы передаём XML, т. е. plain-текст, в котором нет нулевых байт, мы легко можем удалить их с конца (см «де-padding» в конце кода метода Decrypter::readData ниже).

Для поточной расшифровки я сделал класс-обёртку QIODevice под названием Decrypter.
Для начала посмотрите код его заготовки - класса-пустышки Decrypter0, который ничего не делает (можно использовать для отладки, отключив шифрование в PHP):
class Decrypter0 : public QIODevice
{
    Q_OBJECT

public:
    // Decrypter0() {}

    // my methods:
    virtual void unset() { m_source=NULL; } // { setSource(NULL); }

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
    virtual bool setSource(QIODevice* source, QWidget* topwidget)
    { m_source=source; return false; }
#pragma GCC diagnostic pop

    // QIODevice virtual;
    virtual qint64 bytesAvailable () const
    { return m_source==NULL? 0: m_source->bytesAvailable(); }

    virtual bool atEnd() const
    { return m_source==NULL? true: m_source->atEnd(); }

    virtual qint64 size() const
    { return m_source==NULL? 0: m_source->size(); }

    virtual bool open(OpenMode mode)
    {   qDebug() << "open called";
        QIODevice::open(mode);
        if (m_source==NULL) return false;
        return m_source->open(mode);
    }
    virtual void close()
    { QIODevice::close(); if (m_source!=NULL) m_source->close(); }

    // ### Qt 5: pos() and seek() should not be virtual, and
    // ### seek() should call a virtual seekData() function.
    virtual qint64 pos() const
    { return m_source==NULL? qint64(0): m_source->pos(); }
    virtual bool seek(qint64 pos) const
    { return m_source==NULL? false: m_source->seek(pos); }
    virtual bool reset() const
    { return m_source==NULL? false: m_source->reset(); }

    virtual bool canReadLine() const
    { return m_source==NULL? false: m_source->canReadLine(); }

    virtual bool isSequential() const { return true; }

    virtual bool waitForReadyRead ( int msecs )
    { return m_source==NULL? false: m_source->waitForReadyRead(msecs); }

protected:
    virtual void connectNotify ( const char * signal )
    {
        qDebug() << "Don't try to connect the signal to decriptor! "
                 << QLatin1String(signal);
        exit(109);
    }

    virtual qint64 readData(char *data, qint64 maxlen)
    { return m_source==NULL? 0: m_source->read(data,maxlen); }
    // read method is not virtual !

    virtual qint64 readLineData(char *, qint64)
    { exit(111); }

    virtual qint64 writeData(const char *, qint64) { return 0; } // read-only

    QIODevice* m_source;
};
А вот сам Decrypter:
Описание:
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wtype-limits"
#include "cryptopp/cryptlib.h"
#include "cryptopp/twofish.h"
#include "cryptopp/modes.h"
#pragma GCC diagnostic pop

#include <QIODevice>

class Decrypter : public QIODevice
{
    Q_OBJECT

public:
    Decrypter(QIODevice* source, const Dialog_Settings* settings);

    inline qint64 BLOCKSIZE() const;

    virtual qint64 bytesAvailable() const;

    virtual bool atEnd() const { return m_source==NULL? true: m_source->atEnd(); }

    bool unencrypted()  { return !m_protected; } // признак отсутствия шифрования
    size_t get_gc()     { return m_gc; } // только для отладки

protected:
    virtual qint64 readData(char *data, qint64 maxlen);
    virtual qint64 writeData(const char *, qint64) { return 0; } //Этот класс read-only

    QIODevice* m_source;
    bool           m_first;
    bool           m_protected;

    size_t         m_gc;// globar data bytes counter; temporary/ debug - только для отладки

    CryptoPP::CBC_Mode< CryptoPP::Twofish >::Decryption m_d; // дешифратор
};
Имплементация:
#include "decrypter.h"

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wtype-limits"
#include "cryptopp/filters.h"    // StringSink
#include "cryptopp/osrng.h"      // AutoSeededRandomPool
//#include "cryptopp/hex.h"      // HexEncoder
#include "cryptopp/base64.h"
#pragma GCC diagnostic pop

#include <string>
#include <iostream>

#include "dialog_settings.h"

#include <QDebug>


using namespace CryptoPP;

Decrypter::Decrypter(QIODevice* source, const Dialog_Settings* settings)
    : m_source(source)
    , m_first(true)
    , m_protected(false)    // Обозначает, что принимаются/приняты шифрованные данные
    , m_gc(0)
{
    m_first = true;
    m_protected = false;

    byte iv[ BLOCKSIZE() ];
    memset(iv,0,sizeof(iv));

    SecByteBlock keyblock( Twofish::DEFAULT_KEYLENGTH );

    if (settings!=NULL)
    {
        const QByteArray iva = settings->get_iv(); // initialization vector
        // assert iva.size()==BLOCKSIZE()==16
        for (int i=0; i<BLOCKSIZE(); i++) iv[i]=iva[i];

        // формирование рабочего ключа
        QByteArray kk = QByteArray::fromHex(settings->get_key().toLatin1());
        QByteArray kp = QByteArray::fromHex(settings->get_pwd().toLatin1());
        QByteArray k; // Keyblock is to compose from common secret key and user password
        for (int i=0; i<kk.size() || i<kp.size(); i++)
        {
            if (i<kk.size()) k.push_back( kk[i] );
            if (i<kp.size()) k.push_back( kp[i] );
            if (k.size()>=Twofish::MAX_KEYLENGTH)
                break;
        }
        keyblock.Assign(
            (const unsigned char*)
            k.data(), // k.toStdString().c_str(),
            k.size()
        );
    }

    m_d.SetKeyWithIV( keyblock, keyblock.size(), iv );
    // m_d=new CryptoPP::CBC_Mode< CryptoPP::Twofish >::Decryption(
    //  keyblock, keyblock.size(), iv );
}

qint64 Decrypter::BLOCKSIZE() const
{
    return Twofish::BLOCKSIZE;
}

qint64 Decrypter::bytesAvailable() const
{
    if (m_source==NULL) return 0;
    return m_protected?
                BLOCKSIZE()*(qint64)( m_source->bytesAvailable() / BLOCKSIZE() ) :
                m_source->bytesAvailable();
    //return m_source->bytesAvailable(); // <-- так не работает
}

qint64 Decrypter::readData(char *data, qint64 maxlen)
{
    //qDebug() << "Decrypter::m_source=" << m_source;
    if (m_source==NULL) return 0;

    //qDebug() << "Decrypter::m_first=" << m_first;
    if (m_first)
    {
        // выполняется только при приёме первого байта
        // - проверка признака шифровки
        const QByteArray test( (const QByteArray&) m_source->peek(1) );
        if (test.size()==0)
            return 0;
        m_protected = (test.size()==1 && test[0]==0); // передача зашифрована?
        qDebug() << "Data stream:" << (m_protected?"CRYPTED":"OPEN");
        if (m_protected) m_source->seek(32); // skip first 32 bytes ("header")
        m_first = false;
    }

    if (!m_protected)   // Если передача не шифруется просто перекладываем данные
    {
        qint64 r = m_source->read(data,maxlen);
        m_gc+=r;
        return r;
    }

    // qint64 len = BLOCKSIZE()*(qint64)( m_source->bytesAvailable() / BLOCKSIZE() );
    qint64 len = bytesAvailable();
    // qDebug() << "LEN=" << len;

    if (len>maxlen) len = maxlen;
    qint64 len1 = BLOCKSIZE()*((qint64)len/BLOCKSIZE());
    // assign(len1<=maxlen)
    qint64 s = m_source->read(data, len1);
    // assign(s==len1)
    // s - размер прочитанных данных до расшифровки
    size_t s2 = 0; // s2 - размер данных на выходе
    try {
        ArraySink *a = new ArraySink( (byte*)data, (size_t)s ); // объект-посредник для приёма данных в массив data
        StringSource ss( (const byte*)data, (size_t)s, true,
            new StreamTransformationFilter(
                m_d, // это объект CryptoPP::CBC_Mode< CryptoPP::Twofish >::Decryption
                a,
                StreamTransformationFilter::ZEROS_PADDING //StreamTransformationFilter::NO_PADDING
            ) // StreamTransformationFilter
        ); // Source
        s2 = a->TotalPutLength(); // получаем размер данных на выходе
    }
    catch( CryptoPP::Exception& e )
    {
        std::cerr << "Caught Crypto++ Exception: " << e.what() << std::endl;
    }
    for (int j=s-1; j>=s-BLOCKSIZE() && j>=0; j--) // де-padding
        if (data[j]==0) data[j]=' ';               // нулевые байты в конце заменяем пробелами
        else break;

    //qDebug() << "s2="<< s2 << "rest0="<< (len-s) << "rest="<< m_source->bytesAvailable();
    m_gc+=s2;
    //qDebug() << "global.counter=" << m_gc; // чисто для проверки
    return s2;
}

см. продолжение - Часть 2.

Tags: , , , , , , ,
Current Mood: [mood icon] satisfied

12:00 am - Шифровка потока информации PHP-HTTP-Qt. Часть 2

...Это продолжение, начало см. Часть 1!

Decrypter-объект m_decrypter логически "вставляется" между входным потоком QNetworkReply и приёмником QxmlInputSource :

    connect(m_pManager, SIGNAL(finished(QNetworkReply*)),
            this, SLOT(onFinish(QNetworkReply*)));

…
    m_reader = new QXmlSimpleReader;
    // connect QXmlSimpleReader to handlers
    m_reader->setContentHandler(this);
    m_reader->setErrorHandler(this);

    QNetworkRequest request(url);
    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");

    m_first = true;
    m_timestart.start(); // = QTime::currentTime();
    m_timestop  = QTime();
    m_reply = m_pManager->post( request, postData().encodedQuery() );
    qDebug() << "Got new m_reply=" << m_reply;
    //m_reply->setReadBufferSize(16*1024);

    m_decrypter = new Decrypter(m_reply, m_pSettings);
    m_xmlsource = new QxmlInputSource(m_decrypter); // [**]

    connect(m_reply, SIGNAL(readyRead()), this, SLOT(onReadyRead()) );
    connect(m_reply, SIGNAL(downloadProgress(qint64, qint64)),
            this,  SLOT(onDownloadProgress(qint64, qint64)));
К сожалению, мне не удалось добиться того, что бы Decryptor работал в точности также как объект QNetworkReply; возможно причина в том, что он выдаёт информацию только 16-байтовыми порциями и не может в конце правильно оценить размер, но в handler-е SAX-парсера не запускался endDocument();
Мне удалось тем не менее заставить это работать как надо дополнительно «стимулируя» Decryptor в обработчиках сигналов readyRead() и downloadProgress(qint64, qint64)
(если не использовать шифрование и класть m_reply вместо m_decrypter в QxmlInputSource() это не требуется:

void WorkplaceTab::onReadyRead()
{
    if ( m_reader==NULL || m_xmlsource==NULL )
    {
        qDebug() << "WorkplaceTab::onReadyRead() - warning";
        return;
    }

    if (m_first)
    {
        // выполняется один раз - при приёме превой порции данных
        qDebug() << "m_reader->parse(m_xmlsource, true);";
        if (!m_reader->parse(m_xmlsource, true))
            to_idle_state(); // прекращение режима приёма
        m_first = false;
    }

    long watchdog = 100;
    while ( m_decrypter!=NULL && m_decrypter->bytesAvailable()>0 )
    {
        m_xmlsource->fetchData();
        if (!m_reader->parseContinue())
            to_idle_state();    // прекращение режима приёма

        if (watchdog--<0)
            break;
    }
}

void WorkplaceTab::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
    logmessage( tr("%1 bytes and %2 rows loaded").arg(bytesReceived).arg(m_row) );

    if (bytesReceived==bytesTotal)
    {
        // выполняется один раз - после приёма последней порции данных
        qDebug() << "WorkplaceTab::onDownloadProgress LAST TIME";
        if (m_reader==NULL || m_xmlsource==NULL || m_reply==NULL)
        {
            qDebug() << "Warning: m_reader==NULL or m_xmlsource==NULL or m_reply==NULL!";
            return;
        }

        long watchdog=1000000;
        bool b=true;
        while (b && m_xmlsource->data().length()>0 &&--watchdog)
            b = m_reader->parseContinue();

        if (b)
        {
            m_xmlsource->next();        // и ещё раз - вхолостую -
            m_reader->parseContinue();  // что бы вызвалось QXmlInputSource::EndOfDocument
        }
        /*
        if (m_row<=0) // TODO: проверка что QXmlInputSource::EndOfDocument не отработал
            to_idle_state(); // грубая остановка
        */
    }
}

// ?? иногда вызывается до завершения разборки XML! :-O
void WorkplaceTab::onFinish(QNetworkReply *reply)
{
    qDebug() << "WorkplaceTab::onFinish";

    m_timestop.start(); // = QTime::currentTime();

    bool anew = false;

    // отсоединяем обработчики onReadyRead и onDownloadProgress
    //reply->disconnect();
    disconnect(reply, SIGNAL(readyRead()), this, SLOT(onReadyRead()) );
    disconnect(reply, SIGNAL(downloadProgress(qint64, qint64)),
            this,  SLOT(onDownloadProgress(qint64, qint64)));
....
Может кто-то поможет с этим окончательно разобраться?
На этом на сегодя всё... вот так … :-)
Надеюсь вам было интересно!(?)

Tags: , , , , , , ,
Current Mood: [mood icon] satisfied

Apr. 19th, 2014

02:51 am - Qt: сокрытие вкладок в QTabWidget

В стандартном виджете для организации вкадок в Qt нет возможности сокрытия отдельных вкладок (show/hide). Интернете не нашёл решения – предлагают сложные пути: временно удалять вкладки или переделывать виджет.

Кажется мне удалось найти простое решение, оно годится если вы не используйте «задисейбленные» («серые», «режим только-чтение») вкладки (disabled). Кстати, для сокрытых вкладок это и не имеет смысла.

Идея в том, что бы ставить режим disabled вкладкам, которые надо скрыть, а для маскировки вкладок использовать приём задание стиля с нулевым размером вкладки. Причём при установке режима disabled виджет автоматически переключит активную вкладку.

Нужный стиль устанавливается так:

#include "qtesttabwidget.h"

#include <QTabBar>

QTestTabWidget::QTestTabWidget() // наследник QTabWidget
{
    setStyleSheet();
}

void QTestTabWidget::setStyleSheet()
{
    tabBar()->setStyleSheet(
        "QTabBar::tab:disabled { width: 0; height: 0; right: 1px; }" //  ??? border-style: none; margin-left: 1px;
    );
}
У меня чуть-чуть кривовато выглядит последняя граница последней видимой вкладки, если она не активная и если скрыть самую последнюю. Вероятно тут можно как-то добиться идеального отображения и в этом случае поколдовав с полями, смещениями итд в стиле. Если кто разберётся с этим — пожалуйста, отправьте мне результат!
Состояние «:disabled» вроде не документировано для QTabBar::tab, однако у меня в Qt 4.5.0 это работает, думаю, и в новых версиях тоже должно.

Вот тестовая программа (код класса главного окна QMainWindow), которая динамически скрывает/показывает вкладки:
#include "mainwindow.h"

#include <QCheckBox>
#include <QVBoxLayout>
#include <QHBoxLayout>

#include <QLabel>
#include <QTabBar>


MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    QWidget* w = new QWidget(this);
    QVBoxLayout* vl = new QVBoxLayout( w );
    QHBoxLayout* hl = new QHBoxLayout;
    vl->addLayout( hl );
    vl->addWidget( m_tabs = new QTestTabWidget );

    for (int i=0; i<3; i++)
    {
        const QString num = QString::number(i+1);
        QCheckBox* ch = new QCheckBox( QString("Show tab#")+num );
        ch->setChecked(true);
        hl->addWidget(ch);
        m_map.insert( ch, m_tabs->addTab(new QLabel( QString("Label ")+num ), QString("tab#")+num ) );
        // ^ QMap<QCheckBox*,int> m_map;
        connect( ch, SIGNAL(stateChanged(int)), this, SLOT(stateChanged(int)) );
    }

    setCentralWidget( w );
}

void MainWindow::stateChanged( int state )
{
    QCheckBox *p = qobject_cast<QCheckBox*>( sender() );
    if ( p==NULL||!m_map.contains(p) )
        return;

    m_tabs->setTabEnabled( m_map[p], state!=Qt::Unchecked );
    m_tabs->setStyleSheet();
}

PS Наткнулся на одну "кривость": если использовать виджеты на вкладках (в моём случае это раскрывающийся список на том месте, где обычно кнопка закрытия вкладки):
QComboBox *ysel = new YearInTabComboBox( years ); // наследник QComboBox
tabBar()->setTabButton(num, QTabBar::RightSide, ysel); // установка виджета во вкладку num справа от текста
то при сокрытии вкладки этим методом эти виджеты у меня должным образом не скрываются и могут образовать "стопку" уложившись рядом. (С кнопками закрытия, наверное, будет то же самое). Впрочем, на сколько я представляю, во-первых, мало кто ставит виджеты во вкладки, а во-вторых это можно обойти, дополнительно скрывая и показывая такие виджеты на соответствующих вкладках обычным способом (методами show/hide).

Tags: , , , ,
Current Mood: [mood icon] satisfied

Apr. 17th, 2014

09:35 pm - Размер медленной выборки и «последняя страница» (MySQL - PHP - браузер/клиент)

Рассмотрим простую страндартную задачу: данные из таблицы MySQL через PHP направляются в браузер (или иную программу-клиент). В браузере они отображаются в виде таблицы разбитой на «страницы» по 20, 50 или 100 записей на странице. То есть если записей много, пользователь видит только первые, скажем, двадцать, а ниже таблицы переключатель страниц: 1,2,3,..., последняя, называемый «paginator».

Эта задача весьма «заезжанная» и в ней нет ничего сложного, если трудоёмкость выборки из СУБД не большая. В этом случае как правило скрипты PHP принимают аргументом номер показываемой «страницы» N (по умолчанию - N=1) и размер страницы M из фиксированномго множества(20,50,100, а по умолчанию, скажем, M=20) сначала делают запрос SELECT COUNT(*) …. из таблицы с условиями выборки. Полученый размер выборки T делят на M, получая количество страниц C=M/T (приводя до целого числа в большую сторону), проверяют что N<=C, затем делают выборку записей для текущей «страницы» с помощью ...LIMIT (N-1)*M,M
Однако если таблица в MySQL очень большая полная выборка или подсчёт записей в выборке COUNT(*) может затянуться на продолжительное время. При этом, выдача полезных результатов в текущую страницу (особенно в первую) может происходить удовлетворительно быстро:
При не-буверизированном чтении выборки из MySQL записи могут приходить, например, каждые несколько десятков миллисекунд. Таким образом первая «страница», может быть загружена за приемлемое время порядка секунды, но если в выборке сотни тысяч записей и более, запрос COUNT(*), считающий записи, сам по себе может выполняться минуту и более!
Конечно, большого смысла показывать пользователю более нескольких тысяч записей нет. В случае если выборка очень большая, пользователю нужно увидеть просто фрагмент данных, что бы он мог на глаз проверить, что это вообще то что нужно.
Но иногда заказчик хочет, что бы пользователь всё же мог: (a) узнать общее количество записей в выборке; (b) перейти для просмотра на последнюю страницу выбоки.
Эти условия на мой взгляд, являются вполне обоснованными, т.к. могут реально помочь пользователю соориентироваться в его работе.
Мы не можем гарантировать выдачу результата за приемлемое для интерактивного взаимодействия «запрос-ответ» время, но можем показывать пользователю динамическую оценку результата требования (a).
Я имею в виду следующее: получив в течении секунд после запроса первую «страницу» и переключатель страниц: на 1-10 страницы, пользователь наблюдает бегущее увеличивающееся значение количества записей в выборке: «найденно не менее 200 записей...», «не менее 300...», «не менее 400...» итд. По мере этого может соответствующим образом расти и переключатель страниц. Пока не увидет окончательный результат «в выборке 12345 записей!», и появится возможность переключиться на последнюю «страницу».
Как это сделать?
Ни логика реляционных СУБД (в данном случае MySQL) ни логика WWW и HTTP не была изначально расчитана на такую работу.
MySQL не может информировать о состоянии счётчика COUNT(*), до тех пор, пока не подсчёт не будет полностью завершен, то есть задав такой запрос мы должны ждать пока он не выполнится, не дождавшись полного подсчёта мы не можем получить ответ на вопрос «ну а есть ли там хотя бы 100 записей?».
HTTP не был преспособлен для установки постоянных соединений, так как это делают TCP-socket-ы. Однако тут всё же проще: есть что называется «хак», можно просто не закрывать соединение после отработки основной части PHP (предварительно отключив тайм-аут выполнения скриптов!).
После завершающего </HTML> браузер продолжает воспринимать комманды вызова JavaScript-функции с данными в аргументах вида:

<SCRIPT>counter(200);</SCRIPT>
<SCRIPT>counter(300);</SCRIPT>
…
<SCRIPT>total(12345);</SCRIPT>
Естественно, заранее надо создать и загрузить в блоке HEAD эти JavaScript фунцкии counter(...) и total(...), которые будут утилизировать динамически поступающие значения (в данном случае текущая оценка размера выборки) должным образом (показывать их пользователю в должном месте, перестраивать переключатель «страниц» итд). Это не совсем «по-правилам», но это работает. После каждой такой посылки приходится посылать килобайтик пробелов и переводов строки (не считая уж комманды flush(); в PHP), т.к. полностью отключить кеширование по всей цепочке от серверного скрипта до обработчика JavaScript в браузерах не возможно.
При работе с MySQL, ради подсчёта COUNT(*) остаётся только после посылки нужных записей продолжать читать ненужные записи в холостую, ни куда не отправляя результаты, а только пересчитывая их в цикле и вызывая через каждые, скажем, 100 посчитанных записей вывод новой порции тегов «SCRIPT», а в конце -
<SCRIPT>total(...);</SCRIPT> 


В моей работе данные отправлялись не в браузер, а в специальную программу-клиент в формате XML, где разбирались по мере поступления SAX-парсером. С XML такой подход выглядит более элегантно. Я сразу грузил данные для всех «страниц», при их переключении нового запроса к серверу не создавалось. Мой более сложный алгорим всего этого таков:
Запускается небуферезированный цикл чтения выборки. Записи выдаются в клиент, в атрибуте тега XML записи указывается порядковый номер записи. Программма-клинет показывает пользователю записи по мере поступления, и он сразу может с ними работать.
Когда выведено заданное предельное кол-во страниц P, то есть M*P записей, они вместовыдачи в клиент начинают попадать в FIFO буфер $dlist = new SplDoublyLinkedList(); // Класс SplDoublyLinkedList из SPL есть в PHP начинаяя с версии 5.3.0
Этот буфер растёт до предела равного максимальному возможному размеру страницы M:

    $dlist
->push($r); // $r - новый элемент XML с очередной записью из выборки
    
if ($dlsize>=$dlmax// буфер заполнен
    
{
      
$dlist->shift();
    }
    else
    {
      
$dlsize++; // $dlsize это размер буфера
    
}
    
Как только выборка завершилась мы выдаём содержимое этого бувера по HTTP клиенту. В моём коде это выглядит так:
 
      fwrite
$f"<lastblock size=\"$dlsize\">\n" );
      
$dlist->setIteratorMode(SplDoublyLinkedList::IT_MODE_FIFO);
      for (
$dlist->rewind(); $dlist->valid(); $dlist->next()) {
        
fwrite$f$dlist->current() );
      }
      
fwrite$f"</lastblock>\n" );
    
Клиент может понять, что это последняя «страница», во-первых, по тегу lastblock либо, во-вторых, по разрыву в порядковых нумерах записей выборки.
Если же по истечению некоторого времени после начала «чистого счёта» или например, после пересчёта заданного количества записей «конца счёту не видно», запускается второй SQL-запрос COUNT(*), выполнение которого происходит параллельно. Т.к. в первом запросе используется не буфферезированное чтение обязательно надо делать второе соединение с MySQL. Хотя второй одновременно работающий в СУБД запрос тормозит первый, такой подход всё же показывает в среднем выйгрыш в скорости обслуживания.
Эти два условных «потока» выполнения начинают работать параллельно, и информация об общем колличестве записей и о содержании последней «страницы» в конце концов берётся из того потока, который выполнился быстрее (первый по любому поставляет «бегущую» информацию о размере выборки, не позволяя пользователя соскучиться).
В ходе второго «потока» после отработки первого SQL-зароса COUNT(*) запускается второй SQL-запрос с LIMIT-ом для получения записей последней «страницы». Отслеживание его выполнения происходит в цикле чтения основного первого «потока» с помощью упомянутой мной в предыдущем посте конструкции

        
// request with LIMIT is already sent
        
$ready $error $reject = array($mysqli2); // $mysqli2 — объект второго соединения.
        // или может так?: $ready[] = $error[] = $reject[] = $mysqli2;
        
mysqli_poll$ready,$error,$reject0,50000); // wait 1/20 sec
        
if (count($ready)>0)
        { 

    
После того как любой из двух условных потоков завершился происходит корректное закрытие XML, завершение PHP и передачи, и программа-клиент информаирует о том что приём данных, наконец, полностью завершён.

Tags: , , ,
Current Mood: [mood icon] satisfied

Apr. 4th, 2014

04:01 am - Неблокирующий запрос к MySQL из PHP

Казалось бы – что можно сказать нового к такой заезженной и тривиальной теме: доступ к данным в MySQL по запросу через браузер – Apache – PHP?
Оказывается и в такой казалось бы банальной операции можно встретить проблемы, которые мало кто знает как решать.

В задачах, с которыми столкнулся я, запросы к СУБД штатно могут быть столь тяжёлыми, что выполнятся продолжительное время. Заметные задержки при этом оказались неизбежны – даже на самых быстрых серверах с применением рейдов-0 из SSD под данные СУБД. Существенные задержки позволили появиться возможности и проблеме: пользователь может захотеть прервать выполнение запроса, передумать ждать результатов. Иными словами запрос может стать не актуальным, и тогда его надо корректно прервать. То же самое может произойти из-за аварийного обрыва соединения или из-за тайм-аута.

Реализация таких отмен запросов, может потребоваться и в простом web-интерфейсе, и при запросах данных через AJAX, и при взаимодействии с сервером специальных клиентских программами по HTTP. С серверной стороны, в принципе, подобная задача может возникнуть и при получении данных из других СУБД или вообще из других медленных источников. В этой статье я привожу решение, относящееся к извлечению данных именно из MySQL через PHP под Apache2 используя новый программный интерфейс MySQLi, то это относится к множеству вариантов клиентов, но к совершенно конкретному получению данных на сервере.

Знакомство вопросом реализации обработчиков отмены или обрыва сразу выявило принципиальное несовершенство связки PHP-Apache2: PHP-скрипт в принципе не может обнаружить обрыв или даже штатное закрытие соединения до тех пор пока не будет ничего отправлять клиенту в сеть. Причём по не регламентировано сколько данных для этого надо отправить, в указаниях все вроде соглашаются с тем что одного байта вроде бы будет не достаточно, часто отправляют холостые блоки из целого килобайта нулей или пробелов. Возможно, это зависит от конкретного сервера, версии PHP и их настроек. Экспериментально я определил, что на моём сервере можно отправлять 32 перевода строки.

В связке PHP-MySQL тоже оказался существенный изъян: все простые способы чтения данных оказались блокирующими, т.е. они приостанавливают работу PHP скрипта до тех пор, пока MySQL не вернёт их (либо не случится тайм-аут). И соответственно, в это время скрипт ничего послать в сеть не может для того, что бы проверить соединение с HTTP-клиентом. Да, можно применить небуфиризированное чтение, но в моём случае задержки не были связанны с большим количеством записей в выборках; даже извлечение одного единственного значения из СУБД может приостановить ход выполнения PHP. В случае отмены можно, конечно, просто оборвать HTTP-соединение с клиента. Но при этом не только бесполезно перенагружается сервер, который продолжает обрабатывать ненужный “зомби-запрос”, но если вы применяете сессии, а СУБД “уйдёт в себя”, ненароком может произойти глобальная блокировка не только окна браузера с запросом, но и всего сайта, да так, что придётся перегружать всё сбрасывая сессию. Блокировка сессии может случится, если на момент обращения к СУБД, которое затянулось, сессия не была закрыта на запись.

Существует одно универсальное решение – при постановке запроса в MySQL получить номер MySQL-процесса, передать его в клиент. Тот если надо, может его использовать, если понадобится, передав по другому соединению в специальный скрипт на сервере, который “собъёт” ставший не нужным запрос с помощью SQL-зароса-комманды KILL, что в свою очередь приведёт и к разблокировки PHP, обрабатывающего запрос. Этот способ, без сомнения, применим, однако он сложен, некрасив и небезопасен. При его реализации нужно опять-таки иметь в виду опасность блокировок сессии, о которой я упоминал выше.

Мне пришло в голову использовать в PHP параллельный поток (thread) для проверки наличия соединения по HTTP, приблизительно так:

class Ping0 extends Thread {
  public function 
run() {
    
//if (ob_get_level()) ob_end_clean(); 
    
sleep(1);
    echo 
str_repeat("\n",32);
    
flush();    // Error 6 (net::ERR_FILE_NOT_FOUND): The file or directory could not be found.
    
ob_flush();
  }
}

function 
db_query_long($qstring,$conn
{
  
ignore_user_abort(false);
  
//if (ob_get_level()) ob_end_clean(); 

  
$ping0 = new Ping0();
  
$ping0->start(); 
  
  
$ret db_query($qstring,$conn);

  
// $ping0->stop();

  
return $ret;
}
Но этот способ не заработал, скрипт падал с неадекватными ошибками на flush(), создающими впечатление багов в PHP. Встроенный класс потоков Thread в PHP произвёл впечатление очень “сырого”, его даже не было в стандартной инсталляции и что бы воспользоваться требовалось пересобирать PHP из исходников с поддержкой ZTS (Zend Thread Safety) (опции --enable-maintainer-zts или --enable-zts в Windows). В общем, я решил оставить этот путь, хотя его преимущество в том, что он мог бы помочь в решении задачи обработчика отмены запросов не только из MySQL но и из других медленных источников данных.
К счастью, в MySQLi есть нетривиальный способ обработки запроса к БД, который позволил мне благополучно совершить неблокирующее чтение непосредственно.
Итак к вашему вниманию вот фрагмент функции, организующий неблокирующей запрос и возвращающей результат в виде объекта mysqli_result :
    $r $gl_mysqli1->query($sqlMYSQLI_ASYNC ); 

    
ob_implicit_flush(true);
    
ignore_user_abort(false); // можно поставить true

    
for ($i=0$i<$gl_tout$i++) // $gl_tout - тайм-аут; максимальное время в секундах
    
{
      
$ready $error $reject = array($gl_mysqli1);
      
// $ready[] = $error[] = $reject[] = $gl_mysqli1;

      
mysqli_poll$ready,$error,$reject1); // ждём 1 cек; можно воспользоваться пятым параметром - микросекунды
      
if (count($ready)>0)
      {
      
// ready - данные получены
      
$r $gl_mysqli1->reap_async_query();
      if (
$r
      {
        
// успех - данные получены
        
return $r;
      }
      
// some error ??
      
return $r;
      }
      if ( 
count($error)>|| count($reject)>)
      {
      
// какая-то ошибка - error
      
trigger_error("(" $gl_mysqli1->connect_errno ") "
        
$gl_mysqli1->connect_errorE_USER_ERROR);

      return 
null;
      }

      
// проверка соединения с клиентом по HTTP - test connection
      
echo str_repeat("\n",32); // посылка нулей приводит к ошибкам и глюкам
      
flush();
      
ob_flush();

      if (
connection_status()!=CONNECTION_NORMAL)
      {
       
// соединение с клиентом оборволось, запрос на СУБД не актуален
        
return null;
      }
  
      
// возможно нормальное состояние — данные ещё не готовы, надо подождать
    
}
    
// если мы тут - время вышло - таймайт $gl_tout сек.

Метод mysqli::poll до сих пор весьма плохо документирован...
Примечание: лишние переводы строки между управляющими тегами HTML и XML обычно никак не проявляются.

Tags: , , ,

Apr. 2nd, 2014

06:45 pm - Извлечение данных из MS Access-баз простыми, кросс-платформенными и бесплатными способами

По работе мне частенько случается обмениваться большими (иногда многогигабайтовыми) объёмами табличных данных. Такие таблицы обычно приходят в понятном простому пользователю формате — MS Access. Сейчас я расскажу, как можно эффективно извлекать данные из этих файлов для того что бы пустить их в обработку или, например, ввести их в MySQL.

Проблема тут в том, что MS Access — проприетарная программа, которую надо бы покупать, да и не всегда она просто бывает под рукой на конкретной машине, да и сделана под Windows (а я часто на Linux-е). Если архив с базой приходит от коллеги уже на сервер хостинга, приходилось сначала скачивать его на рабочий компьютер, извлекать данные, а потом обычно запаковывать и закачивать извлечённые данные обратно. Кроме этого, на рабочей локальной машине открывать предельно объёмные базы MS Access им самим только лишь для извлечения данных не оптимально по расходу ресурсов оперативной памяти и времени.

Первое на что натыкаешься на этом пути — утилиты mdbtools. Они действительно очень помогли мне. Но к сожалению, у них есть недостатки. Во-первых, не работают с новым форматом accdb. Во-вторых, проблемы с кривыми названиями сущностей в базе (имена таблиц, колонок с пробелами, русскими буквами итд). Добиться ведь, что бы имена колонок были сразу какие удобно для обработки от поставщиков практически не возможно.

Тогда я использовал ODBC. Драйвера для работы с Access-базами (как mdb так и accdb) можно бесплатно взять с сайта Microsoft. Для выгрузки в TSV («значения разделённые табуляцией» — простой универсальный формат, который понимют и офисные программы и MySQL) я написал простейшую программу на Qt.

Этот способ мне то же изрядно помог, однако я столкнулся с такими недостатками:

1) По мере чтения таблицы резервируется оперативная память. Для работы с действительно большими таблицами нужно иметь не менее 4 Gb свободной оперативной памяти.

2) Пожалуй самое страшное — если ресурсов не хвататет, программа прерывает работу без какого-либо сообщения или даже признака ошибки. Иногда экспортируются не все строки таблицы, но ещё хуже, когда в выходных данных пропадают (без какого либо обращающего на себя сообщения!) значения в определённых столбцах.

3) На 64-битную систему ставятся 64-битовые ODBC-драйвера, а что бы с ними работать, программа должна быть также откомпилирована как 64-битовый исполняемый модуль. А для того что бы его сделать понадобится 64-битовый Qt и компилятор…

4) Присутствие в системе JET-движка MS Access-a без MS Office у меня ставит в тупик автоматическую систему обновление Microsoft, тщетно пытающуюся обновить несуществующий Office.

5) Под Linux-ом мне не удалось этим воспользоваться. Хотя в принципе это возможно. Linux-овые ODBC-драйвера для работы с Access-базами платные, но под эмулятором Wine должно быть можно задействовать native-ODBC-драйвера (сделанные для Windows). Инструкции как это сделать есть в Интернете. Но мне не удалось: поддержка этого процесса «из коробки» в новых версиях Ubuntu, очевидно, утратилась. И «подгонять напильником» там, видимо, придётся не мало! Кстати, кому удалось использовать nativeODBC для доступа к Access-файлам через Wine под Ubuntu 12 илм 13 — отпишитесь!

Я уж подумал, ничего лучше не будет — формат то «проприетарный». Но в августе прошлого 2013-го года появилась бесплатная библиоткека Jackcess 2 (см. jackcess.sourceforge.net ), написанная на Java, которая позволяет легко и просто работать с Access-файлами, причём как старыми mdb, так и новыми accdb. Таким образом я не только элементарно самостоятельно сделал конвертер из Access в TSV но и получил его кроссплатформенным — ведь это Java. Теперь пользуюсь им и радуюсь.

Исходники и бинарники конвертеров я выложил для скачки здесь:
netdbview.com/accdb

Успехов в использовании!

Tags: , , , , , , , ,
Current Mood: [mood icon] satisfied

Dec. 24th, 2012

10:26 pm - О самых маленьких компьютерах

Полтора года назад у меня дома завёлся fit-PC2. Иметь абсолютно бесшумный блок, достаточно мощный для веб-серфинга и просмотра видеофильмов - на самом деле удобная штучка. Но увы цена этих коробочек не маленькая. (Поэтому я отложил на неопределённый покупку fit-PC3, который наверняка существенно мощнее).

Так вот, недавно случайно наткнулся на вот такое предложение:
http://www.dont.ru/DONT-Mini-PC-MK802-01-(Cortex-A5(1Ghz)/512mb-DDR3/4GB-ROM/HDMI/USB/WIFI),-minikompyuter,-iptv-player.id16224.html
Вычислительная мощность у них увы мала, но всё же соотношение с ценой вполне даже на уровне! Вот более мощные на Амазоне:
http://www.amazon.com/Ug802-Android-1-2ghz-Cortex-a9-Rockchip/dp/B009A6P2VC/ref=pd_cp_e_2
http://www.amazon.com/Rikomagic-Android-Rockchip-RK3066-1-6Ghz/dp/B00A0I4YJK/ref=pd_cp_e_3
Это устройства с полноценной PC-архитектурой, и на них можно установить Linux Ubuntu.

Так как питаются они через USB и имеют пассивное охлаждение, электрическая мощность у них всего 2-3 ватт!
То такие устройства можно запитать даже от очень маленькой солнечной батареи. Для чего это может быть надо? Ну, к примеру, можно собрать миниматюрную автоматическую автономную систему слежения (которая будет отправлять снимки в Интернет, а то и вообще работать как веб-камера в реальном времени).

Кстати, там же в dont.ru продают редкую вещь — маленький монитор. Нынче маленькие 12-дюймовые и меньше мониторы стоят вдвое дороже больших, и в добавок их сложно сыскать. Маленький монитор, однако, частенько гораздо удобнее большого для наладки системных блоков, «безголовых» серверов и для собственно мониторинга каких-либо меняющихся параметров.

Tags: ,
Current Mood: busy

May. 15th, 2012

03:09 pm - Конвертировать большой тектовой файл из Windows-кодировки в UTF-8

Друзья, подскажите какой утилитой конвертировать txt-файл 12 Gb ?
(iconv тянет только около 4 Gb максимум).
У меня Ubuntu (хотя это не принципиально).

perl, sed - решения итп катят. Можно по скрипт конвертирующий построчно.

Tags:
Current Mood: busy

Dec. 28th, 2011

08:18 pm - Notepad для больших файлов?

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

Файлы протоколов и другие автоматически генерируемые тексты (в моём случае — дампы mysql-баз) могут занимать несколько гигабайт. В конце концов мне таки удалось найти редактор joe, который, о да, умеет это делать. Но интерфейс у него не просто текстовой — он допотопный, из середины 80-х годов, из времён когда Norton Commander-a ещё не было.

А казалось бы такая тривиальная и в общем то не сложная задачка.
Может всё таки есть ещё такие freeware-редакторы? (пусть самые простые, лишь бы была возможность править буковки, а также нужна функция поиск и замена)??

Tags:
Current Mood: [mood icon] stressed

Dec. 2nd, 2011

11:58 pm - TrimSclice в России

В Фиорд-е недавно появился в продаже TrimSlice. Это ультрамаленький безвентиляторный компьютер на базе ARM. Потребление энергии всего несколько ватт, питание 12 вольт.

Модель Trim-Slice Pro - Миниатюрный настольный компьютер на базе NVIDIA Tegra 2 :CPU: NVIDIAR TegraTM 2 Dual Core ARM Cortex A9 1GHz с интегрированным ультра энергоэффективным GeForce GPU, ОЗУ 1 Гбайт DDR2-800, SATA SSD 32 GB, dual head HDMI + DVI, Ethernet
СТОИТ 568 долларов с НДС (в Санкт-Петербурге).

Tags: , ,
Current Mood: busy

Jun. 27th, 2011

11:50 pm - Дорогие приобретения

Сегодня по пути с работы прикупил NAS (сетевое хранилище) D-Link DNS-320 и хард тетрабайтовый SATA-2 Seagate ST1000DL002 "Green".
"Green" потому что мало энергии потребляет и практически не шумит (21-23 дб максимум).
Сетевое хранилище потребляет 17 ватт и питается от тех же 12 вольт. :-) Но дело не в этом. Главное оно хорошо работает.
А ещё оно даже подцепило мой переносной винт по USB.
В общем доволен.

Скоро буду распродавать старые харды IDE и SATA и свой старый комп.

Tags: ,
Current Mood: [mood icon] satisfied

Apr. 20th, 2011

08:59 pm - Монитор для fit-PC2

Недавно я сделал запись о маленьком энергоэффективном неттопе fit-PC2.
К сожалению, у него есть некоторая неудобная особенность: для подключения монитора присутствует только разъём HDMI. И хотя в продаже и есть такие мониторы, их относительно мало.
(Мониторы с DVI-D, кажется, можно подключить через относительно недорогой переходник?)
Кроме этого в Сети есть информация о невозможности работать его видеоадаптера в графическом режиме одновременно со встроенной сетевой картой под системой FreeBSD из-за недоделанных драйверов.

Интересуясь решением этого затруднения, и мысленно подбирая для fit-PC2 подходящую модель монитора наткнулся на монитор Green House GH-USD16K.


Этот монитор подключается к компьютеру по USB, не имея собственного питания потребляет только около 5 ватт энергии.
Строго говоря, сейчас это не единственный монитор подобного рода, но из тех что я видел в интернете он выглядит самым «серьёзным».

Я не рекомендую покупать этот монитор, но просто хочу обратить внимание на такую особенную возможность.
Среди вероятных минусов этого монитора могу предполагать такие:
1.Едва ли есть драйвера для операционных систем кроме MS Windows XP и Vista. :-(
2.В России он пока не продаётся, а когда будет, то стоить наверное будет не меньше $500.
3.Он совсем не сгодится для игр. Быстро меняющееся изображение скорее всего грузит процессор компьютера и никакого 3D-ускорения!

Тем не менее для «офисной» работы в условиях минималистического автономного электрообеспечения такая модель будет удобна.

См. также:
http://www.fcenter.ru/online.shtml?articles/hardware/monitors/21330

Tags: , , ,
Current Mood: busy

Apr. 12th, 2011

08:22 pm - Хостинг! :-)

Один хороший человек, [info]bozhevilnyj@lj, с которым я познакомился прошлым летом в поселении Росы на Украине подарил мне хосинг. Мой домен Shestero.Info сегодня перенаправил туда.
Пока ещё там ничего нет, сайт сейчас есть на следующих зеркалах:
http://reznitsky.info/users/shestero (подарен [info]rezdm@lj)
http://shestero.backtothelander.info (подарен [info]adanil@lj)

Tags: ,
Current Mood: [mood icon] satisfied

Mar. 18th, 2011

09:55 pm - Хостинг ( shestero.info, http://reznitsky.info/users/shestero )

 
Проблемы с хостингом (сайт не работает). :-(
Начались как обычно без всякого предупреждения.
Надеюсь к воскресенью заработает.

PS Надо бы резервный хостинг c PHP для надёжности.

Tags: , ,
Current Mood: [mood icon] distressed

Navigate: (Previous 20 Entries)