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

#include "qsocks5socketengine_p.h"

#include "qtcpsocket.h"
#include "qudpsocket.h"
#include "qtcpserver.h"
#include "qdebug.h"
#include "qhash.h"
#include "qqueue.h"
#include "qdeadlinetimer.h"
#include "qelapsedtimer.h"
#include "qmutex.h"
#include "qthread.h"
#include "qcoreapplication.h"
#include "qurl.h"
#include "qauthenticator.h"
#include "private/qiodevice_p.h"
#include "private/qringbuffer_p.h"
#include <qendian.h>
#include <qnetworkinterface.h>

#include <QtCore/qpointer.h>

#include <memory>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;
using namespace std::chrono_literals;

static const int MaxWriteBufferSize = 128*1024;

//#define QSOCKS5SOCKETLAYER_DEBUG

#define MAX_DATA_DUMP 256
static constexpr auto Socks5BlockingBindTimeout = 5s;

#define Q_INIT_CHECK(returnValue) do { \
    if (!d->data) { \
        return returnValue; \
    } } while (0)

#define S5_VERSION_5 0x05
#define S5_CONNECT 0x01
#define S5_BIND 0x02
#define S5_UDP_ASSOCIATE 0x03
#define S5_IP_V4 0x01
#define S5_DOMAINNAME 0x03
#define S5_IP_V6 0x04
#define S5_SUCCESS 0x00
#define S5_R_ERROR_SOCKS_FAILURE 0x01
#define S5_R_ERROR_CON_NOT_ALLOWED 0x02
#define S5_R_ERROR_NET_UNREACH 0x03
#define S5_R_ERROR_HOST_UNREACH 0x04
#define S5_R_ERROR_CONN_REFUSED 0x05
#define S5_R_ERROR_TTL 0x06
#define S5_R_ERROR_CMD_NOT_SUPPORTED 0x07
#define S5_R_ERROR_ADD_TYPE_NOT_SUPORTED 0x08

#define S5_AUTHMETHOD_NONE 0x00
#define S5_AUTHMETHOD_PASSWORD 0x02
#define S5_AUTHMETHOD_NOTACCEPTABLE 0xFF

#define S5_PASSWORDAUTH_VERSION 0x01

#ifdef QSOCKS5SOCKETLAYER_DEBUG
#  define QSOCKS5_Q_DEBUG qDebug() << this
#  define QSOCKS5_D_DEBUG qDebug() << q_ptr
#  define QSOCKS5_DEBUG qDebug() << "[QSocks5]"
static QString s5StateToString(QSocks5SocketEnginePrivate::Socks5State s)
{
    switch (s) {
    case QSocks5SocketEnginePrivate::Uninitialized: return "Uninitialized"_L1;
    case QSocks5SocketEnginePrivate::ConnectError: return "ConnectError"_L1;
    case QSocks5SocketEnginePrivate::AuthenticationMethodsSent: return "AuthenticationMethodsSent"_L1;
    case QSocks5SocketEnginePrivate::Authenticating: return "Authenticating"_L1;
    case QSocks5SocketEnginePrivate::AuthenticatingError: return "AuthenticatingError"_L1;
    case QSocks5SocketEnginePrivate::RequestMethodSent: return "RequestMethodSent"_L1;
    case QSocks5SocketEnginePrivate::RequestError: return "RequestError"_L1;
    case QSocks5SocketEnginePrivate::Connected: return "Connected"_L1;
    case QSocks5SocketEnginePrivate::UdpAssociateSuccess: return "UdpAssociateSuccess"_L1;
    case QSocks5SocketEnginePrivate::BindSuccess: return "BindSuccess"_L1;
    case QSocks5SocketEnginePrivate::ControlSocketError: return "ControlSocketError"_L1;
    case QSocks5SocketEnginePrivate::SocksError: return "SocksError"_L1;
    case QSocks5SocketEnginePrivate::HostNameLookupError: return "HostNameLookupError"_L1;
    default: break;
    }
    return "unknown state"_L1;
}

static QString dump(const QByteArray &buf)
{
    QString data;
    for (int i = 0; i < qMin<int>(MAX_DATA_DUMP, buf.size()); ++i) {
        if (i) data += u' ';
        uint val = (unsigned char)buf.at(i);
       // data += QString("0x%1").arg(val, 3, 16, u'0');
        data += QString::number(val);
    }
    if (buf.size() > MAX_DATA_DUMP)
        data += " ..."_L1;

    return QString::fromLatin1("size: %1 data: { %2 }").arg(buf.size()).arg(data);
}

#else
#  define QSOCKS5_DEBUG if (0) qDebug()
#  define QSOCKS5_Q_DEBUG if (0) qDebug()
#  define QSOCKS5_D_DEBUG if (0) qDebug()

static inline QString s5StateToString(QSocks5SocketEnginePrivate::Socks5State) { return QString(); }
static inline QString dump(const QByteArray &) { return QString(); }
#endif

/*
   inserts the host address in buf at pos and updates pos.
   if the func fails the data in buf and the value of pos is undefined
*/
static bool qt_socks5_set_host_address_and_port(const QHostAddress &address, quint16 port, QByteArray *pBuf)
{
    QSOCKS5_DEBUG << "setting [" << address << ':' << port << ']';

    union {
        quint16 port;
        quint32 ipv4;
        QIPv6Address ipv6;
        char ptr;
    } data;

    // add address
    if (address.protocol() == QAbstractSocket::IPv4Protocol) {
        data.ipv4 = qToBigEndian<quint32>(address.toIPv4Address());
        pBuf->append(S5_IP_V4);
        pBuf->append(QByteArray::fromRawData(&data.ptr, sizeof data.ipv4));
    } else if (address.protocol() == QAbstractSocket::IPv6Protocol) {
        data.ipv6 = address.toIPv6Address();
        pBuf->append(S5_IP_V6);
        pBuf->append(QByteArray::fromRawData(&data.ptr, sizeof data.ipv6));
    } else {
        return false;
    }

    // add port
    data.port = qToBigEndian<quint16>(port);
    pBuf->append(QByteArray::fromRawData(&data.ptr, sizeof data.port));
    return true;
}

/*
   like above, but for a hostname
*/
static bool qt_socks5_set_host_name_and_port(const QString &hostname, quint16 port, QByteArray *pBuf)
{
    QSOCKS5_DEBUG << "setting [" << hostname << ':' << port << ']';

    QByteArray encodedHostName = QUrl::toAce(hostname);
    QByteArray &buf = *pBuf;

    if (encodedHostName.size() > 255)
        return false;

    buf.append(S5_DOMAINNAME);
    buf.append(uchar(encodedHostName.size()));
    buf.append(encodedHostName);

    // add port
    union {
        quint16 port;
        char ptr;
    } data;
    data.port = qToBigEndian<quint16>(port);
    buf.append(QByteArray::fromRawData(&data.ptr, sizeof data.port));

    return true;
}


/*
   retrieves the host address in buf at pos and updates pos.
   return 1 if OK, 0 if need more data, -1 if error
   if the func fails the value of the address and the pos is undefined
*/
static int qt_socks5_get_host_address_and_port(const QByteArray &buf, QHostAddress *pAddress, quint16 *pPort, int *pPos)
{
    int ret = -1;
    int pos = *pPos;
    const unsigned char *pBuf = reinterpret_cast<const unsigned char*>(buf.constData());
    QHostAddress address;
    quint16 port = 0;

    if (buf.size() - pos < 1) {
        QSOCKS5_DEBUG << "need more data address/port";
        return 0;
    }
    if (pBuf[pos] == S5_IP_V4) {
        pos++;
        if (buf.size() - pos < 4) {
            QSOCKS5_DEBUG << "need more data for ip4 address";
            return 0;
        }
        address.setAddress(qFromBigEndian<quint32>(&pBuf[pos]));
        pos += 4;
        ret = 1;
    } else if (pBuf[pos] == S5_IP_V6) {
        pos++;
        if (buf.size() - pos < 16) {
            QSOCKS5_DEBUG << "need more data for ip6 address";
            return 0;
        }
        QIPv6Address add;
        for (int i = 0; i < 16; ++i)
            add[i] = buf[pos++];
        address.setAddress(add);
        ret = 1;
    } else if (pBuf[pos] == S5_DOMAINNAME){
        // just skip it
        pos++;
        qDebug() << "skipping hostname of len" << uint(pBuf[pos]);
        pos += uchar(pBuf[pos]);
    } else {
        QSOCKS5_DEBUG << "invalid address type" << (int)pBuf[pos];
        ret = -1;
    }

    if (ret == 1) {
        if (buf.size() - pos < 2) {
            QSOCKS5_DEBUG << "need more data for port";
            return 0;
        }
        port = qFromBigEndian<quint16>(&pBuf[pos]);
        pos += 2;
    }

    if (ret == 1) {
        QSOCKS5_DEBUG << "got [" << address << ':' << port << ']';
        *pAddress = address;
        *pPort = port;
        *pPos = pos;
    }

    return ret;
}

