Skip to content

Implementing a Reusable Custom QNetworkReply

Saturday, 28 August 2010  |  rich

In my last blog post, I showed how to use a proxy QNetworkAccessManager to restrict network accesses to sites included in a whitelist. One limitation the previous version had is that it stopped the disallowed requests by rewriting the request itself to be to an invalid url. This then caused the default implementation of QNetworkAccessManager to generate an error reply for us. This post will look at how to create a custom reply directly, to allow us to display messages to the user etc. or even provide 'virtual' content.

The approach I've taken is to try to write a reusable utility class for sending basic network replies. I've called the class QCustomNetworkReply since if there's a reasonable level of support then I'll try to work it up into a merge request for Qt. To begin with, lets see how the whitelisting proxy looks now that it uses the new class:

QNetworkReply *WhiteListNetworkAccessManager::createRequest( Operation op,
                                                             const QNetworkRequest &req,
                                                             QIODevice *outgoingData )
{
    // If host is not whitelisted then kill it
    if ( !isAllowed( req.url().host() ) ) {
        QCustomNetworkReply *reply = new QCustomNetworkReply();
        reply->setHttpStatusCode( 403, "Forbidden" );
        reply->setContentType("text/html");
        reply->setContent( QString("<html><body><h1>That url is not in the whitelist</h1></body></html>") );

        return reply;
    }

    QNetworkReply *reply = QNetworkAccessManager::createRequest( op, myReq, outgoingData );
    return reply;
}

The new code operates if the request is to be disallowed. First it creates our custom reply, it then specifies the HTTP response code to send. Finally it sets up the content type and the content itself. Finally, we return our custom reply. As you can see, the API of the QCustomNetworkReply class is fairly simple to use (in fact the only required part is setting the content).

Now we've seen how it's used, lets take a look at how the custom network reply works. The class declaration is fairly simple:

class QCustomNetworkReply : public QNetworkReply
{
    Q_OBJECT

public:
    QCustomNetworkReply( QObject *parent=0 );
    ~QCustomNetworkReply();

    void setHttpStatusCode( int code, const QByteArray &statusText = QByteArray() );
    void setHeader( QNetworkRequest::KnownHeaders header, const QVariant &value );
    void setContentType( const QByteArray &contentType );

    void setContent( const QString &content );
    void setContent( const QByteArray &content );

    void abort();
    qint64 bytesAvailable() const;
    bool isSequential() const;

protected:
    qint64 readData(char *data, qint64 maxSize);

private:
    struct QCustomNetworkReplyPrivate *d;
};

The first group of methods are those for setting the various headers on our response. They are really just convenience wrappers around existing methods (for example making some protected functionality public). I won't go into any more detail about them since the implementations are obvious looking at the source. The two setContent methods are where things start to get interesting:

void QCustomNetworkReply::setContent( const QString &content )
{
    setContent(content.toUtf8());
}

void QCustomNetworkReply::setContent( const QByteArray &content )
{
    d->content = content;
    d->offset = 0;

    open(ReadOnly | Unbuffered);
    setHeader(QNetworkRequest::ContentLengthHeader, QVariant(content.size()));

    QTimer::singleShot( 0, this, SIGNAL(readyRead()) );
    QTimer::singleShot( 0, this, SIGNAL(finished()) );
}

The first method is simple a convenience and allows us to use a QString rather than a bytearray for the content, yhe second is where the real action is. First, we store the content. Next, we zero the offet that stores how much data has been read from our reply (remember a QNetworkReply is a QIODevice) and open our io device. Now that we have the content, we also set the response header that specifies the amount of data we have. Finally, we use two single shot timers to cause the readyRead() and finished() signal to be emitted when the event loop is reentered. We can't simply emit these signals immediately since the reply has not yet been returned by our QNetworkAccessManager, so nothing is listening for them.

The final part of the code is to provide a basic implementation of a QIODevice to allow the stored content to be read back out:

void QCustomNetworkReply::abort()
{
    // NOOP
}


qint64 QCustomNetworkReply::bytesAvailable() const
{
    return d->content.size() - d->offset;
}

bool QCustomNetworkReply::isSequential() const
{
    return true;
}


qint64 QCustomNetworkReply::readData(char *data, qint64 maxSize)
{
    if (d->offset >= d->content.size())
        return -1;

    qint64 number = qMin(maxSize, d->content.size() - d->offset);
    memcpy(data, d->content.constData() + d->offset, number);
    d->offset += number;

    return number;
}

That's all there is to it - see all fairly straight forward. The nice part is that now we have the QCustomNetworkReply, we don't need to do any of that work again and can simply reuse this class whenever we want to send data directly to clients of QNetworkAccessManager. As usual, the code is available from my qt-examples git repository at http://gitorious.org/qt-examples/.