/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
   It is considered beerware. Prost. Skol. Cheers or whatever.

   $id$ */

/* System */
#include <arpa/inet.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Libowfat */
#include "io.h"
#include "ip6.h"
#include "socket.h"

/* Opentracker */
#include "ot_rijndael.h"
#include "ot_stats.h"
#include "ot_udp.h"
#include "trackerlogic.h"

#if 0
static const uint8_t g_static_connid[8] = { 0x23, 0x42, 0x05, 0x17, 0xde, 0x41, 0x50, 0xff };
#endif
static uint32_t g_rijndael_round_key[44] = {0};
static uint32_t g_key_of_the_hour[2]     = {0};
static ot_time  g_hour_of_the_key;

static void     udp_generate_rijndael_round_key() {
  uint32_t key[16];
#ifdef WANT_ARC4RANDOM
  arc4random_buf(&key[0], sizeof(key));
#else
  key[0] = random();
  key[1] = random();
  key[2] = random();
  key[3] = random();
#endif
  rijndaelKeySetupEnc128(g_rijndael_round_key, (uint8_t *)key);

#ifdef WANT_ARC4RANDOM
  g_key_of_the_hour[0] = arc4random();
#else
  g_key_of_the_hour[0] = random();
#endif
  g_hour_of_the_key = g_now_minutes;
}

/* Generate current and previous connection id for ip */
static void udp_make_connectionid(uint32_t connid[2], const ot_ip6 remoteip, int age) {
  uint32_t plain[4], crypt[4];
  int      i;
  if (g_now_minutes + 60 > g_hour_of_the_key) {
    g_hour_of_the_key    = g_now_minutes;
    g_key_of_the_hour[1] = g_key_of_the_hour[0];
#ifdef WANT_ARC4RANDOM
    g_key_of_the_hour[0] = arc4random();
#else
    g_key_of_the_hour[0] = random();
#endif
  }

  memcpy(plain, remoteip, sizeof(plain));
  for (i = 0; i < 4; ++i)
    plain[i] ^= g_key_of_the_hour[age];
  rijndaelEncrypt128(g_rijndael_round_key, (uint8_t *)remoteip, (uint8_t *)crypt);
  connid[0] = crypt[0] ^ crypt[1];
  connid[1] = crypt[2] ^ crypt[3];
}