struct QSocks5Data
{
    QTcpSocket *controlSocket;
    QSocks5Authenticator *authenticator;
};

struct QSocks5ConnectData : public QSocks5Data
{
    QRingBuffer readBuffer;
};

struct QSocks5BindData : public QSocks5Data
{
    QHostAddress localAddress;
    quint16 localPort = 0;
    QHostAddress peerAddress;
    quint16 peerPort = 0;
    QElapsedTimer timeStamp;
};

struct QSocks5RevivedDatagram
{
    QByteArray data;
    QHostAddress address;
    quint16 port = 0;
};

#ifndef QT_NO_UDPSOCKET
struct QSocks5UdpAssociateData : public QSocks5Data
{
    QUdpSocket *udpSocket = nullptr;
    QHostAddress associateAddress;
    quint16 associatePort = 0;
    QQueue<QSocks5RevivedDatagram> pendingDatagrams;
};
#endif

// needs to be thread safe
class QSocks5BindStore : public QObject
{
public:
    QSocks5BindStore();
    ~QSocks5BindStore();

    void add(qintptr socketDescriptor, QSocks5BindData *bindData);
    bool contains(qintptr socketDescriptor);
    QSocks5BindData *retrieve(qintptr socketDescriptor);

protected:
    void timerEvent(QTimerEvent * event) override;

    QRecursiveMutex mutex;
    int sweepTimerId = -1;
    //socket descriptor, data, timestamp
    QHash<qintptr, QSocks5BindData *> store;
};

Q_GLOBAL_STATIC(QSocks5BindStore, socks5BindStore)

QSocks5BindStore::QSocks5BindStore()
{
    QCoreApplication *app = QCoreApplication::instance();
    if (app && app->thread() != thread())
        moveToThread(app->thread());
}

QSocks5BindStore::~QSocks5BindStore()
{
}

void QSocks5BindStore::add(qintptr socketDescriptor, QSocks5BindData *bindData)
{
    QMutexLocker lock(&mutex);
    if (store.contains(socketDescriptor)) {
        // qDebug("delete it");
    }
    bindData->timeStamp.start();
    store.insert(socketDescriptor, bindData);

    // start sweep timer if not started
    if (sweepTimerId == -1)
        sweepTimerId = startTimer(1min);
}

bool QSocks5BindStore::contains(qintptr socketDescriptor)
{
    QMutexLocker lock(&mutex);
    return store.contains(socketDescriptor);
}

QSocks5BindData *QSocks5BindStore::retrieve(qintptr socketDescriptor)
{
    QMutexLocker lock(&mutex);
    const auto it = store.constFind(socketDescriptor);
    if (it == store.cend())
        return nullptr;
    QSocks5BindData *bindData = it.value();
    store.erase(it);
    if (bindData) {
        if (bindData->controlSocket->thread() != QThread::currentThread()) {
            qWarning("Cannot access socks5 bind data from different thread");
            return nullptr;
        }
    } else {
        QSOCKS5_DEBUG << "__ERROR__ binddata == 0";
    }
    // stop the sweep timer if not needed
    if (store.isEmpty()) {
        killTimer(sweepTimerId);
        sweepTimerId = -1;
    }
    return bindData;
}

void QSocks5BindStore::timerEvent(QTimerEvent * event)
{
    QMutexLocker lock(&mutex);
    if (event->timerId() == sweepTimerId) {
        QSOCKS5_DEBUG << "QSocks5BindStore performing sweep";
        for (auto it = store.begin(), end = store.end(); it != end;) {
            if (it.value()->timeStamp.hasExpired(350000)) {
                QSOCKS5_DEBUG << "QSocks5BindStore removing JJJJ";
                it = store.erase(it);
            } else {
                ++it;
            }
        }
    }
}

QSocks5Authenticator::QSocks5Authenticator()
{
}

QSocks5Authenticator::~QSocks5Authenticator()
{
}

char QSocks5Authenticator::methodId()
{
    return 0x00;
}

bool QSocks5Authenticator::beginAuthenticate(QTcpSocket *socket, bool *completed)
{
    Q_UNUSED(socket);
    *completed = true;
    return true;
}

bool QSocks5Authenticator::continueAuthenticate(QTcpSocket *socket, bool *completed)
{
    Q_UNUSED(socket);
    *completed = true;
    return true;
}

bool QSocks5Authenticator::seal(const QByteArray &buf, QByteArray *sealedBuf)
{
    *sealedBuf = buf;
    return true;
}

bool QSocks5Authenticator::unSeal(const QByteArray &sealedBuf, QByteArray *buf)
{
    *buf = sealedBuf;
    return true;
}

bool QSocks5Authenticator::unSeal(QTcpSocket *sealedSocket, QByteArray *buf)
{
    return unSeal(sealedSocket->readAll(), buf);
}

QSocks5PasswordAuthenticator::QSocks5PasswordAuthenticator(const QString &userName, const QString &password)
{
    this->userName = userName;
    this->password = password;
}

char QSocks5PasswordAuthenticator::methodId()
{
    return 0x02;
}

bool QSocks5PasswordAuthenticator::beginAuthenticate(QTcpSocket *socket, bool *completed)
{
    *completed = false;
    QByteArray uname = userName.toLatin1();
    QByteArray passwd = password.toLatin1();
    QByteArray dataBuf(3 + uname.size() + passwd.size(), 0);
    char *buf = dataBuf.data();
    int pos = 0;
    buf[pos++] = S5_PASSWORDAUTH_VERSION;
    buf[pos++] = uname.size();
    memcpy(&buf[pos], uname.data(), uname.size());
    pos += uname.size();
    buf[pos++] = passwd.size();
    memcpy(&buf[pos], passwd.data(), passwd.size());
    return socket->write(dataBuf) == dataBuf.size();
}

bool QSocks5PasswordAuthenticator::continueAuthenticate(QTcpSocket *socket, bool *completed)
{
    *completed = false;

    if (socket->bytesAvailable() < 2)
        return true;

    QByteArray buf = socket->read(2);
    if (buf.at(0) == S5_PASSWORDAUTH_VERSION && buf.at(1) == 0x00) {
        *completed = true;
        return true;
    }

    // must disconnect
    socket->close();
    return false;
}

QString QSocks5PasswordAuthenticator::errorString()
{
    return "Socks5 user name or password incorrect"_L1;
}



QSocks5SocketEnginePrivate::QSocks5SocketEnginePrivate()
    : socks5State(Uninitialized)
    , readNotificationEnabled(false)
    , writeNotificationEnabled(false)
    , exceptNotificationEnabled(false)
    , socketDescriptor(-1)
    , data(nullptr)
    , connectData(nullptr)
#ifndef QT_NO_UDPSOCKET
    , udpData(nullptr)
#endif
    , bindData(nullptr)
    , readNotificationActivated(false)
    , writeNotificationActivated(false)
    , readNotificationPending(false)
    , writeNotificationPending(false)
    , connectionNotificationPending(false)
{
    mode = NoMode;
}

QSocks5SocketEnginePrivate::~QSocks5SocketEnginePrivate()
{
}

