/*
 * Copyright (C) 2009  Barracuda Networks, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

#include <QCoreApplication>
#include <QNetworkAddressEntry>
#include <QNetworkInterface>
#include <QTimer>
#include <QUdpSocket>

#include <QtCrypto>
#ifdef QCA_STATIC
#include <QtPlugin>
Q_IMPORT_PLUGIN(qca_ossl)
#endif

#include <iris/dtls.h>
#include <iris/ice176.h>
#include <iris/netinterface.h>
#include <iris/netnames.h>
#include <iris/processquit.h>
#include <iris/udpportreserver.h>
#include <stdio.h>

// scope values: 0 = local, 1 = link-local, 2 = private, 3 = public
static int getAddressScope(const QHostAddress &a)
{
    if (a.protocol() == QAbstractSocket::IPv6Protocol) {
        if (a == QHostAddress(QHostAddress::LocalHostIPv6))
            return 0;
        else if (XMPP::Ice176::isIPv6LinkLocalAddress(a))
            return 1;
    } else if (a.protocol() == QAbstractSocket::IPv4Protocol) {
        quint32 v4 = a.toIPv4Address();
        quint8  a0 = v4 >> 24;
        quint8  a1 = (v4 >> 16) & 0xff;
        if (a0 == 127)
            return 0;
        else if (a0 == 169 && a1 == 254)
            return 1;
        else if (a0 == 10)
            return 2;
        else if (a0 == 172 && a1 >= 16 && a1 <= 31)
            return 2;
        else if (a0 == 192 && a1 == 168)
            return 2;
    }

    return 3;
}

// -1 = a is higher priority, 1 = b is higher priority, 0 = equal
static int comparePriority(const QHostAddress &a, const QHostAddress &b)
{
    // prefer closer scope
    int a_scope = getAddressScope(a);
    int b_scope = getAddressScope(b);
    if (a_scope < b_scope)
        return -1;
    else if (a_scope > b_scope)
        return 1;

    // prefer ipv6
    if (a.protocol() == QAbstractSocket::IPv6Protocol && b.protocol() != QAbstractSocket::IPv6Protocol)
        return -1;
    else if (b.protocol() == QAbstractSocket::IPv6Protocol && a.protocol() != QAbstractSocket::IPv6Protocol)
        return 1;

    return 0;
}

static QList<QHostAddress> sortAddrs(const QList<QHostAddress> &in)
{
    QList<QHostAddress> out;

    for (const QHostAddress &a : in) {
        int at;
        for (at = 0; at < out.count(); ++at) {
            if (comparePriority(a, out[at]) < 0)
                break;
        }

        out.insert(at, a);
    }

    return out;
}

// some encoding routines taken from psimedia demo
static QString urlishEncode(const QString &in)
{
    QString out;
    for (int n = 0; n < in.length(); ++n) {
        if (in[n] == '%' || in[n] == ',' || in[n] == ';' || in[n] == ' ' || in[n] == '\n') {
            unsigned char c = (unsigned char)in[n].toLatin1();
            out += QString::asprintf("%%%02x", c);
        } else
            out += in[n];
    }
    return out;
}

static QString urlishDecode(const QString &in, bool *ok = 0)
{
    QString out;
    for (int n = 0; n < in.length(); ++n) {
        if (in[n] == '%') {
            if (n + 2 >= in.length()) {
                if (ok)
                    *ok = false;
                return QString();
            }

            QString hex = in.mid(n + 1, 2);
            bool    b;
            int     x = hex.toInt(&b, 16);
            if (!b) {
                if (ok)
                    *ok = false;
                return QString();
            }

            unsigned char c = (unsigned char)x;
            out += c;
            n += 2;
        } else
            out += in[n];
    }

    if (ok)
        *ok = true;
    return out;
}

static QString candidate_to_line(const XMPP::Ice176::Candidate &in)
{
    QStringList list;
    list += QString::number(in.component);
    list += in.foundation;
    list += QString::number(in.generation);
    list += in.id;
    list += in.ip.toString();
    list += QString::number(in.network);
    list += QString::number(in.port);
    list += QString::number(in.priority);
    list += in.protocol;
    list += in.rel_addr.toString();
    list += QString::number(in.rel_port);
    list += in.rem_addr.toString();
    list += QString::number(in.rem_port);
    list += in.type;

    for (int n = 0; n < list.count(); ++n)
        list[n] = urlishEncode(list[n]);
    return list.join(",");
}

static XMPP::Ice176::Candidate line_to_candidate(const QString &in)
{
    QStringList list = in.split(',');
    if (list.count() < 14)
        return XMPP::Ice176::Candidate();

    for (int n = 0; n < list.count(); ++n) {
        bool    ok;
        QString str = urlishDecode(list[n], &ok);
        if (!ok)
            return XMPP::Ice176::Candidate();
        list[n] = str;
    }

    XMPP::Ice176::Candidate out;
    out.component  = list[0].toInt();
    out.foundation = list[1];
    out.generation = list[2].toInt();
    out.id         = list[3];
    out.ip         = QHostAddress(list[4]);
    out.network    = list[5].toInt();
    out.port       = list[6].toInt();
    out.priority   = list[7].toInt();
    out.protocol   = list[8];
    out.rel_addr   = QHostAddress(list[9]);
    out.rel_port   = list[10].toInt();
    out.rem_addr   = QHostAddress(list[11]);
    out.rem_port   = list[12].toInt();
    out.type       = list[13];
    return out;
}

class IceOffer {
public:
    QString                        user, pass;
    QList<XMPP::Ice176::Candidate> candidates;
    QList<XMPP::Hash>              dtlsFingerprint;
};

static QStringList line_wrap(const QString &prefix, const QString &in, int maxlen)
{
    Q_ASSERT(maxlen >= 1);

    QStringList out;
    int         at = 0;
    while (at < in.length()) {
        int takeAmount = qMin(maxlen - prefix.size() - 1, in.length() - at);
        out += QString("%1:%2").arg(prefix, in.mid(at, takeAmount));
        at += takeAmount;
    }
    return out;
}

static QMap<QString, QString> lines_unwrap(const QStringList &in)
{
    QMap<QString, QString> prefix2body;
    for (auto const &l : in) {
        auto prefix = l.section(':', 0, 0);
        auto body   = l.section(':', 1, 1);
        prefix2body[prefix] += body;
    }

    return prefix2body;
}

static QStringList iceblock_create(const IceOffer &in)
{
    QStringList out;
    out += "-----BEGIN SESSION-----";
    {
        // ice
        QStringList body;
        QStringList userpass;
        userpass += urlishEncode(in.user);
        userpass += urlishEncode(in.pass);
        body += userpass.join(",");
        for (const XMPP::Ice176::Candidate &c : in.candidates)
            body += candidate_to_line(c);
        out += line_wrap(QLatin1String("ice"), body.join(";"), 78);

        // dtls
        body.clear();
        for (auto const &h : in.dtlsFingerprint) {
            body += h.toString();
        }
        out += line_wrap(QLatin1String("dtls"), body.join(";"), 78);
    }
    out += "-----END SESSION-----";
    return out;
}

static IceOffer iceblock_parse(const QStringList &in)
{
    IceOffer out;
    if (in.count() < 3 || in[0] != "-----BEGIN SESSION-----" || in[in.count() - 1] != "-----END SESSION-----")
        return IceOffer();

    QMap<QString, QString> lines = lines_unwrap(in.mid(1, in.count() - 2));

    QStringList body = lines.value(QLatin1String("ice")).split(';');
    if (body.count() < 2)
        return IceOffer();

    QStringList parts = body[0].split(',');
    if (parts.count() != 2)
        return IceOffer();
    bool ok;
    out.user = urlishDecode(parts[0], &ok);
    if (!ok || out.user.isEmpty())
        return IceOffer();
    out.pass = urlishDecode(parts[1], &ok);
    if (!ok || out.pass.isEmpty())
        return IceOffer();

    for (int n = 1; n < body.count(); ++n) {
        XMPP::Ice176::Candidate c = line_to_candidate(body[n]);
        if (c.type.isEmpty())
            return IceOffer();
        out.candidates += c;
    }

    body = lines.value(QLatin1String("dtls")).split(';');
    for (auto const &b : body) {
        out.dtlsFingerprint += XMPP::Hash::from(b);
    }

    return out;
}

class IceBlockReader : public QObject {
    Q_OBJECT

public:
    QCA::ConsoleReference *con;
    QByteArray             in;

    IceBlockReader(QObject *parent = 0) : QObject(parent)
    {
        bool ok;

        con = new QCA::ConsoleReference(this);
        connect(con, SIGNAL(readyRead()), SLOT(con_readyRead()));
        connect(con, SIGNAL(inputClosed()), SLOT(con_inputClosed()));
        ok = con->start(QCA::Console::stdioInstance());
        Q_ASSERT(ok);
    }

signals:
    void finished(const QStringList &lines);
    void error();

private slots:
    void con_readyRead()
    {
        in += con->read();
        if (in.contains("-----END SESSION-----")) {
            delete con;
            con = 0;

            QStringList out;
            QBuffer     buf(&in);
            buf.open(QIODevice::ReadOnly | QIODevice::Text);
            QTextStream ts(&buf);
            while (!ts.atEnd())
                out += ts.readLine();
            emit finished(out);
        }
    }

    void con_inputClosed()
    {
        delete con;
        con = 0;

        emit error();
    }
};

class EnterPrompt : public QObject {
    Q_OBJECT

public:
    QCA::ConsoleReference *con;

    EnterPrompt(QObject *parent = 0) : QObject(parent)
    {
        bool ok;

        con = new QCA::ConsoleReference(this);
        connect(con, SIGNAL(readyRead()), SLOT(con_readyRead()));
        connect(con, SIGNAL(inputClosed()), SLOT(con_inputClosed()));
        ok = con->start(QCA::Console::stdioInstance());
        Q_ASSERT(ok);
    }

signals:
    void finished();
    void error();

private slots:
    void con_readyRead()
    {
        if (con->read().contains('\n')) {
            delete con;
            con = 0;

            emit finished();
        }
    }

    void con_inputClosed()
    {
        delete con;
        con = 0;

        emit error();
    }
};

/*static QStringList iceblock_read()
{
    QStringList out;

    FILE *fp = stdin;
    while(1)
    {
        QByteArray line(1024, 0);
        if(fgets(line.data(), line.size(), fp) == NULL)
            return QStringList();
        if(feof(fp))
            break;

        // hack off newline
        line.resize(qstrlen(line.data()) - 1);

        QString str = QString::fromLocal8Bit(line);
        out += str;
        if(str == "-----END ICE-----")
            break;
    }

    return out;
}*/

