// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QTest>
#include <QtNetwork/QtNetwork>
#include <QtCore/QDateTime>
#include <QtCore/QElapsedTimer>
#include <QtCore/QTextStream>
#include <QtCore/QRandomGenerator>
#include <QtCore/QStandardPaths>
#include <QtCore/private/qiodevice_p.h>

#include "../../network-settings.h"

#ifndef QT_NO_OPENSSL
QT_BEGIN_NAMESPACE
void qt_ForceTlsSecurityLevel();
QT_END_NAMESPACE
#endif

class tst_NetworkSelfTest: public QObject
{
    Q_OBJECT
    // This is either old server's address, or the new http
    // server's address (different from ftp, for example):
    QHostAddress cachedIpAddress;
    // This is only for the new docker test server:
    QHostAddress ftpServerIpAddress;
public:
    tst_NetworkSelfTest();
    virtual ~tst_NetworkSelfTest();

    QHostAddress serverIpAddress();

private slots:
    void initTestCase();
    void hostTest();
    void dnsResolution_data();
    void dnsResolution();
    void serverReachability();
    void remotePortsOpen_data();
    void remotePortsOpen();

    // specific protocol tests
    void ftpServer();
    void ftpProxyServer();
    void imapServer();
    void httpServer();
    void httpServerFiles_data();
    void httpServerFiles();
    void httpServerCGI_data();
    void httpServerCGI();
#ifndef QT_NO_SSL
    void httpsServer();
#endif
    void httpProxy();
    void httpProxyBasicAuth();
    void httpProxyNtlmAuth();
    void socks5Proxy();
    void socks5ProxyAuth();
    void smbServer();

    // ssl supported test
    void supportsSsl();
};

class Chat
{
public:
    enum Type {
        Reconnect,
        Send,
        Expect,
        SkipBytes,
        DiscardUntil,
        DiscardUntilDisconnect,
        Disconnect,
        RemoteDisconnect,
        StartEncryption
    };
    Chat(Type t, const QByteArray &d)
        : data(d), type(t)
    {
    }
    Chat(Type t, int val = 0)
        : value(val), type(t)
    {
    }

    static inline Chat send(const QByteArray &data)
    { return Chat(Send, data); }
    static inline Chat expect(const QByteArray &data)
    { return Chat(Expect, data); }
    static inline Chat discardUntil(const QByteArray &data)
    { return Chat(DiscardUntil, data); }
    static inline Chat skipBytes(int count)
    { return Chat(SkipBytes, count); }

    QByteArray data;
    int value;
    Type type;
};

static QString prettyByteArray(const QByteArray &array)
{
    // any control chars?
    QString result;
    result.reserve(array.size() + array.size() / 3);
    for (int i = 0; i < array.size(); ++i) {
        char c = array.at(i);
        switch (c) {
        case '\n':
            result += "\\n";
            continue;
        case '\r':
            result += "\\r";
            continue;
        case '\t':
            result += "\\t";
            continue;
        case '"':
            result += "\\\"";
            continue;
        default:
            break;
        }

        if (c < 0x20 || uchar(c) >= 0x7f) {
            result += '\\';
            result += QString::number(uchar(c), 8);
        } else {
            result += c;
        }
    }
    return result;
}

enum { defaultReadTimeoutMS = 4000 };

static bool doSocketRead(QTcpSocket *socket, int minBytesAvailable, int timeout = defaultReadTimeoutMS)
{
    QElapsedTimer timer;
    timer.start();
    int t = timeout;
    forever {
        if (socket->bytesAvailable() >= minBytesAvailable)
            return true;
        if (socket->state() == QAbstractSocket::UnconnectedState)
            return false;
        if (!socket->waitForReadyRead(t))
            return false;
        t = qt_subtract_from_timeout(timeout, timer.elapsed());
        if (t == 0)
            return false;
    }
}

static QByteArray msgDoSocketReadFailed(const QString &host, quint16 port,
                                        int step, int minBytesAvailable)
{
    return "Failed to receive "
        + QByteArray::number(minBytesAvailable) + " bytes from "
        + host.toLatin1() + ':' + QByteArray::number(port)
        + " in step " + QByteArray::number(step) + ": timeout";
}

static bool doSocketFlush(QTcpSocket *socket, int timeout = 4000)
{
#ifndef QT_NO_SSL
    QSslSocket *sslSocket = qobject_cast<QSslSocket *>(socket);
#endif
    QElapsedTimer timer;
    timer.start();
    int t = timeout;
    forever {
        if (socket->bytesToWrite() == 0
#ifndef QT_NO_SSL
            && sslSocket->encryptedBytesToWrite() == 0
#endif
            )
            return true;
        if (socket->state() == QAbstractSocket::UnconnectedState)
            return false;
        if (!socket->waitForBytesWritten(t))
            return false;
        t = qt_subtract_from_timeout(timeout, timer.elapsed());
        if (t == 0)
            return false;
    }
}