void QSocks5SocketEnginePrivate::initialize(Socks5Mode socks5Mode)
{
    Q_Q(QSocks5SocketEngine);

    mode = socks5Mode;
    if (mode == ConnectMode) {
        connectData = new QSocks5ConnectData;
        data = connectData;
#ifndef QT_NO_UDPSOCKET
    } else if (mode == UdpAssociateMode) {
        udpData = new QSocks5UdpAssociateData;
        data = udpData;
        udpData->udpSocket = new QUdpSocket(q);
        udpData->udpSocket->setProxy(QNetworkProxy::NoProxy);
        QObject::connect(udpData->udpSocket, SIGNAL(readyRead()),
                         q, SLOT(_q_udpSocketReadNotification()),
                         Qt::DirectConnection);
#endif // QT_NO_UDPSOCKET
    } else if (mode == BindMode) {
        bindData = new QSocks5BindData;
        data = bindData;
    }

    data->controlSocket = new QTcpSocket(q);
    data->controlSocket->setProxy(QNetworkProxy::NoProxy);
    QObject::connect(data->controlSocket, SIGNAL(connected()), q, SLOT(_q_controlSocketConnected()),
                     Qt::DirectConnection);
    QObject::connect(data->controlSocket, SIGNAL(readyRead()), q, SLOT(_q_controlSocketReadNotification()),
                     Qt::DirectConnection);
    QObject::connect(data->controlSocket, SIGNAL(bytesWritten(qint64)), q, SLOT(_q_controlSocketBytesWritten()),
                     Qt::DirectConnection);
    QObject::connect(data->controlSocket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)),
                     q, SLOT(_q_controlSocketErrorOccurred(QAbstractSocket::SocketError)),
                     Qt::DirectConnection);
    QObject::connect(data->controlSocket, SIGNAL(disconnected()), q, SLOT(_q_controlSocketDisconnected()),
                     Qt::DirectConnection);
    QObject::connect(data->controlSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)),
                     q, SLOT(_q_controlSocketStateChanged(QAbstractSocket::SocketState)),
                     Qt::DirectConnection);

    if (!proxyInfo.user().isEmpty() || !proxyInfo.password().isEmpty()) {
        QSOCKS5_D_DEBUG << "using username/password authentication; user =" << proxyInfo.user();
        data->authenticator = new QSocks5PasswordAuthenticator(proxyInfo.user(), proxyInfo.password());
    } else {
        QSOCKS5_D_DEBUG << "not using authentication";
        data->authenticator = new QSocks5Authenticator();
    }
}

void QSocks5SocketEnginePrivate::setErrorState(Socks5State state, const QString &extraMessage)
{
    Q_Q(QSocks5SocketEngine);

    switch (state) {
    case Uninitialized:
    case Authenticating:
    case AuthenticationMethodsSent:
    case RequestMethodSent:
    case Connected:
    case UdpAssociateSuccess:
    case BindSuccess:
        // these aren't error states
        return;

    case ConnectError:
    case ControlSocketError: {
        QAbstractSocket::SocketError controlSocketError = data->controlSocket->error();
        if (socks5State != Connected) {
            switch (controlSocketError) {
            case QAbstractSocket::ConnectionRefusedError:
                q->setError(QAbstractSocket::ProxyConnectionRefusedError,
                             QSocks5SocketEngine::tr("Connection to proxy refused"));
                break;
            case QAbstractSocket::RemoteHostClosedError:
                q->setError(QAbstractSocket::ProxyConnectionClosedError,
                             QSocks5SocketEngine::tr("Connection to proxy closed prematurely"));
                break;
            case QAbstractSocket::HostNotFoundError:
                q->setError(QAbstractSocket::ProxyNotFoundError,
                             QSocks5SocketEngine::tr("Proxy host not found"));
                break;
            case QAbstractSocket::SocketTimeoutError:
                if (state == ConnectError) {
                    q->setError(QAbstractSocket::ProxyConnectionTimeoutError,
                                 QSocks5SocketEngine::tr("Connection to proxy timed out"));
                    break;
                }
                Q_FALLTHROUGH();
            default:
                q->setError(controlSocketError, data->controlSocket->errorString());
                break;
            }
        } else {
            q->setError(controlSocketError, data->controlSocket->errorString());
        }
        break;
    }

    case AuthenticatingError:
        q->setError(QAbstractSocket::ProxyAuthenticationRequiredError,
                    extraMessage.isEmpty() ?
                     QSocks5SocketEngine::tr("Proxy authentication failed") :
                     QSocks5SocketEngine::tr("Proxy authentication failed: %1").arg(extraMessage));
        break;

    case RequestError:
        // error code set by caller (overload)
        break;

    case SocksError:
        q->setError(QAbstractSocket::ProxyProtocolError,
                     QSocks5SocketEngine::tr("SOCKS version 5 protocol error"));
        break;

    case HostNameLookupError:
        q->setError(QAbstractSocket::HostNotFoundError,
                    QAbstractSocket::tr("Host not found"));
        break;
    }

    q->setState(QAbstractSocket::UnconnectedState);
    socks5State = state;
}

void QSocks5SocketEnginePrivate::setErrorState(Socks5State state, Socks5Error socks5error)
{
    Q_Q(QSocks5SocketEngine);
    switch (socks5error) {
    case SocksFailure:
        q->setError(QAbstractSocket::NetworkError,
                     QSocks5SocketEngine::tr("General SOCKSv5 server failure"));
        break;
    case ConnectionNotAllowed:
        q->setError(QAbstractSocket::SocketAccessError,
                     QSocks5SocketEngine::tr("Connection not allowed by SOCKSv5 server"));
        break;
    case NetworkUnreachable:
        q->setError(QAbstractSocket::NetworkError,
                    QAbstractSocket::tr("Network unreachable"));
        break;
    case HostUnreachable:
        q->setError(QAbstractSocket::HostNotFoundError,
                    QAbstractSocket::tr("Host not found"));
        break;
    case ConnectionRefused:
        q->setError(QAbstractSocket::ConnectionRefusedError,
                    QAbstractSocket::tr("Connection refused"));
        break;
    case TTLExpired:
        q->setError(QAbstractSocket::NetworkError,
                     QSocks5SocketEngine::tr("TTL expired"));
        break;
    case CommandNotSupported:
        q->setError(QAbstractSocket::UnsupportedSocketOperationError,
                     QSocks5SocketEngine::tr("SOCKSv5 command not supported"));
        break;
    case AddressTypeNotSupported:
        q->setError(QAbstractSocket::UnsupportedSocketOperationError,
                     QSocks5SocketEngine::tr("Address type not supported"));
        break;

    default:
        q->setError(QAbstractSocket::UnknownSocketError,
                     QSocks5SocketEngine::tr("Unknown SOCKSv5 proxy error code 0x%1").arg(int(socks5error), 16));
        break;
    }

    setErrorState(state, QString());
}

void QSocks5SocketEnginePrivate::reauthenticate()
{
    Q_Q(QSocks5SocketEngine);

    // we require authentication
    QAuthenticator auth;
    q->proxyAuthenticationRequired(proxyInfo, &auth);

    if (!auth.user().isEmpty() || !auth.password().isEmpty()) {
        // we have new credentials, let's try again
        QSOCKS5_DEBUG << "authentication failure: retrying connection";
        socks5State = QSocks5SocketEnginePrivate::Uninitialized;

        delete data->authenticator;
        proxyInfo.setUser(auth.user());
        proxyInfo.setPassword(auth.password());
        data->authenticator = new QSocks5PasswordAuthenticator(proxyInfo.user(), proxyInfo.password());

        {
            const QSignalBlocker blocker(data->controlSocket);
            data->controlSocket->abort();
        }
        data->controlSocket->connectToHost(proxyInfo.hostName(), proxyInfo.port());
    } else {
        // authentication failure

        setErrorState(AuthenticatingError);
        data->controlSocket->close();
        emitConnectionNotification();
    }
}

void QSocks5SocketEnginePrivate::parseAuthenticationMethodReply()
{
    // not enough data to begin
    if (data->controlSocket->bytesAvailable() < 2)
        return;

    QByteArray buf = data->controlSocket->read(2);
    if (buf.at(0) != S5_VERSION_5) {
        QSOCKS5_D_DEBUG << "Socks5 version incorrect";
        setErrorState(SocksError);
        data->controlSocket->close();
        emitConnectionNotification();
        return;
    }

    bool authComplete = false;
    if (uchar(buf.at(1)) == S5_AUTHMETHOD_NONE) {
        authComplete = true;
    } else if (uchar(buf.at(1)) == S5_AUTHMETHOD_NOTACCEPTABLE) {
        reauthenticate();
        return;
    } else if (buf.at(1) != data->authenticator->methodId()
               || !data->authenticator->beginAuthenticate(data->controlSocket, &authComplete)) {
        setErrorState(AuthenticatingError, "Socks5 host did not support authentication method."_L1);
        socketError = QAbstractSocket::SocketAccessError; // change the socket error
        emitConnectionNotification();
        return;
    }

    if (authComplete)
        sendRequestMethod();
    else
        socks5State = Authenticating;
}

void QSocks5SocketEnginePrivate::parseAuthenticatingReply()
{
    bool authComplete = false;
    if (!data->authenticator->continueAuthenticate(data->controlSocket, &authComplete)) {
        reauthenticate();
        return;
    }
    if (authComplete)
        sendRequestMethod();
}