/*static void wait_for_enter()
{
    QByteArray buf(1024, 0);
    if(fgets(buf.data(), buf.size(), stdin) == NULL)
        return;
}*/

class App : public QObject {
    Q_OBJECT

public:
    class Channel {
    public:
        QUdpSocket *sock6, *sock4;
        bool        ready;
    };

    enum StunServiceType { Auto, Basic, Relay };

    int             opt_mode      = 0;
    int             opt_localBase = 0;
    int             opt_iceBase   = 0;
    int             opt_channels  = 0;
    QString         opt_stunHost;
    int             opt_stunPort = 0;
    StunServiceType opt_stunType = Auto;
    QString         opt_user;
    QString         opt_pass;
    bool            opt_ipv6_only      = false;
    bool            opt_relay_udp_only = false;
    bool            opt_relay_tcp_only = false;

    XMPP::NameResolver                dns;
    QHostAddress                      stunAddr;
    XMPP::UdpPortReserver             portReserver;
    XMPP::Ice176                     *ice = nullptr;
    QList<XMPP::Dtls *>               dtls;
    QList<XMPP::Ice176::Candidate>    localCandidates;
    QList<XMPP::Ice176::LocalAddress> localAddrs;
    QList<Channel>                    channels;
    QCA::Console                     *console = nullptr;
    IceBlockReader                   *reader  = nullptr;
    EnterPrompt                      *prompt  = nullptr;
    IceOffer                          inOffer;