static void netChat(int port, const QList<Chat> &chat)
{
#ifndef QT_NO_SSL
    QSslSocket socket;
#else
    QTcpSocket socket;
#endif
    const auto serverName = QtNetworkSettings::hostWithServiceOnPort(port);
    socket.connectToHost(serverName, port);
    qDebug() << 0 << "Connecting to server on port" << port;
    QVERIFY2(socket.waitForConnected(10000),
             QString("Failed to connect to server in step 0: %1").arg(socket.errorString()).toLocal8Bit());

    // now start the chat
    QList<Chat>::ConstIterator it = chat.constBegin();
    for (int i = 1; it != chat.constEnd(); ++it, ++i) {
        switch (it->type) {
            case Chat::Expect: {
                    qDebug() << i << "Expecting" << prettyByteArray(it->data);
                    if (!doSocketRead(&socket, it->data.size(), 3 * defaultReadTimeoutMS))
                        QFAIL(msgDoSocketReadFailed(serverName, port, i, it->data.size()));

                    // pop that many bytes off the socket
                    QByteArray received = socket.read(it->data.size());

                    // is it what we expected?
                    QVERIFY2(received == it->data,
                             QString("Did not receive expected data in step %1: data received was:\n%2")
                             .arg(i).arg(prettyByteArray(received)).toLocal8Bit());

                    break;
                }

            case Chat::DiscardUntil:
                qDebug() << i << "Discarding until" << prettyByteArray(it->data);
                while (true) {
                    // scan the buffer until we have our string
                    if (!doSocketRead(&socket, it->data.size()))
                        QFAIL(msgDoSocketReadFailed(serverName, port, i, it->data.size()));

                    QByteArray buffer;
                    buffer.resize(socket.bytesAvailable());
                    socket.peek(buffer.data(), socket.bytesAvailable());

                    int pos = buffer.indexOf(it->data);
                    if (pos == -1) {
                        // data not found, keep trying
                        continue;
                    }

                    buffer = socket.read(pos + it->data.size());
                    qDebug() << i << "Discarded" << prettyByteArray(buffer);
                    break;
                }
                break;

            case Chat::SkipBytes: {
                    qDebug() << i << "Skipping" << it->value << "bytes";
                    if (!doSocketRead(&socket, it->value))
                        QFAIL(msgDoSocketReadFailed(serverName, port, i, it->value));

                    // now discard the bytes
                    QByteArray buffer = socket.read(it->value);
                    qDebug() << i << "Skipped" << prettyByteArray(buffer);
                    break;
                }

            case Chat::Send: {
                    qDebug() << i << "Sending" << prettyByteArray(it->data);
                    socket.write(it->data);
                    if (!doSocketFlush(&socket)) {
                        QVERIFY2(socket.state() == QAbstractSocket::ConnectedState,
                                 QString("Socket disconnected while sending data in step %1").arg(i).toLocal8Bit());
                        QFAIL(QString("Failed to send data in step %1: timeout").arg(i).toLocal8Bit());
                    }
                    break;
                }

            case Chat::Disconnect:
                qDebug() << i << "Disconnecting from host";
                socket.disconnectFromHost();

                // is this the last command?
                if (it + 1 != chat.constEnd())
                    break;

                Q_FALLTHROUGH();
            case Chat::RemoteDisconnect:
            case Chat::DiscardUntilDisconnect:
                qDebug() << i << "Waiting for remote disconnect";
                if (socket.state() != QAbstractSocket::UnconnectedState)
                    socket.waitForDisconnected(10000);
                QVERIFY2(socket.state() == QAbstractSocket::UnconnectedState,
                         QString("Socket did not disconnect as expected in step %1").arg(i).toLocal8Bit());

                // any data left?
                if (it->type == Chat::DiscardUntilDisconnect) {
                    QByteArray buffer = socket.readAll();
                    qDebug() << i << "Discarded in the process:" << prettyByteArray(buffer);
                }

                if (socket.bytesAvailable() != 0)
                    QFAIL(QString("Unexpected bytes still on buffer when disconnecting in step %1:\n%2")
                          .arg(i).arg(prettyByteArray(socket.readAll())).toLocal8Bit());
                break;

            case Chat::Reconnect:
                qDebug() << i << "Reconnecting to server on port" << port;
                socket.connectToHost(serverName, port);
                QVERIFY2(socket.waitForConnected(10000),
                         QString("Failed to reconnect to server in step %1: %2").arg(i).arg(socket.errorString()).toLocal8Bit());
                break;

            case Chat::StartEncryption:
#ifdef QT_NO_SSL
                QFAIL("Internal error: SSL required for this test");
#else
                qDebug() << i << "Starting client encryption";
                socket.ignoreSslErrors();
                socket.startClientEncryption();
                QVERIFY2(socket.waitForEncrypted(5000),
                         QString("Failed to start client encryption in step %1: %2").arg(i)
                         .arg(socket.errorString()).toLocal8Bit());
                break;
#endif
            }
    }
}

tst_NetworkSelfTest::tst_NetworkSelfTest()
{
#ifndef QT_NO_OPENSSL
    qt_ForceTlsSecurityLevel();
#endif
}