void QSocks5SocketEnginePrivate::sendRequestMethod()
{
    QHostAddress address;
    quint16 port = 0;
    char command = 0;
    if (mode == ConnectMode) {
        command = S5_CONNECT;
        address = peerAddress;
        port = peerPort;
    } else if (mode == BindMode) {
        command = S5_BIND;
        address = localAddress;
        port = localPort;
    } else {
#ifndef QT_NO_UDPSOCKET
        command = S5_UDP_ASSOCIATE;
        address = localAddress; //data->controlSocket->localAddress();
        port = localPort;
#endif
    }

    QByteArray buf;
    buf.reserve(270); // big enough for domain name;
    buf.append(char(S5_VERSION_5));
    buf.append(command);
    buf.append('\0');
    if (peerName.isEmpty() && !qt_socks5_set_host_address_and_port(address, port, &buf)) {
        QSOCKS5_DEBUG << "error setting address" << address << " : " << port;
        //### set error code ....
        return;
    } else if (!peerName.isEmpty() && !qt_socks5_set_host_name_and_port(peerName, port, &buf)) {
        QSOCKS5_DEBUG << "error setting peer name" << peerName << " : " << port;
        //### set error code ....
        return;
    }
    QSOCKS5_DEBUG << "sending" << dump(buf);
    QByteArray sealedBuf;
    if (!data->authenticator->seal(buf, &sealedBuf)) {
        // ### Handle this error.
    }
    data->controlSocket->write(sealedBuf);
    data->controlSocket->flush();
    socks5State = RequestMethodSent;
}

void QSocks5SocketEnginePrivate::parseRequestMethodReply()
{
    Q_Q(QSocks5SocketEngine);
    QSOCKS5_DEBUG << "parseRequestMethodReply()";

    QByteArray inBuf;
    if (!data->authenticator->unSeal(data->controlSocket, &inBuf)) {
        // ### check error and not just not enough data
        QSOCKS5_DEBUG << "unSeal failed, needs more data";
        return;
    }

    inBuf.prepend(receivedHeaderFragment);
    receivedHeaderFragment.clear();
    QSOCKS5_DEBUG << dump(inBuf);
    if (inBuf.size() < 3) {
        QSOCKS5_DEBUG << "need more data for request reply header .. put this data somewhere";
        receivedHeaderFragment = inBuf;
        return;
    }

    QHostAddress address;
    quint16 port = 0;

    if (inBuf.at(0) != S5_VERSION_5 || inBuf.at(2) != 0x00) {
        QSOCKS5_DEBUG << "socks protocol error";
        setErrorState(SocksError);
    } else if (inBuf.at(1) != S5_SUCCESS) {
        Socks5Error socks5Error = Socks5Error(inBuf.at(1));
        QSOCKS5_DEBUG <<  "Request error :" << socks5Error;
        if ((socks5Error == SocksFailure || socks5Error == ConnectionNotAllowed)
            && !peerName.isEmpty()) {
            // Dante seems to use this error code to indicate hostname resolution failure
            setErrorState(HostNameLookupError);
        } else {
            setErrorState(RequestError, socks5Error);
        }
    } else {
        // connection success, retrieve the remote addresses
        int pos = 3;
        int err = qt_socks5_get_host_address_and_port(inBuf, &address, &port, &pos);
        if (err == -1) {
            QSOCKS5_DEBUG << "error getting address";
            setErrorState(SocksError);
        } else if (err == 0) {
            //need more data
            receivedHeaderFragment = inBuf;
            return;
        } else {
            inBuf.remove(0, pos);
            for (int i = inBuf.size() - 1; i >= 0 ; --i)
                data->controlSocket->ungetChar(inBuf.at(i));
        }
    }

    if (socks5State == RequestMethodSent) {
        // no error
        localAddress = address;
        localPort = port;

        if (mode == ConnectMode) {
            inboundStreamCount = outboundStreamCount = 1;
            socks5State = Connected;
            // notify the upper layer that we're done
            q->setState(QAbstractSocket::ConnectedState);
            emitConnectionNotification();
        } else if (mode == BindMode) {
            socks5State = BindSuccess;
            q->setState(QAbstractSocket::ListeningState);
        } else {
            socks5State = UdpAssociateSuccess;
        }
    } else if (socks5State == BindSuccess) {
        // no error and we got a connection
        bindData->peerAddress = address;
        bindData->peerPort = port;

        emitReadNotification();
    } else {
        // got an error
        data->controlSocket->close();
        emitConnectionNotification();
    }
}

void QSocks5SocketEnginePrivate::_q_emitPendingReadNotification()
{
    Q_Q(QSocks5SocketEngine);
    readNotificationPending = false;
    if (readNotificationEnabled) {
        QSOCKS5_D_DEBUG << "emitting readNotification";
        QPointer<QSocks5SocketEngine> qq = q;
        q->readNotification();
        if (!qq)
            return;
        // check if there needs to be a new zero read notification
        if (data && data->controlSocket->state() == QAbstractSocket::UnconnectedState
                && data->controlSocket->error() == QAbstractSocket::RemoteHostClosedError) {
            connectData->readBuffer.clear();
            emitReadNotification();
        }
    }
}

void QSocks5SocketEnginePrivate::emitReadNotification()
{
    Q_Q(QSocks5SocketEngine);
    readNotificationActivated = true;
    if (readNotificationEnabled && !readNotificationPending) {
        QSOCKS5_D_DEBUG << "queueing readNotification";
        readNotificationPending = true;
        QMetaObject::invokeMethod(q, "_q_emitPendingReadNotification", Qt::QueuedConnection);
    }
}

void QSocks5SocketEnginePrivate::_q_emitPendingWriteNotification()
{
    writeNotificationPending = false;
    Q_Q(QSocks5SocketEngine);
    if (writeNotificationEnabled) {
        QSOCKS5_D_DEBUG << "emitting writeNotification";
        q->writeNotification();
    }
}

void QSocks5SocketEnginePrivate::emitWriteNotification()
{
    Q_Q(QSocks5SocketEngine);
    writeNotificationActivated = true;
    if (writeNotificationEnabled && !writeNotificationPending) {
        QSOCKS5_D_DEBUG << "queueing writeNotification";
        writeNotificationPending = true;
        QMetaObject::invokeMethod(q, "_q_emitPendingWriteNotification", Qt::QueuedConnection);
    }
}

void QSocks5SocketEnginePrivate::_q_emitPendingConnectionNotification()
{
    connectionNotificationPending = false;
    Q_Q(QSocks5SocketEngine);
    QSOCKS5_D_DEBUG << "emitting connectionNotification";
    q->connectionNotification();
}

void QSocks5SocketEnginePrivate::emitConnectionNotification()
{
    Q_Q(QSocks5SocketEngine);
    QSOCKS5_D_DEBUG << "queueing connectionNotification";
    connectionNotificationPending = true;
    QMetaObject::invokeMethod(q, "_q_emitPendingConnectionNotification", Qt::QueuedConnection);
}

QSocks5SocketEngine::QSocks5SocketEngine(QObject *parent)
:QAbstractSocketEngine(*new QSocks5SocketEnginePrivate(), parent)
{
}

QSocks5SocketEngine::~QSocks5SocketEngine()
{
    Q_D(QSocks5SocketEngine);

    if (d->data) {
        delete d->data->authenticator;
        delete d->data->controlSocket;
    }
    if (d->connectData)
        delete d->connectData;
#ifndef QT_NO_UDPSOCKET
    if (d->udpData) {
        delete d->udpData->udpSocket;
        delete d->udpData;
    }
#endif
    if (d->bindData)
        delete d->bindData;
}

static int nextDescriptor()
{
    static QBasicAtomicInt counter;
    return 1 + counter.fetchAndAddRelaxed(1);
}

bool QSocks5SocketEngine::initialize(QAbstractSocket::SocketType type, QAbstractSocket::NetworkLayerProtocol protocol)
{
    Q_D(QSocks5SocketEngine);

    d->socketDescriptor = nextDescriptor();

    d->socketType = type;
    d->socketProtocol = protocol;

    return true;
}

