Перехват загружаемых ресурсов в QtWebkit

в 18:19, , рубрики: qt, Qt Software, qt5, qtwebkit, webkit, Программирование, метки: , , ,
Habrahabr::Instance()->hello();

Я давно ничего не писал на хабр, достаточно давно. Но на прошедшей неделе я изрядно пот**хался с модулем QtWebkit 5.1 и решил, что хорошим тоном было бы рассказать вам какой мрак ждет вас там, в случае, если вы захотите попробовать захватить изображение с экрана или что-то в этом роде.

На самом деле, моя задача лежала в том, чтобы сделать браузер, который сохраняет все изображения со всех страниц, которые он браузит. Элементарная задача, на первый взгляд: повесить обработчик на отдельный поток, который перебирает все QWebElement по селектору “img” и отрисовывает их содержание (QWebElement::render()) через QPainter на QImage, который, в свою очередь, сохраняется в файл.

Но оказалось, что не все так просто, к сожалению. Про тот путь самурая, которым я воспользовался, чтобы выполнить поставленную задачу изложен мной под катом этого поста. Приятного аппетита!

Этап 1. Проблема

Я реализовал алгоритм, приведенный в предыдущем параграфе на самом последнем Qt 5 из Git на Mac, собирая Clang 64-bit. В общем, алгоритм не работал. Все сохраненные изображения были либо черными прямоугольниками, либо адовым трэшем. Тут-то я и вспомнил, что есть пример (ССЫЛКА) из поставки Qt 5, который реализует аналогичный функционал. Я быстро собрал его и применил точно так, как было указано в README. Не работает. Оффициальный пример, как ни странно — не работает. Затестил на линуксе — аналогично.

И что же делать? А ничего, не работает эта штука и непонятно почему. У меня не было времени разбираться с этим, так что я искал альтернативные пути решения. Попробовал, внимание, мсье искушений в студии способ с передачей изображения на бэкенд через JavaScript. Способ довольно прост — берем картинку, рисуем ее на canvas и отправляем на бэкенд содержание канвы в base64. Там — дешифруем, чистим и переводим в чистое изображение.

Безобразие! Этот способ предоставил мне аналогичные изображения, что и предыдущий. Что-то тут явно не так, но я бегу, у меня нету времени смотреть назад и тут же рождается другое решение!

Этап 2. Решение

А что, если мы будем перехватывать ресурсы, которые подгружает страница? Почему бы и нет, подумал я. Быстро ушел читать доки QNetworkAccessManager — ура! А вот оно как работает. У нас есть QWebView, которому мы свободное задаем QWebPage с заданным ранее кастомным QNetworkAccessManager, который, на самом деле, наш класс — InterceptorManager (наследуется от QNAM).

Определение InterceptorManager примерно такое:

class InterceptorManager : public QNetworkAccessManager
{
   Q_OBJECT
public:
   explicit InterceptorManager(QObject *parent = 0);
  
protected:
   QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData)
   {
       QNetworkReply *real = QNetworkAccessManager::createRequest(op, request, outgoingData);
       if (request.url().toString().endsWith(".png")) {
           NetworkReplyProxy *proxy = new NetworkReplyProxy(this, real);
           return proxy;
       }
       return real;
   }
};

Мы переопределяем createRequest(), и для всех запросов возвращаем созданную нами прокси класса QNetworkReply. Зачем это надо? QNetworkReply, как наследник QIODevice не обладает возможностью повторного чтения содержимого. Так как нам надо, чтобы QWebPage таки прорендерил изображение. Используя прокси, мы сможем откопировать содержимое и позже его использовать.

Так как проксировать QNetworkReply — не самая простая задача, поэтому, я наведу пример:

networkreplyproxy.h

#include <QApplication>

#include <QWebFrame>
#include <QWebPage>
#include <QWebView>
#include <QWebSettings>
#include <QDebug>

#include <QDateTime>
#include <QDebug>
#include <QFile>
#include <QTimer>
#include <QNetworkProxy>
#include <QNetworkReply>
#include <QNetworkCookie>

class NetworkReplyProxy : public QNetworkReply {
    Q_OBJECT
public:
    NetworkReplyProxy(QObject* parent, QNetworkReply* reply)
        : QNetworkReply(parent)
        , m_reply(reply)
    {
        // apply attributes...
        setOperation(m_reply->operation());
        setRequest(m_reply->request());
        setUrl(m_reply->url());

        // handle these to forward
        connect(m_reply, SIGNAL(metaDataChanged()), SLOT(applyMetaData()));
        connect(m_reply, SIGNAL(readyRead()), SLOT(readInternal()));
        connect(m_reply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(errorInternal(QNetworkReply::NetworkError)));

        // forward signals
        connect(m_reply, SIGNAL(finished()), SIGNAL(finished()));
        connect(m_reply, SIGNAL(uploadProgress(qint64,qint64)), SIGNAL(uploadProgress(qint64,qint64)));
        connect(m_reply, SIGNAL(downloadProgress(qint64,qint64)), SIGNAL(downloadProgress(qint64,qint64)));

        // for the data proxy...
        setOpenMode(ReadOnly);
    }

    ~NetworkReplyProxy()
    {
        if (m_reply->url().scheme() != "data")
            writeDataPrivate();
        delete m_reply;
    }

    // virtual  methids
    void abort() { m_reply->abort(); }
    void close() { m_reply->close(); }
    bool isSequential() const { return m_reply->isSequential(); }