tst_NetworkSelfTest::~tst_NetworkSelfTest()
{
}

QHostAddress tst_NetworkSelfTest::serverIpAddress()
{
    if (cachedIpAddress.protocol() == QAbstractSocket::UnknownNetworkLayerProtocol) {
        // need resolving
        QHostInfo resolved = QHostInfo::fromName(QtNetworkSettings::httpServerName());
        if(resolved.error() != QHostInfo::NoError ||
            resolved.addresses().isEmpty()) {
            qWarning("QHostInfo::fromName failed (%d).", resolved.error());
            return QHostAddress(QHostAddress::Null);
        }
        cachedIpAddress = resolved.addresses().first();
    }
    return cachedIpAddress;
}

void tst_NetworkSelfTest::initTestCase()
{
#if defined(QT_TEST_SERVER)
    QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::echoServerName(), 7));
    // TODO: 'daytime' , port 13.
    QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::ftpServerName(), 21));
    const QHostInfo resolved = QHostInfo::fromName(QtNetworkSettings::ftpServerName());
    if (resolved.error() == QHostInfo::NoError && !resolved.addresses().isEmpty())
        ftpServerIpAddress = resolved.addresses().first();
    // TODO: 'ssh', port 22.
    // QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::ftpProxyServerName(), 2121));
    QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::httpServerName(), 80));
    // TODO: 'smb', port 139.
    QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::imapServerName(), 143));
    QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::httpServerName(), 443));
    QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::httpProxyServerName(), 3128));
    QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::httpProxyServerName(), 3129));
    QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::httpProxyServerName(), 3130));
    QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::socksProxyServerName(), 1080));
    QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::socksProxyServerName(), 1081));
#else
    if (!QtNetworkSettings::verifyTestNetworkSettings())
        QSKIP("No network test server available");
#endif
}

void tst_NetworkSelfTest::hostTest()
{
    // this is a localhost self-test
    QHostInfo localhost = QHostInfo::fromName("localhost");
    QCOMPARE(localhost.error(), QHostInfo::NoError);
    QVERIFY(!localhost.addresses().isEmpty());

    QTcpServer server;
    QVERIFY(server.listen());

    QTcpSocket socket;
    socket.connectToHost("127.0.0.1", server.serverPort());
    QVERIFY(socket.waitForConnected(10000));
}

void tst_NetworkSelfTest::dnsResolution_data()
{
    QTest::addColumn<QString>("hostName");
    QTest::newRow("local-name") << QtNetworkSettings::serverLocalName();
    QTest::newRow("fqdn") << QtNetworkSettings::httpServerName();
}

void tst_NetworkSelfTest::dnsResolution()
{
    QFETCH(QString, hostName);
    QHostInfo resolved = QHostInfo::fromName(hostName);
    QVERIFY2(resolved.error() == QHostInfo::NoError,
             QString("Failed to resolve hostname %1: %2").arg(hostName, resolved.errorString()).toLocal8Bit());
    QVERIFY2(resolved.addresses().size() > 0, "Got 0 addresses for server IP");

    cachedIpAddress = resolved.addresses().first();
}

void tst_NetworkSelfTest::serverReachability()
{
    // check that we get a proper error connecting to port 12346
    QTcpSocket socket;
    socket.connectToHost(QtNetworkSettings::httpServerName(), 12346);

    QElapsedTimer timer;
    timer.start();
    socket.waitForConnected(10000);
    QVERIFY2(timer.elapsed() < 9900, "Connection to closed port timed out instead of refusing, something is wrong");

    QVERIFY2(socket.state() == QAbstractSocket::UnconnectedState, "Socket connected unexpectedly!");
    QVERIFY2(socket.error() == QAbstractSocket::ConnectionRefusedError,
             QString("Could not reach server: %1").arg(socket.errorString()).toLocal8Bit());
}

void tst_NetworkSelfTest::remotePortsOpen_data()
{
#if defined(QT_TEST_SERVER)
    QSKIP("Skipping, for the docker test server already tested by initTestCase()");
#endif
    QTest::addColumn<int>("portNumber");

    QTest::newRow("echo") << 7;
    QTest::newRow("daytime") << 13;
    QTest::newRow("ftp") << 21;
    QTest::newRow("ssh") << 22;
    QTest::newRow("imap") << 143;
    QTest::newRow("http") << 80;
    QTest::newRow("https") << 443;
    QTest::newRow("http-proxy") << 3128;
    QTest::newRow("http-proxy-auth-basic") << 3129;
    QTest::newRow("http-proxy-auth-ntlm") << 3130;
    QTest::newRow("socks5-proxy") << 1080;
    QTest::newRow("socks5-proxy-auth") << 1081;
    QTest::newRow("ftp-proxy") << 2121;
    QTest::newRow("smb") << 139;
}

