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

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

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