bool QSocks5SocketEngine::initialize(qintptr socketDescriptor, QAbstractSocket::SocketState socketState)
{
    Q_D(QSocks5SocketEngine);

    QSOCKS5_Q_DEBUG << "initialize" << socketDescriptor;

    // this is only valid for the other side of a bind, nothing else is supported

    if (socketState != QAbstractSocket::ConnectedState) {
        //### must be connected state ???
        return false;
    }

    QSocks5BindData *bindData = socks5BindStore()->retrieve(socketDescriptor);
    if (bindData) {

        d->socketState = QAbstractSocket::ConnectedState;
        d->socketType = QAbstractSocket::TcpSocket;
        d->connectData = new QSocks5ConnectData;
        d->data = d->connectData;
        d->mode = QSocks5SocketEnginePrivate::ConnectMode;
        d->data->controlSocket = bindData->controlSocket;
        bindData->controlSocket = nullptr;
        d->data->controlSocket->setParent(this);
        d->socketProtocol = d->data->controlSocket->localAddress().protocol();
        d->data->authenticator = bindData->authenticator;
        bindData->authenticator = nullptr;
        d->localPort = bindData->localPort;
        d->localAddress = bindData->localAddress;
        d->peerPort = bindData->peerPort;
        d->peerAddress = bindData->peerAddress;
        d->inboundStreamCount = d->outboundStreamCount = 1;
        delete bindData;

        QObject::connect(d->data->controlSocket, SIGNAL(connected()), this, SLOT(_q_controlSocketConnected()),
                         Qt::DirectConnection);
        QObject::connect(d->data->controlSocket, SIGNAL(readyRead()), this, SLOT(_q_controlSocketReadNotification()),
                         Qt::DirectConnection);
        QObject::connect(d->data->controlSocket, SIGNAL(bytesWritten(qint64)), this, SLOT(_q_controlSocketBytesWritten()),
                         Qt::DirectConnection);
        QObject::connect(d->data->controlSocket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), this, SLOT(_q_controlSocketErrorOccurred(QAbstractSocket::SocketError)),
                         Qt::DirectConnection);
        QObject::connect(d->data->controlSocket, SIGNAL(disconnected()), this, SLOT(_q_controlSocketDisconnected()),
                         Qt::DirectConnection);
        QObject::connect(d->data->controlSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)),
                         this, SLOT(_q_controlSocketStateChanged(QAbstractSocket::SocketState)),
                         Qt::DirectConnection);

        d->socks5State = QSocks5SocketEnginePrivate::Connected;

        if (d->data->controlSocket->bytesAvailable() != 0)
            d->_q_controlSocketReadNotification();
        return true;
    }
    return false;
}

void QSocks5SocketEngine::setProxy(const QNetworkProxy &networkProxy)
{
    Q_D(QSocks5SocketEngine);
    d->proxyInfo = networkProxy;
}

qintptr QSocks5SocketEngine::socketDescriptor() const
{
    Q_D(const QSocks5SocketEngine);
    return d->socketDescriptor;
}

bool QSocks5SocketEngine::isValid() const
{
    Q_D(const QSocks5SocketEngine);
    return d->socketType != QAbstractSocket::UnknownSocketType
           && d->socks5State != QSocks5SocketEnginePrivate::SocksError
           && (d->socketError == QAbstractSocket::UnknownSocketError
               || d->socketError == QAbstractSocket::SocketTimeoutError
               || d->socketError == QAbstractSocket::UnfinishedSocketOperationError);
}

bool QSocks5SocketEngine::connectInternal()
{
    Q_D(QSocks5SocketEngine);

    if (!d->data) {
        if (socketType() == QAbstractSocket::TcpSocket) {
            d->initialize(QSocks5SocketEnginePrivate::ConnectMode);
#ifndef QT_NO_UDPSOCKET
        } else if (socketType() == QAbstractSocket::UdpSocket) {
            d->initialize(QSocks5SocketEnginePrivate::UdpAssociateMode);
            // all udp needs to be bound
            if (!bind(QHostAddress("0.0.0.0"_L1), 0))
                return false;

            setState(QAbstractSocket::ConnectedState);
            return true;
#endif
        } else {
            qFatal("QSocks5SocketEngine::connectToHost: in QTcpServer mode");
            return false;
        }
    }

    if (d->socketState != QAbstractSocket::ConnectingState) {
        if (d->socks5State == QSocks5SocketEnginePrivate::Uninitialized
            // We may have new auth credentials since an earlier failure:
         || d->socks5State == QSocks5SocketEnginePrivate::AuthenticatingError) {
            setState(QAbstractSocket::ConnectingState);
            //limit buffer in internal socket, data is buffered in the external socket under application control
            d->data->controlSocket->setReadBufferSize(65536);
        }

        d->data->controlSocket->connectToHost(d->proxyInfo.hostName(), d->proxyInfo.port());
    }

    return false;
}

bool QSocks5SocketEngine::connectToHost(const QHostAddress &address, quint16 port)
{
    Q_D(QSocks5SocketEngine);
    QSOCKS5_DEBUG << "connectToHost" << address << ':' << port;

    setPeerAddress(address);
    setPeerPort(port);
    d->peerName.clear();

    return connectInternal();
}

bool QSocks5SocketEngine::connectToHostByName(const QString &hostname, quint16 port)
{
    Q_D(QSocks5SocketEngine);

    setPeerAddress(QHostAddress());
    setPeerPort(port);
    d->peerName = hostname;

    return connectInternal();
}

void QSocks5SocketEnginePrivate::_q_controlSocketConnected()
{
    QSOCKS5_DEBUG << "_q_controlSocketConnected";
    QByteArray buf(3, 0);
    buf[0] = S5_VERSION_5;
    buf[1] = 0x01;
    buf[2] = data->authenticator->methodId();
    data->controlSocket->write(buf);
    socks5State = AuthenticationMethodsSent;
}

void QSocks5SocketEnginePrivate::_q_controlSocketReadNotification()
{
    QSOCKS5_D_DEBUG << "_q_controlSocketReadNotification socks5state" <<  s5StateToString(socks5State)
                    << "bytes available" << data->controlSocket->bytesAvailable();

    if (data->controlSocket->bytesAvailable() == 0) {
        QSOCKS5_D_DEBUG << "########## bogus read why do we get these ... on windows only";
        return;
    }

    switch (socks5State) {
        case AuthenticationMethodsSent:
            parseAuthenticationMethodReply();
            break;
        case Authenticating:
            parseAuthenticatingReply();
            break;
        case RequestMethodSent:
            parseRequestMethodReply();
            if (socks5State == Connected && data->controlSocket->bytesAvailable())
                _q_controlSocketReadNotification();
            break;
        case Connected: {
            QByteArray buf;
            if (!data->authenticator->unSeal(data->controlSocket, &buf)) {
                // qDebug("unseal error maybe need to wait for more data");
            }
            if (buf.size()) {
                QSOCKS5_DEBUG << dump(buf);
                connectData->readBuffer.append(std::move(buf));
                emitReadNotification();
            }
            break;
        }
        case BindSuccess:
            // only get here if command is bind
            if (mode == BindMode) {
                parseRequestMethodReply();
                break;
            }

            Q_FALLTHROUGH();
        default:
            qWarning("QSocks5SocketEnginePrivate::_q_controlSocketReadNotification: "
                     "Unexpectedly received data while in state=%d and mode=%d",
                     socks5State, mode);
            break;
    };
}

void QSocks5SocketEnginePrivate::_q_controlSocketBytesWritten()
{
    QSOCKS5_DEBUG << "_q_controlSocketBytesWritten";

    if (socks5State != Connected
        || (mode == ConnectMode
        && data->controlSocket->bytesToWrite()))
        return;
    if (data->controlSocket->bytesToWrite() < MaxWriteBufferSize) {
        emitWriteNotification();
        writeNotificationActivated = false;
    }
}

void QSocks5SocketEnginePrivate::_q_controlSocketErrorOccurred(QAbstractSocket::SocketError error)
{
    QSOCKS5_D_DEBUG << "controlSocketError" << error << data->controlSocket->errorString();

    if (error == QAbstractSocket::SocketTimeoutError)
        return;                 // ignore this error -- comes from the waitFor* functions

    if (error == QAbstractSocket::RemoteHostClosedError
        && socks5State == Connected) {
        // clear the read buffer in connect mode so that bytes available returns 0
        // if there already is a read notification pending then this will be processed first
        if (!readNotificationPending)
            connectData->readBuffer.clear();
        emitReadNotification();
        data->controlSocket->close();
        // cause a disconnect in the outer socket
        emitWriteNotification();
    } else if (socks5State == Uninitialized
               || socks5State == AuthenticationMethodsSent
               || socks5State == Authenticating
               || socks5State == RequestMethodSent) {
        setErrorState(socks5State == Uninitialized ? ConnectError : ControlSocketError);
        data->controlSocket->close();
        emitConnectionNotification();
    } else {
        q_func()->setError(data->controlSocket->error(), data->controlSocket->errorString());
        emitReadNotification();
        emitWriteNotification();
    }
}