/* UDP implementation according to http://xbtt.sourceforge.net/udp_tracker_protocol.html */
int handle_udp6(int64 serversocket, struct ot_workstruct *ws) {
  ot_ip6    remoteip;
  uint32_t *inpacket  = (uint32_t *)ws->inbuf;
  uint32_t *outpacket = (uint32_t *)ws->outbuf;
  uint32_t  left, event, scopeid;
  uint32_t  connid[2];
  uint32_t  action;
  uint16_t  port, remoteport;
  size_t    byte_count, scrape_count;

  byte_count = socket_recv6(serversocket, ws->inbuf, G_INBUF_SIZE, remoteip, &remoteport, &scopeid);
  if (!byte_count)
    return 0;

  stats_issue_event(EVENT_ACCEPT, FLAG_UDP, (uintptr_t)remoteip);
  stats_issue_event(EVENT_READ, FLAG_UDP, byte_count);

  /* Minimum udp tracker packet size, also catches error */
  if (byte_count < 16)
    return 1;

  /* Get action to take. Ignore error messages and broken packets */
  action = ntohl(inpacket[2]);
  if (action > 2)
    return 1;

  /* Generate the connection id we give out and expect to and from
     the requesting ip address, this prevents udp spoofing */
  udp_make_connectionid(connid, remoteip, 0);

  /* Initialise hash pointer */
  ws->hash    = NULL;
  ws->peer_id = NULL;

  /* If action is not 0 (connect), then we expect the derived
     connection id in first 64 bit */
  if ((action > 0) && (inpacket[0] != connid[0] || inpacket[1] != connid[1])) {
    /* If connection id does not match, try the one that was
       valid in the previous hour. Only if this also does not
       match, return an error packet */
    udp_make_connectionid(connid, remoteip, 1);
    if (inpacket[0] != connid[0] || inpacket[1] != connid[1]) {
      const size_t s = sizeof("Connection ID missmatch.");
      outpacket[0]   = htonl(3);
      outpacket[1]   = inpacket[3];
      memcpy(&outpacket[2], "Connection ID missmatch.", s);
      socket_send6(serversocket, ws->outbuf, 8 + s, remoteip, remoteport, 0);
      stats_issue_event(EVENT_CONNID_MISSMATCH, FLAG_UDP, 8 + s);
      return 1;
    }
  }

  switch (action) {
  case 0: /* This is a connect action */
    /* look for udp bittorrent magic id */
    if ((ntohl(inpacket[0]) != 0x00000417) || (ntohl(inpacket[1]) != 0x27101980))
      return 1;

    outpacket[0] = 0;
    outpacket[1] = inpacket[3];
    outpacket[2] = connid[0];
    outpacket[3] = connid[1];

    socket_send6(serversocket, ws->outbuf, 16, remoteip, remoteport, 0);
    stats_issue_event(EVENT_CONNECT, FLAG_UDP, 16);
    break;
  case 1: /* This is an announce action */
    /* Minimum udp announce packet size */
    if (byte_count < 98)
      return 1;

    /* We do only want to know, if it is zero */
    left     = inpacket[64 / 4] | inpacket[68 / 4];

    event    = ntohl(inpacket[80 / 4]);
    port     = *(uint16_t *)(((char *)inpacket) + 96);
    ws->hash = (ot_hash *)(((char *)inpacket) + 16);

    OT_SETIP(ws->peer, remoteip);
    OT_SETPORT(ws->peer, &port);
    OT_PEERFLAG(ws->peer) = 0;

    switch (event) {
    case 1:
      OT_PEERFLAG(ws->peer) |= PEER_FLAG_COMPLETED;
      break;
    case 3:
      OT_PEERFLAG(ws->peer) |= PEER_FLAG_STOPPED;
      break;
    default:
      break;
    }

    if (!left)
      OT_PEERFLAG(ws->peer) |= PEER_FLAG_SEEDING;

    outpacket[0] = htonl(1); /* announce action */
    outpacket[1] = inpacket[12 / 4];

    if (OT_PEERFLAG(ws->peer) & PEER_FLAG_STOPPED) { /* Peer is gone. */
      ws->reply      = ws->outbuf;
      ws->reply_size = remove_peer_from_torrent(FLAG_UDP, ws);
    } else {
      /* Limit amount of peers to OT_MAX_PEERS_UDP */
      uint32_t numwant   = ntohl(inpacket[92 / 4]);
      size_t   max_peers = ip6_isv4mapped(remoteip) ? OT_MAX_PEERS_UDP4 : OT_MAX_PEERS_UDP6;
      if (numwant > max_peers)
        numwant = 400;

      ws->reply      = ws->outbuf + 8;
      ws->reply_size = 8 + add_peer_to_torrent_and_return_peers(FLAG_UDP, ws, numwant);
    }

    socket_send6(serversocket, ws->outbuf, ws->reply_size, remoteip, remoteport, 0);
    stats_issue_event(EVENT_ANNOUNCE, FLAG_UDP, ws->reply_size);
    break;

  case 2:                    /* This is a scrape action */
    outpacket[0] = htonl(2); /* scrape action */
    outpacket[1] = inpacket[12 / 4];

    for (scrape_count = 0; (scrape_count * 20 < byte_count - 16) && (scrape_count <= 74); scrape_count++)
      return_udp_scrape_for_torrent(*(ot_hash *)(((char *)inpacket) + 16 + 20 * scrape_count), ((char *)outpacket) + 8 + 12 * scrape_count);

    socket_send6(serversocket, ws->outbuf, 8 + 12 * scrape_count, remoteip, remoteport, 0);
    stats_issue_event(EVENT_SCRAPE, FLAG_UDP, scrape_count);
    break;
  }
  return 1;
}

static void *udp_worker(void *args) {
  int64                sock = (int64)args;
  struct ot_workstruct ws;
  memset(&ws, 0, sizeof(ws));

  ws.inbuf  = malloc(G_INBUF_SIZE);
  ws.outbuf = malloc(G_OUTBUF_SIZE);
#ifdef _DEBUG_HTTPERROR
  ws.debugbuf = malloc(G_DEBUGBUF_SIZE);
#endif

  while (g_opentracker_running)
    handle_udp6(sock, &ws);

  free(ws.inbuf);
  free(ws.outbuf);
#ifdef _DEBUG_HTTPERROR
  free(ws.debugbuf);
#endif
  return NULL;
}

void udp_init(int64 sock, unsigned int worker_count) {
  pthread_t thread_id;
  if (!g_rijndael_round_key[0])
    udp_generate_rijndael_round_key();
#ifdef _DEBUG
  fprintf(stderr, " installing %d workers on udp socket %ld\n", worker_count, (unsigned long)sock);
#endif
  while (worker_count--)
    pthread_create(&thread_id, NULL, udp_worker, (void *)sock);
}