    App() : portReserver(this) { }

    ~App()
    {
        delete prompt;
        delete reader;
        delete console;

        delete ice;

        for (int n = 0; n < channels.count(); ++n) {
            if (channels[n].sock6) {
                channels[n].sock6->disconnect(this);
                channels[n].sock6->setParent(0);
                channels[n].sock6->deleteLater();
                channels[n].sock6 = 0;
            }

            if (channels[n].sock4) {
                channels[n].sock4->disconnect(this);
                channels[n].sock4->setParent(0);
                channels[n].sock4->deleteLater();
                channels[n].sock4 = 0;
            }
        }
    }

public slots:
    void start()
    {
        connect(XMPP::ProcessQuit::instance(), SIGNAL(quit()), SLOT(do_quit()));

        connect(&dns, &XMPP::NameResolver::resultsReady, this, [this](const QList<XMPP::NameRecord> &results) {
            stunAddr = results.first().address();
            start_ice();
        });
        connect(&dns, &XMPP::NameResolver::error, this, [this](XMPP::NameResolver::Error e) {
            Q_UNUSED(e);
            printf("Unable to resolve stun host.\n");
            Q_EMIT quit();
        });

        if (!opt_stunHost.isEmpty())
            dns.start(opt_stunHost.toLatin1(), XMPP::NameRecord::A);
        else
            start_ice();
    }

public:
    void start_ice()
    {
        ice = new XMPP::Ice176(this);
        connect(ice, SIGNAL(started()), SLOT(ice_started()));
        connect(ice, SIGNAL(stopped()), SLOT(ice_stopped()));
        connect(ice, SIGNAL(error(XMPP::Ice176::Error)), SLOT(ice_error(XMPP::Ice176::Error)));
        connect(ice, &XMPP::Ice176::localCandidatesReady, this,
                [this](const QList<XMPP::Ice176::Candidate> &cs) { localCandidates += cs; });
        connect(ice, &XMPP::Ice176::localGatheringComplete, this, &App::ice_localGatheringComplete);
        connect(ice, &XMPP::Ice176::componentReady, this, [this](int componentIndex) {
            printf("Channel %d ready.\n", componentIndex);
            channels[componentIndex].ready = true;
        });

        connect(ice, SIGNAL(readyRead(int)), SLOT(ice_readyRead(int)));
        connect(ice, SIGNAL(datagramsWritten(int, int)), SLOT(ice_datagramsWritten(int, int)));
        connect(ice, &XMPP::Ice176::readyToSendMedia, this, &App::ice_readToSendMedia);
        connect(ice, &XMPP::Ice176::iceFinished, this,
                []() { printf("ICE negotiation has finished and media stream is active now!\n"); });

        // set up local ports for forwarding
        for (int n = 0; n < opt_channels; ++n) {
            Channel chan;

            int port   = opt_localBase + 32 + n;
            chan.sock6 = setupSocket(QHostAddress::LocalHostIPv6, port);
            chan.sock4 = setupSocket(QHostAddress::LocalHost, port);

            if (!chan.sock6 && !chan.sock4) {
                printf("Unable to bind to port %d.\n", port);
                emit quit();
                return;
            }

            chan.ready = false;

            channels += chan;
        }

        QList<QHostAddress> listenAddrs;
        if (!opt_relay_tcp_only) {
            /*XMPP::NetInterfaceManager netman;
            foreach(const QString &id, netman.interfaces())
            {
                XMPP::NetInterface ni(id, &netman);

                QHostAddress v4addr;
                foreach(const QHostAddress &h, ni.addresses())
                {
                    if(h.protocol() == QAbstractSocket::IPv4Protocol)
                    {
                        v4addr = h;
                        break;
                    }
                }

                if(!v4addr.isNull())
                {
                    XMPP::Ice176::LocalAddress addr;
                    addr.addr = v4addr;
                    localAddrs += addr;
                    strList += addr.addr.toString();
                }
            }*/
            auto const interfaces = QNetworkInterface::allInterfaces();
            for (const QNetworkInterface &ni : interfaces) {
                if (!(ni.flags() & QNetworkInterface::IsUp))
                    continue;
                QList<QNetworkAddressEntry> entries = ni.addressEntries();
                for (const QNetworkAddressEntry &na : entries) {
                    QHostAddress h = na.ip();

                    if (opt_ipv6_only && h.protocol() != QAbstractSocket::IPv6Protocol)
                        continue;

                    // skip localhost
                    if (getAddressScope(h) == 0)
                        continue;

                    // don't put the same address in twice.
                    //   this also means that if there are
                    //   two link-local ipv6 interfaces
                    //   with the exact same address, we
                    //   only use the first one
                    if (!listenAddrs.contains(h)) {
                        if (h.protocol() == QAbstractSocket::IPv6Protocol && XMPP::Ice176::isIPv6LinkLocalAddress(h))
                            h.setScopeId(ni.name());
                        listenAddrs += h;
                    }
                }
            }
        }
        /*{
            XMPP::Ice176::LocalAddress addr;
            addr.addr = QHostAddress::LocalHost;
            localAddrs += addr;
            strList += addr.addr.toString();
        }*/

        listenAddrs = sortAddrs(listenAddrs);

        QStringList strList;
        for (const QHostAddress &h : listenAddrs) // QNetworkInterface::allAddresses())
        {
            XMPP::Ice176::LocalAddress addr;
            addr.addr = h;
            localAddrs += addr;
            strList += h.toString();
        }

        if (opt_iceBase > 0) {
            portReserver.setAddresses(listenAddrs);
            portReserver.setPorts(opt_iceBase, opt_channels);

            ice->setPortReserver(&portReserver);
        }

        ice->setLocalAddresses(localAddrs);

        if (!strList.isEmpty()) {
            printf("Host addresses:\n");
            for (const QString &s : strList)
                printf("  %s\n", qPrintable(s));
        }

        ice->setComponentCount(opt_channels);
        ice->setLocalFeatures(XMPP::Ice176::Trickle | XMPP::Ice176::NotNominatedData);

        if (!stunAddr.isNull()) {
            if (opt_stunType == Basic || opt_stunType == Auto)
                ice->setStunBindService(stunAddr, opt_stunPort);
            if ((opt_stunType == Relay || opt_stunType == Auto) && !opt_user.isEmpty()) {
                ice->setStunRelayUdpService(stunAddr, opt_stunPort, opt_user, opt_pass.toUtf8());
                ice->setStunRelayTcpService(stunAddr, opt_stunPort, opt_user, opt_pass.toUtf8());
            }

            printf("STUN service: %s\n", qPrintable(stunAddr.toString()));
        }

        if (opt_relay_udp_only) {
            ice->setUseLocal(false);
            ice->setUseStunBind(false);
            ice->setUseStunRelayTcp(false);
        }

        if (opt_mode == 0)
            ice->start(XMPP::Ice176::Initiator);
        else
            ice->start(XMPP::Ice176::Responder);

        if (QCA::isSupported("dtls")) {
            start_dtls();
        }
    }

Q_SIGNALS:
    void quit();

private:
    void proxyNet2App(int componentIndex, const QByteArray &buf)
    {
        if (channels[componentIndex].sock6)
            channels[componentIndex].sock6->writeDatagram(buf, QHostAddress::LocalHostIPv6,
                                                          opt_localBase + componentIndex);
        if (channels[componentIndex].sock4)
            channels[componentIndex].sock4->writeDatagram(buf, QHostAddress::LocalHost, opt_localBase + componentIndex);
    }