void tst_NetworkSelfTest::remotePortsOpen()
{
    QFETCH(const int, portNumber);

    QTcpSocket socket;
    socket.connectToHost(QtNetworkSettings::hostWithServiceOnPort(portNumber), quint16(portNumber));

    if (!socket.waitForConnected(10000)) {
        if (socket.error() == QAbstractSocket::SocketTimeoutError)
            QFAIL(QString("Network timeout connecting to the server on port %1").arg(portNumber).toLocal8Bit());
        else
            QFAIL(QString("Error connecting to server on port %1: %2").arg(portNumber).arg(socket.errorString()).toLocal8Bit());
    }
    QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
}

static QList<Chat> ftpChat(const QByteArray &userSuffix = QByteArray())
{
    QList<Chat> rv;
    rv << Chat::expect("220")
       << Chat::discardUntil("\r\n")
       << Chat::send("USER anonymous" + userSuffix + "\r\n")
       << Chat::expect("331")
       << Chat::discardUntil("\r\n")
       << Chat::send("PASS user@hostname\r\n")
       << Chat::expect("230")
       << Chat::discardUntil("\r\n")
       << Chat::send("CWD pub\r\n")
       << Chat::expect("250")
       << Chat::discardUntil("\r\n")
       << Chat::send("CWD dir-not-readable\r\n")
       << Chat::expect("550")
       << Chat::discardUntil("\r\n")
       << Chat::send("PWD\r\n")
#if defined(QT_TEST_SERVER)
       << Chat::expect("257 \"/pub\" is the current directory\r\n")
#else
       << Chat::expect("257 \"/pub\"\r\n")
#endif
       << Chat::send("SIZE file-not-readable.txt\r\n")
       << Chat::expect("213 41\r\n")
       << Chat::send("CWD qxmlquery\r\n")
       << Chat::expect("250")
       << Chat::discardUntil("\r\n")
       << Chat::send("CWD /qtest\r\n")
       << Chat::expect("250")
       << Chat::discardUntil("\r\n")
       << Chat::send("SIZE bigfile\r\n")
       << Chat::expect("213 519240\r\n")
       << Chat::send("SIZE rfc3252\r\n")
       << Chat::expect("213 25962\r\n")
       << Chat::send("SIZE rfc3252.txt\r\n")
       << Chat::expect("213 25962\r\n")
       << Chat::send("QUIT\r\n");

    rv << Chat::expect("221")
       << Chat::discardUntil("\r\n");

    rv << Chat::RemoteDisconnect;
    return rv;
}

void tst_NetworkSelfTest::ftpServer()
{
    netChat(21, ftpChat());
}

void tst_NetworkSelfTest::ftpProxyServer()
{
    QSKIP("FTP not currently supported.");
    netChat(2121, ftpChat("@" + QtNetworkSettings::ftpServerName().toLatin1()));
}

void tst_NetworkSelfTest::imapServer()
{
    netChat(143, QList<Chat>()
            << Chat::expect("* OK ")
            << Chat::discardUntil("\r\n")
            << Chat::send("1 CAPABILITY\r\n")
            << Chat::expect("* CAPABILITY ")
            << Chat::discardUntil("1 OK")
            << Chat::discardUntil("\r\n")
            << Chat::send("2 LOGOUT\r\n")
            << Chat::discardUntil("2 OK")
            << Chat::discardUntil("\r\n")
            << Chat::RemoteDisconnect);
}

void tst_NetworkSelfTest::httpServer()
{
    QByteArray uniqueExtension = QByteArray::number((qulonglong)this) +
                                 QByteArray::number((qulonglong)QRandomGenerator::global()->generate()) +
                                 QByteArray::number(QDateTime::currentSecsSinceEpoch());

    netChat(80, QList<Chat>()
            // HTTP/0.9 chat:
            << Chat::send("GET /\r\n")
            << Chat::DiscardUntilDisconnect

            // HTTP/1.0 chat:
            << Chat::Reconnect
            << Chat::send("GET / HTTP/1.0\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                          "Connection: close\r\n"
                          "\r\n")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("200 ")
            << Chat::DiscardUntilDisconnect

            // HTTP/1.0 POST:
            << Chat::Reconnect
            << Chat::send("POST / HTTP/1.0\r\n"
                          "Content-Length: 5\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                          "Connection: close\r\n"
                          "\r\n"
                          "Hello")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("200 ")
            << Chat::DiscardUntilDisconnect

            // HTTP protected area
            << Chat::Reconnect
            << Chat::send("GET /qtest/protected/rfc3252.txt HTTP/1.0\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                          "Connection: close\r\n"
                          "\r\n")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("401 ")
            << Chat::DiscardUntilDisconnect

            << Chat::Reconnect
            << Chat::send("HEAD /qtest/protected/rfc3252.txt HTTP/1.0\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                          "Connection: close\r\n"
                          "Authorization: Basic cXNvY2tzdGVzdDpwYXNzd29yZA==\r\n"
                          "\r\n")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("200 ")
            << Chat::DiscardUntilDisconnect

            // DAV area
            << Chat::Reconnect
            << Chat::send("HEAD /dav/ HTTP/1.0\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                          "Connection: close\r\n"
                          "\r\n")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("200 ")
            << Chat::DiscardUntilDisconnect

            // HTTP/1.0 PUT
            << Chat::Reconnect
            << Chat::send("PUT /dav/networkselftest-" + uniqueExtension + ".txt HTTP/1.0\r\n"
                          "Content-Length: 5\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                          "Connection: close\r\n"
                          "\r\n"
                          "Hello")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("201 ")
            << Chat::DiscardUntilDisconnect

            // check that the file did get uploaded
            << Chat::Reconnect
            << Chat::send("HEAD /dav/networkselftest-" + uniqueExtension + ".txt HTTP/1.0\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                          "Connection: close\r\n"
                          "\r\n")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("200 ")
            << Chat::discardUntil("\r\nContent-Length: 5\r\n")
            << Chat::DiscardUntilDisconnect

            // HTTP/1.0 DELETE
            << Chat::Reconnect
            << Chat::send("DELETE /dav/networkselftest-" + uniqueExtension + ".txt HTTP/1.0\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                          "Connection: close\r\n"
                          "\r\n")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("204 ")
            << Chat::DiscardUntilDisconnect
            );
}