    // not possible...
    void setReadBufferSize(qint64 size) { QNetworkReply::setReadBufferSize(size); m_reply->setReadBufferSize(size); }

    // ssl magic is not done....
    // isFinished()/isRunning can not be done *sigh*


    // QIODevice proxy...
    virtual qint64 bytesAvailable() const
    {
        return m_buffer.size() + QIODevice::bytesAvailable();
    }

    virtual qint64 bytesToWrite() const { return -1; }
    virtual bool canReadLine() const { qFatal("not implemented"); return false; }

    virtual bool waitForReadyRead(int) { qFatal("not implemented"); return false; }
    virtual bool waitForBytesWritten(int) { qFatal("not implemented"); return false; }

    virtual qint64 readData(char* data, qint64 maxlen)
    {
        qint64 size = qMin(maxlen, qint64(m_buffer.size()));
        memcpy(data, m_buffer.constData(), size);
        m_buffer.remove(0, size);
        return size;
    }

signals:
    void resourceIntercepted(QByteArray);

public Q_SLOTS:
    void ignoreSslErrors() { m_reply->ignoreSslErrors(); }
    void applyMetaData() {
        QList<QByteArray> headers = m_reply->rawHeaderList();
        foreach(QByteArray header, headers)
            setRawHeader(header, m_reply->rawHeader(header));

        setHeader(QNetworkRequest::ContentTypeHeader, m_reply->header(QNetworkRequest::ContentTypeHeader));
        setHeader(QNetworkRequest::ContentLengthHeader, m_reply->header(QNetworkRequest::ContentLengthHeader));
        setHeader(QNetworkRequest::LocationHeader, m_reply->header(QNetworkRequest::LocationHeader));
        setHeader(QNetworkRequest::LastModifiedHeader, m_reply->header(QNetworkRequest::LastModifiedHeader));
        setHeader(QNetworkRequest::SetCookieHeader, m_reply->header(QNetworkRequest::SetCookieHeader));

        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute));
        setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute));
        setAttribute(QNetworkRequest::RedirectionTargetAttribute, m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute));
        setAttribute(QNetworkRequest::ConnectionEncryptedAttribute, m_reply->attribute(QNetworkRequest::ConnectionEncryptedAttribute));
        setAttribute(QNetworkRequest::CacheLoadControlAttribute, m_reply->attribute(QNetworkRequest::CacheLoadControlAttribute));
        setAttribute(QNetworkRequest::CacheSaveControlAttribute, m_reply->attribute(QNetworkRequest::CacheSaveControlAttribute));
        setAttribute(QNetworkRequest::SourceIsFromCacheAttribute, m_reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute));
        setAttribute(QNetworkRequest::DoNotBufferUploadDataAttribute, m_reply->attribute(QNetworkRequest::DoNotBufferUploadDataAttribute));
        emit metaDataChanged();
    }
    void errorInternal(QNetworkReply::NetworkError _error)
    {
        setError(_error, errorString());
        emit error(_error);
    }
    void readInternal()
    {
        QByteArray data = m_reply->readAll();
        m_data += data;
        m_buffer += data;
        emit readyRead();
    }

protected:
    void writeDataPrivate()
    {
        QByteArray httpHeader;
        QList<QByteArray> headers = rawHeaderList();
        foreach(QByteArray header, headers) {
            if (header.toLower() == "content-encoding"
                || header.toLower() == "transfer-encoding"
                || header.toLower() == "content-length"
                || header.toLower() == "connection")
                continue;

            // special case for cookies.... we need to generate separate lines
            // QNetworkCookie::toRawForm is a bit broken and we have to do this
            // ourselves... some simple heuristic here..
            if (header.toLower() == "set-cookie") {
                QList<QNetworkCookie> cookies = QNetworkCookie::parseCookies(rawHeader(header));
                foreach (QNetworkCookie cookie, cookies) {
                    httpHeader += "set-cookie: " + cookie.toRawForm() + "rn";
                }
            } else {
                httpHeader += header + ": " + rawHeader(header) + "rn";
            }
        }
        httpHeader += "content-length: " + QByteArray::number(m_data.size()) + "rn";
        httpHeader += "rn";

        if(m_reply->error() != QNetworkReply::NoError) {
            qWarning() << "tError with: " << this << url() << error();
            return;
        }

        const QByteArray origUrl = m_reply->url().toEncoded();
        const QByteArray strippedUrl = m_reply->url().toEncoded(QUrl::RemoveFragment | QUrl::RemoveQuery);
        interceptResource(origUrl, m_data, httpHeader, operation(), attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
    }

    void interceptResource(const QByteArray& url, const QByteArray& data, const QByteArray& header, int operation, int response)
    {
        Q_UNUSED(header);
        Q_UNUSED(url);
        Q_UNUSED(operation);
        Q_UNUSED(response);

        emit resourceIntercepted(data);
    }

private:
    QNetworkReply* m_reply;
    QByteArray m_data;
    QByteArray m_buffer;
};

Сразу говорю, что особо не могу отвечать за правильность строк, указанных выше, но эта штуковина работает, а это — самое главное. В любом случае, безопасность использования этой прокси лежит исключительно на ваших плечах.

Заключение

Я проверил все это добро на одном коммерческом проекте — работает. Надеюсь это чем-то кому-то поможет.

Спасибо за внимание,
namespace

Автор: namespace

Источник

Поделиться

* - обязательные к заполнению поля