    void start_dtls()
    {
        dtls.clear();
        dtls.reserve(opt_channels);
        for (int componentIndex = 0; componentIndex < opt_channels; componentIndex++) {
            dtls.append(new XMPP::Dtls(this));
            dtls[componentIndex]->generateCertificate();
            auto h = dtls[componentIndex]->fingerprint();
            printf("fingerprint[%d]:%s:%s\n", componentIndex, qPrintable(h.stringType()), h.data().toHex(':').data());
            connect(dtls[componentIndex], &XMPP::Dtls::readyRead, this,
                    [this, dtls = dtls[componentIndex], componentIndex]() {
                        proxyNet2App(componentIndex, dtls->readDatagram());
                    });
            connect(dtls[componentIndex], &XMPP::Dtls::readyReadOutgoing, this,
                    [this, dtls = dtls[componentIndex], componentIndex]() {
                        ice->writeDatagram(componentIndex, dtls->readOutgoingDatagram());
                    });
            connect(dtls[componentIndex], &XMPP::Dtls::connected, this, []() { printf("DTLS connected\n"); });
            connect(dtls[componentIndex], &XMPP::Dtls::closed, this, []() { printf("DTLS closed\n"); });
            connect(dtls[componentIndex], &XMPP::Dtls::errorOccurred, this,
                    [](QAbstractSocket::SocketError err) { printf("DTLS error: %d\n", int(err)); });
        }
    }

