Back to Home

Interception of loaded resources in QtWebKit or how I saddled a unicorn under dubstep

qt · qtwebkit · webkit · qt5

Interception of loaded resources in QtWebKit or how I saddled a unicorn under dubstep


    Habrahabr::Instance()->hello();


    I didn’t write anything for a long time, for a long time. But last week I pretty much shook ** with the QtWebkit 5.1 module and decided that it would be nice to tell you what gloom awaits you there, in case you want to try to capture a screen image or something like that.

    In fact, my task was to make a browser that saves all the images from all the pages that it browses. An elementary task, at first glance: hang the handler on a separate thread, which iterates over all QWebElement by the selector “img” and draws their contents (QWebElement :: render ()) through QPainter on QImage, which, in turn, is saved to a file.

    But it turned out that not everything is so simple, unfortunately. About the path of the samurai, which I took to complete the task set out by me under the cut of this post. Bon Appetit!


    Stage 1. Problem


    I implemented the algorithm given in the previous paragraph on the most recent Qt 5 from Git on Mac, building Clang 64-bit. In general, the algorithm did not work. All saved images were either black rectangles or hell trash. It was then that I remembered that there is an example from the delivery of Qt 5, which implements similar functionality. I quickly assembled it and applied it exactly as indicated in README. Does not work. An official example, oddly enough, does not work. I tested it on Linux - similarly.

    And what to do? But nothing, this thing does not work and it is not clear why. I did not have time to deal with this, so I was looking for alternative solutions. I tried, attention, monsieur of temptations in the studio a method with transferring images to the backend via JavaScript. The method is quite simple - take a picture, draw it on canvas and send the contents of the canvas in base64 to the backend. There we decode, clean and translate into a clear image.

    Disgrace! This method provided me with similar images as the previous one. Something is clearly not right here, but I’m running, I don’t have time to look back and then another solution is born!


    Stage 2. Solution


    But what if we intercept the resources that the page loads? Why not, I thought. Quickly left to read docks QNetworkAccessManager - hurray! And here it is how it works. We have a QWebView, to which we freely set QWebPage with the previously defined custom QNetworkAccessManager, which, in fact, our class is InterceptorManager (inherited from QNAM).

    The definition of InterceptorManager is something like this:

    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;
       }
    };
    

    We override createRequest (), and for all requests we return the QNetworkReply class proxy that we created. Why is this necessary? QNetworkReply, as a descendant of QIODevice, does not have the ability to read content again. Since we need QWebPage to render the image. Using a proxy, we can copy the contents and use it later.

    Since proxying QNetworkReply is not an easy task, therefore, I will give an example:
    networkreplyproxy.h (deprecated - the most recent in the repository below)
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    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 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 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 cookies = QNetworkCookie::parseCookies(rawHeader(header));
                    foreach (QNetworkCookie cookie, cookies) {
                        httpHeader += "set-cookie: " + cookie.toRawForm() + "\r\n";
                    }
                } else {
                    httpHeader += header + ": " + rawHeader(header) + "\r\n";
                }
            }
            httpHeader += "content-length: " + QByteArray::number(m_data.size()) + "\r\n";
            httpHeader += "\r\n";
            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;
    };
    


    I must say right away that I especially cannot answer for the correctness of the lines indicated above, but this thing works, and this is the most important thing. In any case, the safety of using this proxy lies solely on your shoulders.

    Conclusion


    I checked all this stuff on one commercial project - it works. I hope this helps someone with something.

    Thank you for your attention,
    namespace

    UPD: I designed it as a project on the github - github.com/tucnak/qtwebkit-ri , Bon appetit.

    Only registered users can participate in the survey. Please come in.

    Is it worth it to bring proxy to mind and start a repository on GitHub

    • 74.2% Yes 144
    • 25.7% No 50

    Read Next