/*************************************************************************** * __________ __ ___. * Open \______ \ ____ ____ | | _\_ |__ _______ ___ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ * * Copyright (C) 2013 Dominik Riebeling * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY * KIND, either express or implied. * ****************************************************************************/ #include #include #include "httpget.h" #define TEST_USER_AGENT "TestAgent/2.3" #define TEST_HTTP_TIMEOUT 10000 #define TEST_BINARY_BLOB "\x01\x10\x20\x30\x40\x50\x60\x70" \ "\x80\x90\xff\xee\xdd\xcc\xbb\xaa" // HttpDaemon is the the class that implements the simple HTTP server. class HttpDaemon : public QTcpServer { Q_OBJECT public: HttpDaemon(quint16 port = 0, QObject* parent = 0) : QTcpServer(parent) { listen(QHostAddress::Any, port); } quint16 port(void) { return this->serverPort(); } #if QT_VERSION < 0x050000 void incomingConnection(int socket) #else // Qt 5 uses a different prototype for this function! void incomingConnection(qintptr socket) #endif { // When a new client connects, the server constructs a QTcpSocket and all // communication with the client is done over this QTcpSocket. QTcpSocket // works asynchronously, this means that all the communication is done // in the two slots readClient() and discardClient(). QTcpSocket* s = new QTcpSocket(this); connect(s, &QIODevice::readyRead, this, &HttpDaemon::readClient); connect(s, &QAbstractSocket::disconnected, this, &HttpDaemon::discardClient); s->setSocketDescriptor(socket); } QList lastRequestData(void) { return m_lastRequestData; } void setResponsesToSend(QList response) { m_requestNumber = 0; m_responsesToSend = response; } void reset(void) { m_requestNumber = 0; m_lastRequestData.clear(); QString now = QDateTime::currentDateTime().toString("ddd, d MMM yyyy hh:mm:ss"); m_defaultResponse = QByteArray( "HTTP/1.1 404 Not Found\r\n" "Date: " + now.toLatin1() + "\r\n" "Last-Modified: " + now.toLatin1() + "\r\n" "Connection: close\r\n" "\r\n"); } private slots: void readClient() { // This slot is called when the client sent data to the server. QTcpSocket* socket = (QTcpSocket*)sender(); // read whole request QString request; while(socket->canReadLine()) { QString line = socket->readLine(); request.append(line); if(request.endsWith("\r\n\r\n")) { m_lastRequestData.append(request); if(m_requestNumber < m_responsesToSend.size()) socket->write(m_responsesToSend.at(m_requestNumber)); else socket->write(m_defaultResponse); socket->close(); m_requestNumber++; } if (socket->state() == QTcpSocket::UnconnectedState) { delete socket; break; } } } void discardClient() { QTcpSocket* socket = (QTcpSocket*)sender(); socket->deleteLater(); } private: int m_requestNumber; QList m_responsesToSend; QList m_lastRequestData; QByteArray m_defaultResponse; }; class TestHttpGet : public QObject { Q_OBJECT private slots: void testFileUrlRequest(void); void testCachedRequest(void); void testUncachedRepeatedRequest(void); void testUncachedMovedRequest(void); void testUserAgent(void); void testResponseCode(void); void testContentToBuffer(void); void testContentToFile(void); void testNoServer(void); void testServerTimestamp(void); void testMovedQuery(void); void init(void); void cleanup(void); public slots: void waitTimeout(void) { m_waitTimeoutOccured = true; } QDir temporaryFolder(void) { // Qt unfortunately doesn't support creating temporary folders so // we need to do that ourselves. QString tempdir; for(int i = 0; i < 100000; i++) { tempdir = QDir::tempPath() + QString("/qttest-temp-%1").arg(i); if(!QFileInfo::exists(tempdir)) break; } QDir().mkpath(tempdir); return QDir(tempdir); } void rmTree(QString folder) { // no function in Qt to recursively delete a folder :( QDir dir(folder); Q_FOREACH(QFileInfo info, dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)) { if(info.isDir()) rmTree(info.absoluteFilePath()); else QFile::remove(info.absoluteFilePath()); } dir.rmdir(folder); } private: HttpDaemon *m_daemon; QByteArray m_port; bool m_waitTimeoutOccured; QString m_now; QDir m_cachedir; HttpGet *m_getter; QSignalSpy *m_doneSpy; QSignalSpy *m_progressSpy; }; void TestHttpGet::init(void) { m_now = QDateTime::currentDateTime().toString("ddd, d MMM yyyy hh:mm:ss"); m_daemon = new HttpDaemon(0, this); // use port 0 to auto-pick m_daemon->reset(); m_port = QString("%1").arg(m_daemon->port()).toLatin1(); m_cachedir = temporaryFolder(); m_getter = new HttpGet(this); m_doneSpy = new QSignalSpy(m_getter, &HttpGet::done); m_progressSpy = new QSignalSpy(m_getter, &HttpGet::dataReadProgress); m_waitTimeoutOccured = false; } void TestHttpGet::cleanup(void) { rmTree(m_cachedir.absolutePath()); if(m_getter) { m_getter->abort(); delete m_getter; m_getter = nullptr; } if(m_daemon) { delete m_daemon; m_daemon = nullptr; } if(m_doneSpy) { delete m_doneSpy; m_doneSpy = nullptr; } if(m_progressSpy) { delete m_progressSpy; m_progressSpy = nullptr; } } void TestHttpGet::testFileUrlRequest(void) { QTimer::singleShot(TEST_HTTP_TIMEOUT, this, &TestHttpGet::waitTimeout); QString teststring = "The quick brown fox jumps over the lazy dog."; QTemporaryFile datafile; datafile.open(); datafile.write(teststring.toLatin1()); m_getter->getFile(QUrl::fromLocalFile(datafile.fileName())); datafile.close(); while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false) QCoreApplication::processEvents(); QCOMPARE(m_doneSpy->count(), 1); QCOMPARE(m_waitTimeoutOccured, false); QCOMPARE(m_daemon->lastRequestData().size(), 0); QCOMPARE(m_getter->readAll(), teststring.toLatin1()); QCOMPARE(m_getter->httpResponse(), 200); QCOMPARE(m_progressSpy->at(0).at(0).toInt(), 0); } /* On uncached requests, HttpGet is supposed to sent a GET request only. */ void TestHttpGet::testUncachedRepeatedRequest(void) { QList responses; responses << QByteArray( "HTTP/1.1 200 OK\r\n" "Date: " + m_now.toLatin1() + "\r\n" "Last-Modified: " + m_now.toLatin1() + "\r\n" "\r\n\r\n"); responses << QByteArray( "HTTP/1.1 200 OK\r\n" "Last-Modified: " + m_now.toLatin1() + "\r\n" "Date: " + m_now.toLatin1() + "\r\n" "\r\n" "\r\n\r\n"); m_daemon->setResponsesToSend(responses); QTimer::singleShot(TEST_HTTP_TIMEOUT, this, &TestHttpGet::waitTimeout); m_getter->getFile(QUrl("http://localhost:" + m_port + "/test1.txt")); while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false) QCoreApplication::processEvents(); QCOMPARE(m_doneSpy->count(), 1); QCOMPARE(m_waitTimeoutOccured, false); QCOMPARE(m_daemon->lastRequestData().size(), 1); QCOMPARE(m_daemon->lastRequestData().at(0).startsWith("GET"), true); // request second time m_getter->getFile(QUrl("http://localhost:" + m_port + "/test1.txt")); while(m_doneSpy->count() < 2 && m_waitTimeoutOccured == false) QCoreApplication::processEvents(); QCOMPARE(m_doneSpy->count(), 2); QCOMPARE(m_waitTimeoutOccured, false); QCOMPARE(m_daemon->lastRequestData().size(), 2); QCOMPARE(m_daemon->lastRequestData().at(1).startsWith("GET"), true); QCOMPARE(m_getter->httpResponse(), 200); } /* With enabled cache HttpGet is supposed to check the server file using a HEAD * request first, then request the file using GET if the server file is newer * than the cached one (or the file does not exist in the cache) */ void TestHttpGet::testCachedRequest(void) { QList responses; responses << QByteArray( "HTTP/1.1 302 Found\r\n" "Location: http://localhost:" + m_port + "/test2.txt\r\n" "Date: " + m_now.toLatin1() + "\r\n" "Last-Modified: " + m_now.toLatin1() + "\r\n" "\r\n"); responses << QByteArray( "HTTP/1.1 200 OK\r\n" "Last-Modified: " + m_now.toLatin1() + "\r\n" "Date: " + m_now.toLatin1() + "\r\n" "\r\n" "\r\n\r\n"); responses << QByteArray( "HTTP/1.1 200 OK\r\n" "Last-Modified: 1 Jan 2000 00:00:00\r\n" "Date: " + m_now.toLatin1() + "\r\n" "\r\n"); m_daemon->setResponsesToSend(responses); QTimer::singleShot(TEST_HTTP_TIMEOUT, this, &TestHttpGet::waitTimeout); m_getter->setCache(m_cachedir); m_getter->getFile(QUrl("http://localhost:" + m_port + "/test1.txt")); while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false) QCoreApplication::processEvents(); QList requests = m_daemon->lastRequestData(); QCOMPARE(m_doneSpy->count(), 1); QCOMPARE(m_doneSpy->at(0).at(0).toInt(), QNetworkReply::NoError); QCOMPARE(m_waitTimeoutOccured, false); QCOMPARE(requests.size(), 2); QCOMPARE(requests.at(0).startsWith("GET"), true); QCOMPARE(requests.at(1).startsWith("GET"), true); QCOMPARE(m_getter->httpResponse(), 200); // request real file, this time the response should come from cache. m_getter->getFile(QUrl("http://localhost:" + m_port + "/test2.txt")); while(m_doneSpy->count() < 2 && m_waitTimeoutOccured == false) QCoreApplication::processEvents(); QCOMPARE(m_doneSpy->count(), 2); // 2 requests, 2 times done() QCOMPARE(m_doneSpy->at(1).at(0).toInt(), QNetworkReply::NoError); QCOMPARE(m_waitTimeoutOccured, false); QCOMPARE(m_daemon->lastRequestData().size(), 3); // redirect will not cache as the redirection target file. QCOMPARE(m_daemon->lastRequestData().at(2).startsWith("GET"), true); QCOMPARE(m_getter->httpResponse(), 200); } /* When a custom user agent is set all requests are supposed to contain it. * Enable cache to make HttpGet performs a HEAD request. Answer with 302, so * HttpGet follows and sends another HEAD request before finally doing a GET. */ void TestHttpGet::testUserAgent(void) { QList responses; responses << QByteArray( "HTTP/1.1 200 OK\r\n" "Date: " + m_now.toLatin1() + "\r\n" "Last-Modified: " + m_now.toLatin1() + "\r\n" "\r\n\r\n"); responses << QByteArray( "HTTP/1.1 200 OK\r\n" "Last-Modified: " + m_now.toLatin1() + "\r\n" "Date: " + m_now.toLatin1() + "\r\n" "\r\n" "\r\n\r\n"); m_daemon->setResponsesToSend(responses); QTimer::singleShot(TEST_HTTP_TIMEOUT, this, &TestHttpGet::waitTimeout); m_getter->setGlobalUserAgent(TEST_USER_AGENT); m_getter->setCache(m_cachedir); m_getter->getFile(QUrl("http://localhost:" + m_port + "/test1.txt")); while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false) QCoreApplication::processEvents(); QList requests = m_daemon->lastRequestData(); QCOMPARE(m_doneSpy->count(), 1); QCOMPARE(m_waitTimeoutOccured, false); QCOMPARE(requests.size(), 1); QCOMPARE(requests.at(0).startsWith("GET"), true); for(int i = 0; i < requests.size(); ++i) { QRegularExpression rx("User-Agent:[\t ]+([a-zA-Z0-9\\./]+)"); auto match = rx.match(requests.at(i)); bool userAgentFound = match.hasMatch(); QCOMPARE(userAgentFound, true); QString userAgentString = match.captured(1); QCOMPARE(userAgentString, QString(TEST_USER_AGENT)); } } void TestHttpGet::testUncachedMovedRequest(void) { QList responses; responses << QByteArray( "HTTP/1.1 302 Found\r\n" "Location: http://localhost:" + m_port + "/test2.txt\r\n" "Date: " + m_now.toLatin1() + "\r\n" "Last-Modified: " + m_now.toLatin1() + "\r\n" "\r\n"); responses << QByteArray( "HTTP/1.1 200 OK\r\n" "Last-Modified: " + m_now.toLatin1() + "\r\n" "Date: " + m_now.toLatin1() + "\r\n" "\r\n" "\r\n\r\n"); m_daemon->setResponsesToSend(responses); QTimer::singleShot(TEST_HTTP_TIMEOUT, this, &TestHttpGet::waitTimeout); m_getter->getFile(QUrl("http://localhost:" + m_port + "/test1.php?var=1&b=foo")); while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false) QCoreApplication::processEvents(); QCOMPARE(m_doneSpy->count(), 1); QCOMPARE(m_waitTimeoutOccured, false); QCOMPARE(m_daemon->lastRequestData().size(), 2); QCOMPARE(m_daemon->lastRequestData().at(0).startsWith("GET"), true); QCOMPARE(m_daemon->lastRequestData().at(1).startsWith("GET"), true); } void TestHttpGet::testResponseCode(void) { QTimer::singleShot(TEST_HTTP_TIMEOUT, this, &TestHttpGet::waitTimeout); m_getter->getFile(QUrl("http://localhost:" + m_port + "/test1.txt")); while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false) QCoreApplication::processEvents(); QCOMPARE(m_doneSpy->count(), 1); QCOMPARE(m_doneSpy->at(0).at(0).toInt(), QNetworkReply::ContentNotFoundError); QCOMPARE(m_waitTimeoutOccured, false); QCOMPARE(m_daemon->lastRequestData().size(), 1); QCOMPARE(m_daemon->lastRequestData().at(0).startsWith("GET"), true); QCOMPARE(m_getter->httpResponse(), 404); } void TestHttpGet::testContentToBuffer(void) { QList responses; responses << QByteArray( "HTTP/1.1 200 OK\r\n" "Last-Modified: " + m_now.toLatin1() + "\r\n" "Date: " + m_now.toLatin1() + "\r\n" "\r\n" TEST_BINARY_BLOB); m_daemon->setResponsesToSend(responses); QTimer::singleShot(TEST_HTTP_TIMEOUT, this, &TestHttpGet::waitTimeout); m_getter->getFile(QUrl("http://localhost:" + m_port + "/test1.txt")); while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false) QCoreApplication::processEvents(); QCOMPARE(m_doneSpy->count(), 1); QCOMPARE(m_waitTimeoutOccured, false); QCOMPARE(m_getter->readAll(), QByteArray(TEST_BINARY_BLOB)); // sizeof(TEST_BINARY_BLOB) will include an additional terminating NULL. QCOMPARE(m_getter->readAll().size(), (int)sizeof(TEST_BINARY_BLOB) - 1); QCOMPARE(m_progressSpy->at(m_progressSpy->count() - 1).at(0).toInt(), (int)sizeof(TEST_BINARY_BLOB) - 1); QCOMPARE(m_progressSpy->at(m_progressSpy->count() - 1).at(1).toInt(), (int)sizeof(TEST_BINARY_BLOB) - 1); } void TestHttpGet::testContentToFile(void) { QTemporaryFile tf(this); QList responses; responses << QByteArray( "HTTP/1.1 200 OK\r\n" "Last-Modified: " + m_now.toLatin1() + "\r\n" "Date: " + m_now.toLatin1() + "\r\n" "\r\n" TEST_BINARY_BLOB); m_daemon->setResponsesToSend(responses); QTimer::singleShot(TEST_HTTP_TIMEOUT, this, &TestHttpGet::waitTimeout); m_getter->setFile(&tf); m_getter->getFile(QUrl("http://localhost:" + m_port + "/test1.txt")); while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false) QCoreApplication::processEvents(); QCOMPARE(m_doneSpy->count(), 1); QCOMPARE(m_waitTimeoutOccured, false); tf.open(); QByteArray data = tf.readAll(); QCOMPARE(data, QByteArray(TEST_BINARY_BLOB)); QCOMPARE((unsigned long)data.size(), sizeof(TEST_BINARY_BLOB) - 1); tf.close(); } void TestHttpGet::testNoServer(void) { QTimer::singleShot(TEST_HTTP_TIMEOUT, this, &TestHttpGet::waitTimeout); m_getter->getFile(QUrl("http://localhost:53/test1.txt")); while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false) QCoreApplication::processEvents(); QCOMPARE(m_doneSpy->count(), 1); QCOMPARE(m_doneSpy->at(0).at(0).toInt(), QNetworkReply::ConnectionRefusedError); QCOMPARE(m_waitTimeoutOccured, false); } void TestHttpGet::testServerTimestamp(void) { QList responses; responses << QByteArray( "HTTP/1.1 200 OK\r\n" "Last-Modified: Wed, 20 Jan 2010 10:20:30\r\n" // RFC 822 "Date: Wed, 20 Jan 2010 10:20:30\r\n" "\r\n" "\r\n"); responses << QByteArray( "HTTP/1.1 200 OK\r\n" "Last-Modified: Sat Feb 19 09:08:07 2011\r\n" // asctime "Date: Sat Feb 19 09:08:07 2011\r\n" "\r\n" "\r\n"); QList times; times << QDateTime::fromString("2010-01-20T11:20:30", Qt::ISODate); times << QDateTime::fromString("2011-02-19T10:08:07", Qt::ISODate); m_daemon->setResponsesToSend(responses); QTimer::singleShot(TEST_HTTP_TIMEOUT, this, &TestHttpGet::waitTimeout); int count = m_doneSpy->count(); for(int i = 0; i < responses.size(); ++i) { m_getter->getFile(QUrl("http://localhost:" + m_port + "/test1.txt")); while(m_doneSpy->count() == count && m_waitTimeoutOccured == false) QCoreApplication::processEvents(); count = m_doneSpy->count(); QCOMPARE(m_getter->timestamp(), times.at(i)); } } void TestHttpGet::testMovedQuery(void) { QList responses; responses << QByteArray( "HTTP/1.1 302 Found\r\n" "Location: http://localhost:" + m_port + "/test2.php\r\n" "Date: " + m_now.toLatin1() + "\r\n" "Last-Modified: " + m_now.toLatin1() + "\r\n" "\r\n"); responses << QByteArray( "HTTP/1.1 200 OK\r\n" "Last-Modified: " + m_now.toLatin1() + "\r\n" "Date: " + m_now.toLatin1() + "\r\n" "\r\n" "\r\n\r\n"); m_daemon->setResponsesToSend(responses); QTimer::singleShot(TEST_HTTP_TIMEOUT, this, &TestHttpGet::waitTimeout); m_getter->getFile(QUrl("http://localhost:" + m_port + "/test1.php?var=1&b=foo")); while(m_doneSpy->count() == 0 && m_waitTimeoutOccured == false) QCoreApplication::processEvents(); QCOMPARE(m_doneSpy->count(), 1); QCOMPARE(m_waitTimeoutOccured, false); QCOMPARE(m_getter->httpResponse(), 200); QCOMPARE(m_daemon->lastRequestData().size(), 2); QCOMPARE(m_daemon->lastRequestData().at(0).startsWith("GET"), true); QCOMPARE(m_daemon->lastRequestData().at(1).startsWith("GET"), true); // current implementation keeps order of query items. qDebug() << m_daemon->lastRequestData().at(1); QCOMPARE(m_daemon->lastRequestData().at(1).contains("/test2.php?var=1&b=foo"), true); } QTEST_MAIN(TestHttpGet) // this include is needed because we don't use a separate header file for the // test class. It also needs to be at the end. #include "test-httpget.moc"