    QUdpSocket *setupSocket(const QHostAddress &addr, int port)
    {
        QUdpSocket *sock = new QUdpSocket(this);
        connect(sock, SIGNAL(readyRead()), SLOT(sock_readyRead()));
        connect(sock, SIGNAL(bytesWritten(qint64)), SLOT(sock_bytesWritten(qint64)));
        if (!sock->bind(addr, port)) {
            delete sock;
            return 0;
        }

        return sock;
    }

private Q_SLOTS:
    void do_quit()
    {
        // a good idea
        XMPP::ProcessQuit::cleanup();

        if (ice) {
            printf("Stopping ICE.\n");
            ice->stop();
        } else
            emit quit();
    }

    void ice_started()
    {
        if (channels.count() > 1) {
            printf("Local ports: %d-%d\n", opt_localBase, opt_localBase + channels.count() - 1);
            printf("Tunnel ports: %d-%d\n", opt_localBase + 32, opt_localBase + 32 + channels.count() - 1);
        } else {
            printf("Local port: %d\n", opt_localBase);
            printf("Tunnel port: %d\n", opt_localBase + 32);
        }
    }

    void ice_stopped() { emit quit(); }

    void ice_error(XMPP::Ice176::Error e)
    {
        printf("ICE error: %d\n", e);
        emit quit();
    }