void tst_NetworkSelfTest::httpServerFiles_data()
{
    QTest::addColumn<QString>("uri");
    QTest::addColumn<int>("size");

    QTest::newRow("fluke.gif") << "/qtest/fluke.gif" << -1;
    QTest::newRow("bigfile") << "/qtest/bigfile" << 519240;
    QTest::newRow("rfc3252.txt") << "/qtest/rfc3252.txt" << 25962;
    QTest::newRow("protected/rfc3252.txt") << "/qtest/protected/rfc3252.txt" << 25962;
    QTest::newRow("completelyEmptyQuery.xq") << "/qtest/qxmlquery/completelyEmptyQuery.xq" << -1;
    QTest::newRow("notWellformedViaHttps.xml") << "/qtest/qxmlquery/notWellformedViaHttps.xml" << -1;
    QTest::newRow("notWellformed.xml") << "/qtest/qxmlquery/notWellformed.xml" << -1;
    QTest::newRow("viaHttp.xq") << "/qtest/qxmlquery/viaHttp.xq" << -1;
    QTest::newRow("wellFormedViaHttps.xml") << "/qtest/qxmlquery/wellFormedViaHttps.xml" << -1;
    QTest::newRow("wellFormed.xml") << "/qtest/qxmlquery/wellFormed.xml" << -1;
}

void tst_NetworkSelfTest::httpServerFiles()
{
    QFETCH(QString, uri);
    QFETCH(int, size);
    QUrl url(uri);

    QList<Chat> chat;
    chat << Chat::send("HEAD " + url.toEncoded() + " HTTP/1.0\r\n"
                       "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                       "Connection: close\r\n"
                       "Authorization: Basic cXNvY2tzdGVzdDpwYXNzd29yZA==\r\n"
                       "\r\n")
         << Chat::expect("HTTP/1.")
         << Chat::skipBytes(1) // HTTP/1.0 or 1.1 reply
         << Chat::expect(" 200 ");
    if (size != -1)
        chat << Chat::discardUntil("\r\nContent-Length: " + QByteArray::number(size) + "\r\n");
    chat << Chat::DiscardUntilDisconnect;
    netChat(80, chat);
}

void tst_NetworkSelfTest::httpServerCGI_data()
{
    QTest::addColumn<QByteArray>("request");
    QTest::addColumn<QByteArray>("result");
    QTest::addColumn<QByteArray>("additionalHeader");

    QTest::newRow("echo.cgi")
            << QByteArray("GET /qtest/cgi-bin/echo.cgi?Hello+World HTTP/1.0\r\n"
                          "Connection: close\r\n"
                          "\r\n")
            << QByteArray("Hello+World")
            << QByteArray();

    QTest::newRow("echo.cgi(POST)")
            << QByteArray("POST /qtest/cgi-bin/echo.cgi?Hello+World HTTP/1.0\r\n"
                          "Connection: close\r\n"
                          "Content-Length: 15\r\n"
                          "\r\n"
                          "Hello, World!\r\n")
            << QByteArray("Hello, World!\r\n")
            << QByteArray();

    QTest::newRow("md5sum.cgi")
            << QByteArray("POST /qtest/cgi-bin/md5sum.cgi HTTP/1.0\r\n"
                          "Connection: close\r\n"
                          "Content-Length: 15\r\n"
                          "\r\n"
                          "Hello, World!\r\n")
            << QByteArray("29b933a8d9a0fcef0af75f1713f4940e\n")
            << QByteArray();

    QTest::newRow("protected/md5sum.cgi")
            << QByteArray("POST /qtest/protected/cgi-bin/md5sum.cgi HTTP/1.0\r\n"
                          "Connection: close\r\n"
                          "Authorization: Basic cXNvY2tzdGVzdDpwYXNzd29yZA==\r\n"
                          "Content-Length: 15\r\n"
                          "\r\n"
                          "Hello, World!\r\n")
            << QByteArray("29b933a8d9a0fcef0af75f1713f4940e\n")
            << QByteArray();

    QTest::newRow("set-cookie.cgi")
            << QByteArray("POST /qtest/cgi-bin/set-cookie.cgi HTTP/1.0\r\n"
                          "Connection: close\r\n"
                          "Content-Length: 8\r\n"
                          "\r\n"
                          "foo=bar\n")
            << QByteArray("Success\n")
            << QByteArray("\r\nSet-Cookie: foo=bar\r\n");
}