void QSocks5SocketEnginePrivate::_q_controlSocketDisconnected()
{
    QSOCKS5_D_DEBUG << "_q_controlSocketDisconnected";
}

void QSocks5SocketEnginePrivate::_q_controlSocketStateChanged(QAbstractSocket::SocketState state)
{
    QSOCKS5_D_DEBUG << "_q_controlSocketStateChanged" << state;
}

#ifndef QT_NO_UDPSOCKET
void QSocks5SocketEnginePrivate::_q_udpSocketReadNotification()
{
    QSOCKS5_D_DEBUG << "_q_udpSocketReadNotification()";

    // check some state stuff
    if (!udpData->udpSocket->hasPendingDatagrams()) {
        QSOCKS5_D_DEBUG << "false read ??";
        return;
    }

    while (udpData->udpSocket->hasPendingDatagrams()) {
        QByteArray sealedBuf(udpData->udpSocket->pendingDatagramSize(), 0);
        QSOCKS5_D_DEBUG << "new datagram";
        udpData->udpSocket->readDatagram(sealedBuf.data(), sealedBuf.size());
        QByteArray inBuf;
        if (!data->authenticator->unSeal(sealedBuf, &inBuf)) {
            QSOCKS5_D_DEBUG << "failed unsealing datagram discarding";
            return;
        }
        QSOCKS5_DEBUG << dump(inBuf);
        int pos = 0;
        const char *buf = inBuf.constData();
        if (inBuf.size() < 4) {
            QSOCKS5_D_DEBUG << "bogus udp data, discarding";
            return;
        }
        QSocks5RevivedDatagram datagram;
        if (buf[pos++] != 0 || buf[pos++] != 0) {
            QSOCKS5_D_DEBUG << "invalid datagram discarding";
            return;
        }
        if (buf[pos++] != 0) { //### add fragmentation reading support
            QSOCKS5_D_DEBUG << "don't support fragmentation yet disgarding";
            return;
        }
        if (qt_socks5_get_host_address_and_port(inBuf, &datagram.address, &datagram.port, &pos) != 1) {
            QSOCKS5_D_DEBUG << "failed to get address from datagram disgarding";
            return;
        }
        datagram.data = QByteArray(&buf[pos], inBuf.size() - pos);
        udpData->pendingDatagrams.enqueue(datagram);
    }
    emitReadNotification();
}
#endif // QT_NO_UDPSOCKET

bool QSocks5SocketEngine::bind(const QHostAddress &addr, quint16 port)
{
    Q_D(QSocks5SocketEngine);

    // when bind we will block until the bind is finished as the info from the proxy server is needed

    QHostAddress address;
    if (addr.protocol() == QAbstractSocket::AnyIPProtocol)
        address = QHostAddress::AnyIPv4; //SOCKS5 doesn't support dual stack, and there isn't any implementation of udp on ipv6 yet
    else
        address = addr;

    if (!d->data) {
        if (socketType() == QAbstractSocket::TcpSocket) {
            d->initialize(QSocks5SocketEnginePrivate::BindMode);
#ifndef QT_NO_UDPSOCKET
        } else if (socketType() == QAbstractSocket::UdpSocket) {
            d->initialize(QSocks5SocketEnginePrivate::UdpAssociateMode);
#endif
        } else {
            //### something invalid
            return false;
        }
    }

#ifndef QT_NO_UDPSOCKET
    if (d->mode == QSocks5SocketEnginePrivate::UdpAssociateMode) {
        if (!d->udpData->udpSocket->bind(address, port)) {
            QSOCKS5_Q_DEBUG << "local udp bind failed";
            setError(d->udpData->udpSocket->error(), d->udpData->udpSocket->errorString());
            return false;
        }
        d->localAddress = d->udpData->udpSocket->localAddress();
        d->localPort = d->udpData->udpSocket->localPort();
    } else
#endif
    if (d->mode == QSocks5SocketEnginePrivate::BindMode) {
        d->localAddress = address;
        d->localPort = port;
    } else {
        //### something invalid
        return false;
    }

    d->data->controlSocket->connectToHost(d->proxyInfo.hostName(), d->proxyInfo.port());
    if (!d->waitForConnected(QDeadlineTimer{Socks5BlockingBindTimeout}, nullptr) ||
        d->data->controlSocket->state() == QAbstractSocket::UnconnectedState) {
        // waitForConnected sets the error state and closes the socket
        QSOCKS5_Q_DEBUG << "waitForConnected to proxy server" << d->data->controlSocket->errorString();
        return false;
    }
    if (d->socks5State == QSocks5SocketEnginePrivate::BindSuccess) {
        setState(QAbstractSocket::BoundState);
        return true;
#ifndef QT_NO_UDPSOCKET
    } else if (d->socks5State == QSocks5SocketEnginePrivate::UdpAssociateSuccess) {
        setState(QAbstractSocket::BoundState);
        d->udpData->associateAddress = d->localAddress;
        d->localAddress = QHostAddress();
        d->udpData->associatePort = d->localPort;
        d->localPort = 0;
        return true;
#endif // QT_NO_UDPSOCKET
    }

    // binding timed out
    setError(QAbstractSocket::SocketTimeoutError,
             QLatin1StringView(QT_TRANSLATE_NOOP("QSocks5SocketEngine", "Network operation timed out")));

///###    delete d->udpSocket;
///###    d->udpSocket = 0;
    return false;
}


bool QSocks5SocketEngine::listen(int backlog)
{
    Q_D(QSocks5SocketEngine);
    Q_UNUSED(backlog);

    QSOCKS5_Q_DEBUG << "listen()";

    // check that we are in bound and then go to listening.
    if (d->socketState == QAbstractSocket::BoundState) {
        d->socketState = QAbstractSocket::ListeningState;

        // check if we already have a connection
        if (d->socks5State == QSocks5SocketEnginePrivate::BindSuccess)
            d->emitReadNotification();

        return true;
    }
    return false;
}

qintptr QSocks5SocketEngine::accept()
{
    Q_D(QSocks5SocketEngine);
    // check we are listing ---

    QSOCKS5_Q_DEBUG << "accept()";

    qintptr sd = -1;
    switch (d->socks5State) {
    case QSocks5SocketEnginePrivate::BindSuccess:
        QSOCKS5_Q_DEBUG << "BindSuccess adding" << d->socketDescriptor << "to the bind store";
        d->data->controlSocket->disconnect();
        d->data->controlSocket->setParent(nullptr);
        d->bindData->localAddress = d->localAddress;
        d->bindData->localPort = d->localPort;
        sd = d->socketDescriptor;
        socks5BindStore()->add(sd, d->bindData);
        d->data = nullptr;
        d->bindData = nullptr;
        d->socketDescriptor = 0;
        //### do something about this socket layer ... set it closed and an error about why ...
        // reset state and local port/address
        d->socks5State = QSocks5SocketEnginePrivate::Uninitialized; // ..??
        d->socketState = QAbstractSocket::UnconnectedState;
        break;
    case QSocks5SocketEnginePrivate::ControlSocketError:
        setError(QAbstractSocket::ProxyProtocolError, "Control socket error"_L1);
        break;
    default:
        setError(QAbstractSocket::ProxyProtocolError, "SOCKS5 proxy error"_L1);
        break;
    }
    return sd;
}

void QSocks5SocketEngine::close()
{
    QSOCKS5_Q_DEBUG << "close()";
    Q_D(QSocks5SocketEngine);
    if (d->data && d->data->controlSocket) {
        if (d->data->controlSocket->state() == QAbstractSocket::ConnectedState) {
            QDeadlineTimer deadline(100ms);
            while (!d->data->controlSocket->bytesToWrite()) {
               if (!d->data->controlSocket->waitForBytesWritten(deadline.remainingTime()))
                   break;
            }
        }
        d->data->controlSocket->close();
    }
    d->inboundStreamCount = d->outboundStreamCount = 0;
#ifndef QT_NO_UDPSOCKET
    if (d->udpData && d->udpData->udpSocket)
        d->udpData->udpSocket->close();
#endif
}