    void ice_localGatheringComplete()
    {
        IceOffer out;
        out.user       = ice->localUfrag();
        out.pass       = ice->localPassword();
        out.candidates = localCandidates;
        for (const auto d : dtls) {
            out.dtlsFingerprint += d->fingerprint();
        }
        QStringList block = iceblock_create(out);

        for (const QString &s : block)
            printf("%s\n", qPrintable(s));

        printf("Give above SESSION block to peer.  Obtain peer ICE block and paste below...\n");

        console = new QCA::Console(QCA::Console::Stdio, QCA::Console::Read, QCA::Console::Default, this);

        reader = new IceBlockReader(this);
        connect(reader, SIGNAL(finished(const QStringList &)), SLOT(reader_finished(const QStringList &)));
        connect(reader, SIGNAL(error()), SLOT(reader_error()));
    }

    void ice_readyRead(int componentIndex)
    {
        while (ice->hasPendingDatagrams(componentIndex)) {
            QByteArray buf = ice->readDatagram(componentIndex);
            if (componentIndex < dtls.size()) {
                dtls[componentIndex]->writeIncomingDatagram(buf);
            } else {
                proxyNet2App(componentIndex, buf);
            }
        }
    }

    void ice_datagramsWritten(int componentIndex, int count)
    {
        Q_UNUSED(componentIndex);
        Q_UNUSED(count);

        // do nothing
    }