void tst_NetworkSelfTest::httpServerCGI()
{
    QFETCH(QByteArray, request);
    QFETCH(QByteArray, result);
    QFETCH(QByteArray, additionalHeader);
    QList<Chat> chat;
    chat << Chat::send(request)
         << Chat::expect("HTTP/1.") << Chat::skipBytes(1)
         << Chat::expect(" 200 ");

    if (!additionalHeader.isEmpty())
        chat << Chat::discardUntil(additionalHeader);

    chat << Chat::discardUntil("\r\n\r\n")
         << Chat::expect(result)
         << Chat::RemoteDisconnect;
    netChat(80, chat);
}

#ifndef QT_NO_SSL
void tst_NetworkSelfTest::httpsServer()
{
    netChat(443, QList<Chat>()
            << Chat::StartEncryption
            << Chat::send("GET / HTTP/1.0\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                          "Connection: close\r\n"
                          "\r\n")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("200 ")
            << Chat::DiscardUntilDisconnect);
}
#endif

void tst_NetworkSelfTest::httpProxy()
{
    netChat(3128, QList<Chat>()
            // proxy GET by IP
            << Chat::send("GET http://" + serverIpAddress().toString().toLatin1() + "/ HTTP/1.0\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                          "Proxy-connection: close\r\n"
                          "\r\n")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("200 ")
            << Chat::DiscardUntilDisconnect

            // proxy GET by hostname
            << Chat::Reconnect
            << Chat::send("GET http://" + QtNetworkSettings::httpServerName().toLatin1() + "/ HTTP/1.0\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                          "Proxy-connection: close\r\n"
                          "\r\n")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("200 ")
            << Chat::DiscardUntilDisconnect

            // proxy CONNECT by IP
            << Chat::Reconnect
#if !defined(QT_TEST_SERVER)
            << Chat::send("CONNECT " + serverIpAddress().toString().toLatin1() + ":21 HTTP/1.0\r\n"
                          "\r\n")
#else
            << Chat::send("CONNECT " + ftpServerIpAddress.toString().toLatin1() + ":21 HTTP/1.0\r\n"
                          "\r\n")
#endif
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("200 ")
            << Chat::discardUntil("\r\n\r\n")
            << ftpChat()

            // proxy CONNECT by hostname
            << Chat::Reconnect
            << Chat::send("CONNECT " + QtNetworkSettings::ftpServerName().toLatin1() + ":21 HTTP/1.0\r\n"
                          "\r\n")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("200 ")
            << Chat::discardUntil("\r\n\r\n")
            << ftpChat()
            );
}

void tst_NetworkSelfTest::httpProxyBasicAuth()
{
    netChat(3129, QList<Chat>()
            // test auth required response
            << Chat::send("GET http://" + QtNetworkSettings::httpServerName().toLatin1() + "/ HTTP/1.0\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                          "Proxy-connection: close\r\n"
                          "\r\n")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("407 ")
            << Chat::discardUntil("\r\nProxy-Authenticate: Basic realm=\"")
            << Chat::DiscardUntilDisconnect

            // now try sending our credentials
            << Chat::Reconnect
            << Chat::send("GET http://" + QtNetworkSettings::httpServerName().toLatin1() + "/ HTTP/1.0\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
                          "Proxy-connection: close\r\n"
                          "Proxy-Authorization: Basic cXNvY2tzdGVzdDpwYXNzd29yZA==\r\n"
                          "\r\n")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("200 ")
            << Chat::DiscardUntilDisconnect);
}

void tst_NetworkSelfTest::httpProxyNtlmAuth()
{
    netChat(3130, QList<Chat>()
            // test auth required response
            << Chat::send("GET http://" + QtNetworkSettings::httpServerName().toLatin1() + "/ HTTP/1.0\r\n"
                          "Host: " + QtNetworkSettings::httpServerName().toLatin1() + "\r\n"
#if !defined(QT_TEST_SERVER)
                          "Proxy-connection: keep-alive\r\n" // NTLM auth will disconnect
#else
                          "Proxy-connection: close\r\n" // Well, what do you know? It keeps it alive!
#endif
                          "\r\n")
            << Chat::expect("HTTP/1.")
            << Chat::discardUntil(" ")
            << Chat::expect("407 ")
            << Chat::discardUntil("\r\nProxy-Authenticate: NTLM\r\n")
            << Chat::DiscardUntilDisconnect
            );
}

