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

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. 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