    void reader_finished(const QStringList &lines)
    {
        delete reader;
        reader = 0;

        inOffer = iceblock_parse(lines);
        if (inOffer.user.isEmpty()) {
            printf("Error parsing ICE block.\n");
            emit quit();
            return;
        }

        if (dtls.size() != inOffer.dtlsFingerprint.size()) {
            printf("DTLS disabled due to mismatch in amount of dtls instances and remote fingerprints (not yet "
                   "unsupported).\n");
            qDeleteAll(dtls);
            dtls.clear();
            inOffer.dtlsFingerprint.clear();
        }

        printf("Press enter to begin.\n");
        prompt = new EnterPrompt(this);
        connect(prompt, SIGNAL(finished()), SLOT(prompt_finished()));
        connect(prompt, SIGNAL(finished()), SLOT(prompt_error()));
    }

    void reader_error()
    {
        delete reader;
        reader = 0;

        printf("Unable to read stdin.\n");
        emit quit();
    }

    void prompt_finished()
    {
        delete prompt;
        prompt = 0;

        ice->setPeerUfrag(inOffer.user);
        ice->setPeerPassword(inOffer.pass);
        ice->addRemoteCandidates(inOffer.candidates);
        ice->startChecks();
    }

    void prompt_error()
    {
        delete prompt;
        prompt = 0;

        printf("Unable to read stdin.\n");
        emit quit();
    }

    void sock_readyRead()
    {
        QUdpSocket *sock = static_cast<QUdpSocket *>(sender());
        int         at   = -1;
        for (int n = 0; n < channels.count(); ++n) {
            if (channels[n].sock6 == sock || channels[n].sock4 == sock) {
                at = n;
                break;
            }
        }
        Q_ASSERT(at != -1);

        while (sock->hasPendingDatagrams()) {
            QByteArray buf;
            buf.resize(sock->pendingDatagramSize());

            // note: we don't care who sent it
            sock->readDatagram(buf.data(), buf.size());

            if (channels[at].ready) {
                if (at < dtls.size()) {
                    dtls[at]->writeDatagram(buf);
                } else {
                    ice->writeDatagram(at, buf);
                }
            }
        }
    }

    void sock_bytesWritten(qint64 bytes)
    {
        Q_UNUSED(bytes);

        // do nothing
    }

    void ice_readToSendMedia()
    {
        printf("ICE ready to send media.\n");
        for (int i = 0; i < dtls.size(); i++) {
            dtls[i]->setRemoteFingerprint(inOffer.dtlsFingerprint[i]);
            if (opt_mode == 0)
                dtls[i]->startClient();
            else
                dtls[i]->startServer();
        }
    }
};

void usage()
{
    printf("icetunnel: create a peer-to-peer UDP tunnel based on ICE\n");
    printf("usage: icetunnel initiator (options)\n");
    printf("       icetunnel responder (options)\n");
    printf("\n");
    printf(" --localbase=[n]     local base port (default=60000)\n");
    printf(" --icebase=[n]       ICE base port (default=0 (None))\n");
    printf(" --channels=[n]      number of channels to create (default=1)\n");
    printf(" --stunhost=[host]   STUN server to use\n");
    printf(" --stunport=[n]      STUN server port to use (default=3478)\n");
    printf(" --stuntype=[type]   auto, basic, or relay (default=auto)\n");
    printf(" --user=[user]       STUN server username\n");
    printf(" --pass=[pass]       STUN server password\n");
    printf(" --ipv6-only         only use IPv6 network interface addresses\n");
    printf(" --relay-udp-only    only offer UDP relay candidate\n");
    printf(" --relay-tcp-only    only offer TCP relay candidate\n");
    printf("\n");
}