// SOCKSv5 is a binary protocol
static const char handshakeNoAuth[] = "\5\1\0";
static const char handshakeOkNoAuth[] = "\5\0";
static const char handshakeAuthPassword[] = "\5\1\2\1\12qsockstest\10password";
static const char handshakeOkPasswdAuth[] = "\5\2\1\0";
static const char handshakeAuthNotOk[] = "\5\377";
static const char connect1[] = "\5\1\0\1\177\0\0\1\0\25"; // Connect IPv4 127.0.0.1 port 21
static const char connect1a[] = "\5\1\0\1"; // just "Connect to IPv4"
static const char connect1b[] = "\0\25"; // just "port 21"
static const char connect2[] = "\5\1\0\3\11localhost\0\25"; // Connect hostname localhost 21
static const char connect2a[] = "\5\1\0\3"; // just "Connect to hostname"
static const char connected[] = "\5\0\0";

#define QBA(x) (QByteArray::fromRawData(x, int(sizeof(x)) - 1))

void tst_NetworkSelfTest::socks5Proxy()
{
    union {
        char buf[4];
        quint32 data;
    } ip4Address;
    ip4Address.data =
#if !defined(QT_TEST_SERVER)
    qToBigEndian(serverIpAddress().toIPv4Address());
#else
    qToBigEndian(ftpServerIpAddress.toIPv4Address());
#endif
    const QByteArray handshakeNoAuthData = QByteArray(handshakeNoAuth, int(sizeof handshakeNoAuth) - 1);
    const QByteArray handshakeOkNoAuthData = QByteArray(handshakeOkNoAuth, int(sizeof handshakeOkNoAuth) - 1);
    const QByteArray connect1Data = QByteArray(connect1, int(sizeof connect1) - 1);
    const QByteArray connectedData = QByteArray(connected, int(sizeof connected) - 1);
    const QByteArray connect2Data = QByteArray(connect2, int(sizeof connect2) - 1);

    netChat(1080, QList<Chat>()
            // IP address connection
#if !defined(QT_TEST_SERVER)
            // This test relies on the proxy and ftp servers
            // running on the same machine, which is not the
            // case for the docker test server.
            << Chat::send(handshakeNoAuthData)
            << Chat::expect(handshakeOkNoAuthData)
            << Chat::send(connect1Data)
            << Chat::expect(connectedData)
            << Chat::expect("\1") // IPv4 address following
            << Chat::skipBytes(6) // the server's local address and port
            << ftpChat()

            // connect by IP
            << Chat::Reconnect
#endif
            << Chat::send(handshakeNoAuthData)
            << Chat::expect(handshakeOkNoAuthData)
            << Chat::send(QBA(connect1a) + QByteArray::fromRawData(ip4Address.buf, 4) + QBA(connect1b))
            << Chat::expect(connectedData)
            << Chat::expect("\1") // IPv4 address following
            << Chat::skipBytes(6) // the server's local address and port
            << ftpChat()
#if !defined(QT_TEST_SERVER)
            // connect to "localhost" by hostname, the same as above:
            // makes no sense with the docker test server.
            << Chat::Reconnect
            << Chat::send(handshakeNoAuthData)
            << Chat::expect(handshakeOkNoAuthData)
            << Chat::send(connect2Data)
            << Chat::expect(connectedData)
            << Chat::expect("\1") // IPv4 address following
            << Chat::skipBytes(6) // the server's local address and port
            << ftpChat()
#endif
            // connect to server by its official name
            << Chat::Reconnect
            << Chat::send(handshakeNoAuthData)
            << Chat::expect(handshakeOkNoAuthData)
            << Chat::send(QBA(connect2a) + char(QtNetworkSettings::ftpServerName().size()) + QtNetworkSettings::ftpServerName().toLatin1() + QBA(connect1b))
            << Chat::expect(connectedData)
            << Chat::expect("\1") // IPv4 address following
            << Chat::skipBytes(6) // the server's local address and port
            << ftpChat()
            );
}

void tst_NetworkSelfTest::socks5ProxyAuth()
{
    const QByteArray handshakeNoAuthData = QByteArray(handshakeNoAuth, int(sizeof handshakeNoAuth) - 1);
    const QByteArray connect1Data = QByteArray(connect1, int(sizeof connect1) - 1);
    const QByteArray connectedData = QByteArray(connected, int(sizeof connected) - 1);
    const QByteArray handshakeAuthNotOkData = QByteArray(handshakeAuthNotOk, int(sizeof(handshakeAuthNotOk)) - 1);
    const QByteArray handshakeAuthPasswordData = QByteArray(handshakeAuthPassword, int(sizeof(handshakeAuthPassword)) - 1);
    const QByteArray handshakeOkPasswdAuthData = QByteArray(handshakeOkPasswdAuth, int(sizeof(handshakeOkPasswdAuth)) - 1);

    netChat(1081, QList<Chat>()
            // unauthenticated connect -- will get error
            << Chat::send(handshakeNoAuthData)
            << Chat::expect(handshakeAuthNotOkData)
            << Chat::RemoteDisconnect
#if !defined(QT_TEST_SERVER)
            // now try to connect with authentication,
            // danted is just that, socks 5 proxy and
            // does not have ftp running, skip!
            << Chat::Reconnect
            << Chat::send(handshakeAuthPasswordData)
            << Chat::expect(handshakeOkPasswdAuthData)
            << Chat::send(connect1Data)
            << Chat::expect(connectedData)
            << Chat::expect("\1") // IPv4 address following
            << Chat::skipBytes(6) // the server's local address and port
            << ftpChat()
#else
            << Chat::Reconnect
            << Chat::send(handshakeAuthPasswordData)
            << Chat::expect(handshakeOkPasswdAuthData)
            << Chat::send(QBA(connect2a) + char(QtNetworkSettings::ftpServerName().size()) + QtNetworkSettings::ftpServerName().toLatin1() + QBA(connect1b))
            << Chat::expect(connectedData)
            << Chat::expect("\1") // IPv4 address following
            << Chat::skipBytes(6) // the server's local address and port
            << ftpChat()
#endif
            );
}

