/*
 * Copyright (c) 2019-2021, OARC, Inc.
 * Copyright (c) 2019, DENIC eG
 * All rights reserved.
 *
 * This file is part of dnsmeter.
 *
 * dnsmeter is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * dnsmeter is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with dnsmeter.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "dns_sender_thread.h"
#include "query.h"
#include "exceptions.h"

#include <netinet/in.h>
#include <arpa/inet.h>
#include <math.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <string.h>
#include <errno.h>

DNSSenderThread::DNSSenderThread()
{
    buffer = (unsigned char*)malloc(4096);
    if (!buffer)
        throw ppl7::OutOfMemoryException();
    Timeslice            = 0.0f;
    runtime              = 10;
    timeout              = 5;
    queryrate            = 0;
    counter_packets_send = 0;
    counter_bytes_send   = 0;
    errors               = 0;
    counter_0bytes       = 0;
    duration             = 0.0;
    for (int i                = 0; i < 255; i++)
        counter_errorcodes[i] = 0;
    verbose                   = false;
    spoofingEnabled           = false;
    DnssecRate                = 0;
    dnsseccounter             = 0;
    payload                   = NULL;
    spoofing_net_start        = 0;
    spoofing_net_size         = 0;
    payloadIsPcap             = false;
    spoofingFromPcap          = false;
}

DNSSenderThread::~DNSSenderThread()
{
    free(buffer);
}

void DNSSenderThread::setDestination(const ppl7::IPAddress& ip, int port)
{
    Socket.setDestination(ip, port);
    pkt.setDestination(ip, port);
}

void DNSSenderThread::setPayload(PayloadFile& payload)
{
    this->payload       = &payload;
    this->payloadIsPcap = payload.isPcap();
}

void DNSSenderThread::setRuntime(int seconds)
{
    runtime = seconds;
}

void DNSSenderThread::setTimeout(int seconds)
{
    timeout = seconds;
}

void DNSSenderThread::setDNSSECRate(int rate)
{
    DnssecRate = rate;
}

void DNSSenderThread::setQueryRate(ppluint64 qps)
{
    queryrate = qps;
}

void DNSSenderThread::setTimeslice(float ms)
{
    if (ms == 0.0f || ms > 1000.0f)
        throw ppl7::InvalidArgumentsException();
    //if ((1000 % ms)!=0) throw ppl7::InvalidArgumentsException();
    Timeslice = (double)ms / 1000;
}

void DNSSenderThread::setSourceIP(const ppl7::IPAddress& ip)
{
    sourceip        = ip;
    spoofingEnabled = false;
}

void DNSSenderThread::setSourceNet(const ppl7::IPNetwork& net)
{
    sourcenet          = net;
    spoofingEnabled    = true;
    spoofing_net_start = ntohl(*(in_addr_t*)net.first().addr());
    spoofing_net_size  = powl(2, 32 - net.prefixlen());
}

void DNSSenderThread::setSourcePcap()
{
    spoofingEnabled  = true;
    spoofingFromPcap = true;
}

void DNSSenderThread::setVerbose(bool verbose)
{
    this->verbose = verbose;
}

#define PCAP_HEADER_SIZE 14 + sizeof(struct ip) + sizeof(struct udphdr)

void DNSSenderThread::sendPacket()
{
    size_t query_size;
    while (1) {
        try {
            const ppl7::ByteArrayPtr& bap = payload->getQuery();
            query_size                    = bap.size();
            if (payloadIsPcap) {
                query_size -= PCAP_HEADER_SIZE;
                memcpy(buffer, ((const char*)bap.ptr()) + PCAP_HEADER_SIZE, query_size);
            } else {
                memcpy(buffer, bap.ptr(), query_size);
                dnsseccounter += DnssecRate;
                if (dnsseccounter >= 100) {
                    query_size = AddDnssecToQuery(buffer, 4096, query_size);
                    dnsseccounter -= 100;
                }
            }
            pkt.setPayload(buffer, query_size);
            if (spoofingEnabled) {
                if (spoofingFromPcap) {
                    pkt.useSourceFromPcap((const char*)bap.ptr(), bap.size());
                } else {
                    pkt.randomSourceIP(spoofing_net_start, spoofing_net_size);
                    pkt.randomSourcePort();
                }
            } else {
                pkt.randomSourcePort();
            }
            pkt.setDnsId(getQueryTimestamp());
            ssize_t n = Socket.send(pkt);
            if (n > 0 && (size_t)n == pkt.size()) {
                counter_packets_send++;
                counter_bytes_send += pkt.size();
            } else if (n < 0) {
                if (errno < 255)
                    counter_errorcodes[errno]++;
                errors++;
            } else {
                counter_0bytes++;
            }
            return;
        } catch (const UnknownRRType& exp) {
            continue;
        } catch (const InvalidDNSQuery& exp) {
            continue;
        }
    }
}

void DNSSenderThread::run()
{
    if (!payload)
        throw ppl7::NullPointerException("payload not set!");
    if (!spoofingEnabled) {
        pkt.setSource(sourceip, 0x4567);
    }
    dnsseccounter        = 0;
    counter_packets_send = 0;
    counter_bytes_send   = 0;
    counter_0bytes       = 0;
    errors               = 0;
    duration             = 0.0;
    for (int i                = 0; i < 255; i++)
        counter_errorcodes[i] = 0;
    double start              = ppl7::GetMicrotime();
    if (queryrate > 0) {
        runWithRateLimit();
    } else {
        runWithoutRateLimit();
    }
    duration = ppl7::GetMicrotime() - start;
    waitForTimeout();
}

void DNSSenderThread::runWithoutRateLimit()
{
    double start = ppl7::GetMicrotime();
    double end   = start + (double)runtime;
    double now;
    int    pc = 0;
    while (1) {
        sendPacket();
        pc++;
        if (pc > 10000) {
            pc = 0;
            if (this->threadShouldStop())
                break;
            now = ppl7::GetMicrotime();
            if (now > end)
                break;
        }
    }
}

static inline double getNsec()
{
    struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &ts);
    return (double)ts.tv_sec + ((double)ts.tv_nsec / 1000000000.0);
}

void DNSSenderThread::runWithRateLimit()
{
    struct timespec ts;
    ppluint64       total_timeslices = runtime * 1000 / (Timeslice * 1000.0);
    ppluint64       queries_rest     = runtime * queryrate;
    ppl7::SockAddr  addr             = Socket.getSockAddr();
    verbose                          = true;
    if (verbose) {
        //printf ("qps=%d, runtime=%d\n",queryrate, runtime);
        printf("runtime: %d s, timeslice: %0.6f s, total timeslices: %llu, Qpts: %llu, Source: %s:%d\n",
            runtime, Timeslice, total_timeslices,
            queries_rest / total_timeslices,
            (const char*)addr.toIPAddress().toString(), addr.port());
    }
    double now            = getNsec();
    double next_timeslice = now;
    double next_checktime = now + 0.1;

    double start      = ppl7::GetMicrotime();
    double end        = start + (double)runtime;
    double total_idle = 0.0;

    for (ppluint64 z = 0; z < total_timeslices; z++) {
        next_timeslice += Timeslice;
        ppluint64 timeslices_rest       = total_timeslices - z;
        ppluint64 queries_per_timeslice = queries_rest / timeslices_rest;
        if (timeslices_rest == 1)
            queries_per_timeslice = queries_rest;
        for (ppluint64 i = 0; i < queries_per_timeslice; i++) {
            sendPacket();
        }

        queries_rest -= queries_per_timeslice;
        while ((now = getNsec()) < next_timeslice) {
            if (now < next_timeslice) {
                total_idle += next_timeslice - now;
                ts.tv_sec  = 0;
                ts.tv_nsec = (next_timeslice - now) * 1000000000;
                nanosleep(&ts, NULL);
            }
        }
        if (now > next_checktime) {
            next_checktime = now + 0.1;
            if (this->threadShouldStop())
                break;
            if (ppl7::GetMicrotime() >= end)
                break;
            //printf ("Zeitscheiben rest: %llu\n", z);
        }
    }
    if (verbose) {
        //printf ("total idle: %0.6f\n",total_idle);
    }
}

void DNSSenderThread::waitForTimeout()
{
    double start = ppl7::GetMicrotime();
    double end   = start + (double)timeout;
    double now, next_checktime = start + 0.1;
    while ((now = ppl7::GetMicrotime()) < end) {
        if (now > next_checktime) {
            next_checktime = now + 0.1;
            if (this->threadShouldStop())
                break;
        }
        ppl7::MSleep(10);
    }
}

ppluint64 DNSSenderThread::getPacketsSend() const
{
    return counter_packets_send;
}

ppluint64 DNSSenderThread::getBytesSend() const
{
    return counter_bytes_send;
}

ppluint64 DNSSenderThread::getErrors() const
{
    return errors;
}

ppluint64 DNSSenderThread::getCounter0Bytes() const
{
    return counter_0bytes;
}

ppluint64 DNSSenderThread::getCounterErrorCode(int err) const
{
    if (err < 255)
        return counter_errorcodes[err];
    return 0;
}