qint64 QSocks5SocketEngine::bytesAvailable() const
{
    Q_D(const QSocks5SocketEngine);
    if (d->mode == QSocks5SocketEnginePrivate::ConnectMode)
        return d->connectData->readBuffer.size();
#ifndef QT_NO_UDPSOCKET
    else if (d->mode == QSocks5SocketEnginePrivate::UdpAssociateMode
             && !d->udpData->pendingDatagrams.isEmpty())
        return d->udpData->pendingDatagrams.constFirst().data.size();
#endif
    return 0;
}

qint64 QSocks5SocketEngine::read(char *data, qint64 maxlen)
{
    Q_D(QSocks5SocketEngine);
    QSOCKS5_Q_DEBUG << "read( , maxlen = " << maxlen << ')';
    if (d->mode == QSocks5SocketEnginePrivate::ConnectMode) {
        if (d->connectData->readBuffer.isEmpty()) {
            if (d->data->controlSocket->state() == QAbstractSocket::UnconnectedState) {
                //imitate remote closed
                close();
                setError(QAbstractSocket::RemoteHostClosedError,
                         "Remote host closed connection"_L1);
                setState(QAbstractSocket::UnconnectedState);
                return -1;
            } else {
                return 0;       // nothing to be read
            }
        }
        const qint64 copy = d->connectData->readBuffer.read(data, maxlen);
        QSOCKS5_DEBUG << "read" << dump(QByteArray(data, copy));
        return copy;
#ifndef QT_NO_UDPSOCKET
    } else if (d->mode == QSocks5SocketEnginePrivate::UdpAssociateMode) {
        return readDatagram(data, maxlen);
#endif
    }
    return 0;
}

qint64 QSocks5SocketEngine::write(const char *data, qint64 len)
{
    Q_D(QSocks5SocketEngine);
    QSOCKS5_Q_DEBUG << "write" << dump(QByteArray(data, len));

    if (d->mode == QSocks5SocketEnginePrivate::ConnectMode) {
        // clamp down the amount of bytes to transfer at once
        len = qMin<qint64>(len, MaxWriteBufferSize) - d->data->controlSocket->bytesToWrite();
        if (len <= 0)
            return 0;

        QByteArray buf = QByteArray::fromRawData(data, len);
        QByteArray sealedBuf;
        if (!d->data->authenticator->seal(buf, &sealedBuf)) {
            // ### Handle this error.
        }
        // We pass pointer and size because 'sealedBuf' is (most definitely) raw data:
        // QIODevice might have to cache the byte array if the socket cannot write the data.
        // If the _whole_ array needs to be cached then it would simply store a copy of the
        // array whose data will go out of scope and be deallocated before it can be used.
        qint64 written = d->data->controlSocket->write(sealedBuf.constData(), sealedBuf.size());

        if (written <= 0) {
            QSOCKS5_Q_DEBUG << "native write returned" << written;
            return written;
        }
        d->data->controlSocket->waitForBytesWritten(0);
        //NB: returning len rather than written for the OK case, because the "sealing" may increase the length
        return len;
#ifndef QT_NO_UDPSOCKET
    } else if (d->mode == QSocks5SocketEnginePrivate::UdpAssociateMode) {
        // send to connected address
        return writeDatagram(data, len, QIpPacketHeader(d->peerAddress, d->peerPort));
#endif
    }
    //### set an error ???
    return -1;
}

#ifndef QT_NO_UDPSOCKET
#ifndef QT_NO_NETWORKINTERFACE
bool QSocks5SocketEngine::joinMulticastGroup(const QHostAddress &,
                                             const QNetworkInterface &)
{
    setError(QAbstractSocket::UnsupportedSocketOperationError,
             "Operation on socket is not supported"_L1);
    return false;
}

bool QSocks5SocketEngine::leaveMulticastGroup(const QHostAddress &,
                                              const QNetworkInterface &)
{
    setError(QAbstractSocket::UnsupportedSocketOperationError,
             "Operation on socket is not supported"_L1);
    return false;
}


QNetworkInterface QSocks5SocketEngine::multicastInterface() const
{
    return QNetworkInterface();
}

bool QSocks5SocketEngine::setMulticastInterface(const QNetworkInterface &)
{
    setError(QAbstractSocket::UnsupportedSocketOperationError,
             "Operation on socket is not supported"_L1);
    return false;
}
#endif // QT_NO_NETWORKINTERFACE

bool QSocks5SocketEngine::hasPendingDatagrams() const
{
    Q_D(const QSocks5SocketEngine);
    Q_INIT_CHECK(false);

    return !d->udpData->pendingDatagrams.isEmpty();
}

qint64 QSocks5SocketEngine::pendingDatagramSize() const
{
    Q_D(const QSocks5SocketEngine);

    if (!d->udpData->pendingDatagrams.isEmpty())
        return d->udpData->pendingDatagrams.head().data.size();
    return 0;
}
#endif // QT_NO_UDPSOCKET

qint64 QSocks5SocketEngine::readDatagram(char *data, qint64 maxlen, QIpPacketHeader *header, PacketHeaderOptions)
{
#ifndef QT_NO_UDPSOCKET
    Q_D(QSocks5SocketEngine);

    if (d->udpData->pendingDatagrams.isEmpty())
        return 0;

    QSocks5RevivedDatagram datagram = d->udpData->pendingDatagrams.dequeue();
    int copyLen = qMin<int>(maxlen, datagram.data.size());
    memcpy(data, datagram.data.constData(), copyLen);
    if (header) {
        header->senderAddress = datagram.address;
        header->senderPort = datagram.port;
    }
    return copyLen;
#else
    Q_UNUSED(data);
    Q_UNUSED(maxlen);
    Q_UNUSED(header);
    return -1;
#endif // QT_NO_UDPSOCKET
}

qint64 QSocks5SocketEngine::writeDatagram(const char *data, qint64 len, const QIpPacketHeader &header)
{
#ifndef QT_NO_UDPSOCKET
    Q_D(QSocks5SocketEngine);

    // it is possible to send with out first binding with udp, but socks5 requires a bind.
    if (!d->data) {
        d->initialize(QSocks5SocketEnginePrivate::UdpAssociateMode);
        // all udp needs to be bound
        if (!bind(QHostAddress("0.0.0.0"_L1), 0)) {
            //### set error
            return -1;
        }
    }

    QByteArray outBuf;
    outBuf.reserve(270 + len);
    outBuf.append(3, '\0');
    if (!qt_socks5_set_host_address_and_port(header.destinationAddress, header.destinationPort, &outBuf)) {
        QSOCKS5_DEBUG << "error setting address" << header.destinationAddress << " : "
                      << header.destinationPort;
        //### set error code ....
        return -1;
    }
    outBuf += QByteArray(data, len);
    QSOCKS5_DEBUG << "sending" << dump(outBuf);
    QByteArray sealedBuf;
    if (!d->data->authenticator->seal(outBuf, &sealedBuf)) {
        QSOCKS5_DEBUG << "sealing data failed";
        setError(QAbstractSocket::SocketAccessError, d->data->authenticator->errorString());
        return -1;
    }
    if (d->udpData->udpSocket->writeDatagram(sealedBuf, d->udpData->associateAddress, d->udpData->associatePort) != sealedBuf.size()) {
        //### try frgamenting
        if (d->udpData->udpSocket->error() == QAbstractSocket::DatagramTooLargeError)
            setError(d->udpData->udpSocket->error(), d->udpData->udpSocket->errorString());
        //### else maybe more serious error
        return -1;
    }

    return len;
#else
    Q_UNUSED(data);
    Q_UNUSED(len);
    Q_UNUSED(header);
    return -1;
#endif // QT_NO_UDPSOCKET
}

qint64 QSocks5SocketEngine::bytesToWrite() const
{
    Q_D(const QSocks5SocketEngine);
    if (d->data && d->data->controlSocket) {
        return d->data->controlSocket->bytesToWrite();
    } else {
        return 0;
    }
}

int QSocks5SocketEngine::option(SocketOption option) const
{
    Q_D(const QSocks5SocketEngine);
    if (d->data && d->data->controlSocket) {
        // convert the enum and call the real socket
        if (option == QAbstractSocketEngine::LowDelayOption)
            return d->data->controlSocket->socketOption(QAbstractSocket::LowDelayOption).toInt();
        if (option == QAbstractSocketEngine::KeepAliveOption)
            return d->data->controlSocket->socketOption(QAbstractSocket::KeepAliveOption).toInt();
    }
    return -1;
}