int main(int argc, char **argv)
{

    QCA::Initializer qcaInit;
    QCoreApplication qapp(argc, argv);

    QStringList args = qapp.arguments();
    args.removeFirst();

    int                  localBase = 60000;
    int                  iceBase   = 0;
    int                  channels  = 1;
    QString              stunHost;
    int                  stunPort = 3478;
    App::StunServiceType stunType = App::Auto;
    QString              user, pass;
    bool                 ipv6_only      = false;
    bool                 relay_udp_only = false;
    bool                 relay_tcp_only = false;
    bool                 enable_dtls    = true;

    for (int n = 0; n < args.count(); ++n) {
        QString s = args[n];
        if (!s.startsWith("--"))
            continue;
        QString var;
        QString val;
        int     x = s.indexOf('=');
        if (x != -1) {
            var = s.mid(2, x - 2);
            val = s.mid(x + 1);
        } else {
            var = s.mid(2);
        }

        bool known = true;

        if (var == "localbase")
            localBase = val.toInt();
        else if (var == "icebase")
            iceBase = val.toInt();
        else if (var == "channels") {
            channels = val.toInt();
            if (channels < 1 || channels > 32) {
                fprintf(stderr, "Number of channels must be between 1-32.\n");
                return 1;
            }
        } else if (var == "stunhost")
            stunHost = val;
        else if (var == "stunport")
            stunPort = val.toInt();
        else if (var == "stuntype") {
            if (val == "auto")
                stunType = App::Auto;
            else if (val == "basic")
                stunType = App::Basic;
            else if (val == "relay")
                stunType = App::Relay;
            else {
                usage();
                return 1;
            }
        } else if (var == "user")
            user = val;
        else if (var == "pass")
            pass = val;
        else if (var == "ipv6-only")
            ipv6_only = true;
        else if (var == "relay-udp-only")
            relay_udp_only = true;
        else if (var == "relay-tcp-only")
            relay_tcp_only = true;
        else if (var == "dtls")
            enable_dtls = true;
        else
            known = false;

        if (!known) {
            fprintf(stderr, "Unknown option '%s'.\n", qPrintable(var));
            return 1;
        }

        args.removeAt(n);
        --n; // adjust position
    }

    if (args.isEmpty()) {
        usage();
        return 1;
    }

    if (relay_udp_only && relay_tcp_only) {
        fprintf(stderr, "Cannot use both --relay-udp-only and --relay-tcp-only.\n");
        return 1;
    }

    int mode = -1;
    if (args[0] == "initiator")
        mode = 0;
    else if (args[0] == "responder")
        mode = 1;

    if (mode == -1) {
        usage();
        return 1;
    }

    if (!QCA::isSupported("hmac(sha1)")) {
        printf("Error: Need hmac(sha1) support.\n");
        return 1;
    }

    App app;
    app.opt_mode           = mode;
    app.opt_localBase      = localBase;
    app.opt_iceBase        = iceBase;
    app.opt_channels       = channels;
    app.opt_stunHost       = stunHost;
    app.opt_stunPort       = stunPort;
    app.opt_stunType       = stunType;
    app.opt_user           = user;
    app.opt_pass           = pass;
    app.opt_ipv6_only      = ipv6_only;
    app.opt_relay_udp_only = relay_udp_only;
    app.opt_relay_tcp_only = relay_tcp_only;

    QObject::connect(&app, SIGNAL(quit()), &qapp, SLOT(quit()));
    QTimer::singleShot(0, &app, SLOT(start()));
    qapp.exec();

    return 0;
}

#include "main.moc"