void tst_NetworkSelfTest::supportsSsl()
{
#ifdef QT_NO_SSL
    QSKIP("SSL not compiled in");
#else
    QVERIFY2(QSslSocket::supportsSsl(), "Could not load SSL libraries");
#endif
}

#if QT_CONFIG(process)
static const QByteArray msgProcessError(const QProcess &process, const char *what)
{
    QString result;
    QTextStream(&result) << what << ": \"" << process.program() << ' '
        << process.arguments().join(QLatin1Char(' ')) << "\": " << process.errorString();
    return result.toLocal8Bit();
}

static void ensureTermination(QProcess &process)
{
    if (process.state() == QProcess::Running) {
        process.terminate();
        if (!process.waitForFinished(300))
            process.kill();
    }
}
#endif // QT_CONFIG(process)

void tst_NetworkSelfTest::smbServer()
{
#if defined(QT_TEST_SERVER)
    QSKIP("Not supported by the docker test server");
#endif // QT_TEST_SERVER
    static const char contents[] = "This is 34 bytes. Do not change...";
#ifdef Q_OS_WIN
    // use Windows's native UNC support to try and open a file on the server
    QByteArray filepath = "\\\\" + QtNetworkSettings::winServerName().toLatin1() + "\\testshare\\test.pri";
    FILE *f = fopen(filepath.constData(), "rb");
    QVERIFY2(f, qt_error_string().toLocal8Bit());

    char buf[128];
    size_t ret = fread(buf, 1, sizeof buf, f);
    fclose(f);

    QCOMPARE(ret, strlen(contents));
    QVERIFY(memcmp(buf, contents, strlen(contents)) == 0);
#else
#if QT_CONFIG(process)
    enum { sambaTimeOutSecs = 5 };
    // try to use Samba
    const QString progname = "smbclient";
    const QString binary = QStandardPaths::findExecutable(progname);
    if (binary.isEmpty())
        QSKIP("Could not find smbclient (from Samba), cannot continue testing");

    // try listing the server
    const QStringList timeOutArguments = QStringList()
        << "--timeout" << QString::number(sambaTimeOutSecs);
    QStringList arguments = timeOutArguments;
    arguments << "-g" << "-N" << "-L" << QtNetworkSettings::winServerName();
    QProcess smbclient;
    smbclient.start(binary, arguments, QIODevice::ReadOnly);
    QVERIFY2(smbclient.waitForStarted(), msgProcessError(smbclient, "Unable to start"));
    const bool listFinished = smbclient.waitForFinished((1 + sambaTimeOutSecs) * 1000);
    ensureTermination(smbclient);
    QVERIFY2(listFinished, msgProcessError(smbclient, "Listing servers timed out"));
    if (smbclient.exitStatus() != QProcess::NormalExit)
        QSKIP("smbclient crashed");
    QVERIFY2(smbclient.exitCode() == 0, "Test server not found");

    QByteArray output = smbclient.readAll();
    QVERIFY(output.contains("Disk|testshare|"));
    QVERIFY(output.contains("Disk|testsharewritable|"));
    QVERIFY(output.contains("Disk|testsharelargefile|"));
    qDebug() << "Test server found and shares are correct";

    // try getting a file
    QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
    env.insert("PAGER", "/bin/cat"); // just in case
    smbclient.setProcessEnvironment(env);
    arguments = timeOutArguments;
    arguments << "-N" << "-c" << "more test.pri"
        << ("\\\\" + QtNetworkSettings::winServerName() + "\\testshare");
    smbclient.start(binary, arguments, QIODevice::ReadOnly);
    const bool fileFinished = smbclient.waitForFinished((1 + sambaTimeOutSecs) * 1000);
    ensureTermination(smbclient);
    QVERIFY2(fileFinished, msgProcessError(smbclient, "Timed out"));
    if (smbclient.exitStatus() != QProcess::NormalExit)
        QSKIP("smbclient crashed");
    QVERIFY2(smbclient.exitCode() == 0, "File //qt-test-server/testshare/test.pri not found");

    output = smbclient.readAll();
    QCOMPARE(output.constData(), contents);
    qDebug("Test file is correct");
#else
    QSKIP("No QProcess support");
#endif
#endif
}

QTEST_MAIN(tst_NetworkSelfTest)
#include "tst_networkselftest.moc"