bool QSocks5SocketEngine::setOption(SocketOption option, int value)
{
    Q_D(QSocks5SocketEngine);
    if (d->data && d->data->controlSocket) {
        // convert the enum and call the real socket
        if (option == QAbstractSocketEngine::LowDelayOption)
            d->data->controlSocket->setSocketOption(QAbstractSocket::LowDelayOption, value);
        if (option == QAbstractSocketEngine::KeepAliveOption)
            d->data->controlSocket->setSocketOption(QAbstractSocket::KeepAliveOption, value);
        return true;
    }
    return false;
}

bool QSocks5SocketEnginePrivate::waitForConnected(QDeadlineTimer deadline, bool *timedOut)
{
    if (data->controlSocket->state() == QAbstractSocket::UnconnectedState)
        return false;

    const Socks5State wantedState =
        mode == ConnectMode ? Connected :
        mode == BindMode ? BindSuccess :
        UdpAssociateSuccess;

    while (socks5State != wantedState) {
        if (!data->controlSocket->waitForReadyRead(deadline.remainingTime())) {
            if (data->controlSocket->state() == QAbstractSocket::UnconnectedState)
                return true;

            setErrorState(QSocks5SocketEnginePrivate::ControlSocketError);
            if (timedOut && data->controlSocket->error() == QAbstractSocket::SocketTimeoutError)
                *timedOut = true;
            return false;
        }
    }

    return true;
}

bool QSocks5SocketEngine::waitForRead(QDeadlineTimer deadline, bool *timedOut)
{
    Q_D(QSocks5SocketEngine);
    QSOCKS5_DEBUG << "waitForRead" << deadline.remainingTimeAsDuration();

    d->readNotificationActivated = false;

    // are we connected yet?
    if (!d->waitForConnected(deadline, timedOut))
        return false;
    if (d->data->controlSocket->state() == QAbstractSocket::UnconnectedState)
        return true;
    if (bytesAvailable() && d->readNotificationPending) {
        // We've got some data incoming, but the queued call hasn't been performed yet.
        // The data is where we expect it to be already, so just return true.
        return true;
    }

    // we're connected
    if (d->mode == QSocks5SocketEnginePrivate::ConnectMode ||
        d->mode == QSocks5SocketEnginePrivate::BindMode) {
        while (!d->readNotificationActivated) {
            if (!d->data->controlSocket->waitForReadyRead(deadline.remainingTime())) {
                if (d->data->controlSocket->state() == QAbstractSocket::UnconnectedState)
                    return true;

                setError(d->data->controlSocket->error(), d->data->controlSocket->errorString());
                if (timedOut && d->data->controlSocket->error() == QAbstractSocket::SocketTimeoutError)
                    *timedOut = true;
                return false;
            }
        }
#ifndef QT_NO_UDPSOCKET
    } else {
        while (!d->readNotificationActivated) {
            if (!d->udpData->udpSocket->waitForReadyRead(deadline.remainingTime())) {
                setError(d->udpData->udpSocket->error(), d->udpData->udpSocket->errorString());
                if (timedOut && d->udpData->udpSocket->error() == QAbstractSocket::SocketTimeoutError)
                    *timedOut = true;
                return false;
            }
        }
#endif // QT_NO_UDPSOCKET
    }


    bool ret = d->readNotificationActivated;
    d->readNotificationActivated = false;

    QSOCKS5_DEBUG << "waitForRead returned" << ret;
    return ret;
}


bool QSocks5SocketEngine::waitForWrite(QDeadlineTimer deadline, bool *timedOut)
{
    Q_D(QSocks5SocketEngine);
    QSOCKS5_DEBUG << "waitForWrite" << deadline.remainingTimeAsDuration();

    // are we connected yet?
    if (!d->waitForConnected(deadline, timedOut))
        return false;
    if (d->data->controlSocket->state() == QAbstractSocket::UnconnectedState)
        return true;

    // we're connected

    // flush any bytes we may still have buffered in the time that we have left
    if (d->data->controlSocket->bytesToWrite())
        d->data->controlSocket->waitForBytesWritten(deadline.remainingTime());

    auto shouldWriteBytes = [&]() {
        return d->data->controlSocket->state() == QAbstractSocket::ConnectedState
                && d->data->controlSocket->bytesToWrite() >= MaxWriteBufferSize;
    };

    qint64 remainingTime = deadline.remainingTime();
    for (; remainingTime > 0 && shouldWriteBytes(); remainingTime = deadline.remainingTime())
        d->data->controlSocket->waitForBytesWritten(remainingTime);
    return d->data->controlSocket->bytesToWrite() < MaxWriteBufferSize;
}

bool QSocks5SocketEngine::waitForReadOrWrite(bool *readyToRead, bool *readyToWrite,
                                            bool checkRead, bool checkWrite,
                                            QDeadlineTimer deadline, bool *timedOut)
{
    Q_UNUSED(checkRead);
    if (!checkWrite) {
        bool canRead = waitForRead(deadline, timedOut);
        if (readyToRead)
            *readyToRead = canRead;
        return canRead;
    }

    bool canWrite = waitForWrite(deadline, timedOut);
    if (readyToWrite)
        *readyToWrite = canWrite;
    return canWrite;
}

bool QSocks5SocketEngine::isReadNotificationEnabled() const
{
    Q_D(const QSocks5SocketEngine);
    return d->readNotificationEnabled;
}

void QSocks5SocketEngine::setReadNotificationEnabled(bool enable)
{
    Q_D(QSocks5SocketEngine);

    QSOCKS5_Q_DEBUG << "setReadNotificationEnabled(" << enable << ')';

    bool emitSignal = false;
    if (!d->readNotificationEnabled
        && enable) {
        if (d->mode == QSocks5SocketEnginePrivate::ConnectMode)
            emitSignal = !d->connectData->readBuffer.isEmpty();
#ifndef QT_NO_UDPSOCKET
        else if (d->mode == QSocks5SocketEnginePrivate::UdpAssociateMode)
            emitSignal = !d->udpData->pendingDatagrams.isEmpty();
#endif
        else if (d->mode == QSocks5SocketEnginePrivate::BindMode
            && d->socketState == QAbstractSocket::ListeningState
            && d->socks5State == QSocks5SocketEnginePrivate::BindSuccess)
            emitSignal = true;
    }

    d->readNotificationEnabled = enable;

    if (emitSignal)
        d->emitReadNotification();
}

bool QSocks5SocketEngine::isWriteNotificationEnabled() const
{
    Q_D(const QSocks5SocketEngine);
    return d->writeNotificationEnabled;
}

void QSocks5SocketEngine::setWriteNotificationEnabled(bool enable)
{
    Q_D(QSocks5SocketEngine);
    d->writeNotificationEnabled = enable;
    if (enable && d->socketState == QAbstractSocket::ConnectedState) {
        if (d->mode == QSocks5SocketEnginePrivate::ConnectMode && d->data->controlSocket->bytesToWrite())
            return; // will be emitted as a result of bytes written
       d->emitWriteNotification();
       d->writeNotificationActivated = false;
    }
}

bool QSocks5SocketEngine::isExceptionNotificationEnabled() const
{
    Q_D(const QSocks5SocketEngine);
    return d->exceptNotificationEnabled;
}

void QSocks5SocketEngine::setExceptionNotificationEnabled(bool enable)
{
    Q_D(QSocks5SocketEngine);
    d->exceptNotificationEnabled = enable;
}

QAbstractSocketEngine *
QSocks5SocketEngineHandler::createSocketEngine(QAbstractSocket::SocketType socketType,
                                               const QNetworkProxy &proxy, QObject *parent)
{
    Q_UNUSED(socketType);

    // proxy type must have been resolved by now
    if (proxy.type() != QNetworkProxy::Socks5Proxy) {
        QSOCKS5_DEBUG << "not proxying";
        return nullptr;
    }
    auto engine = std::make_unique<QSocks5SocketEngine>(parent);
    engine->setProxy(proxy);
    return engine.release();
}

QAbstractSocketEngine *QSocks5SocketEngineHandler::createSocketEngine(qintptr socketDescriptor, QObject *parent)
{
    QSOCKS5_DEBUG << "createSocketEngine" << socketDescriptor;
    if (socks5BindStore()->contains(socketDescriptor)) {
        QSOCKS5_DEBUG << "bind store contains" << socketDescriptor;
        return new QSocks5SocketEngine(parent);
    }
    return nullptr;
}

QT_END_NAMESPACE

#include "moc_qsocks5socketengine_p.cpp"
