/**
    interface to network services, including SOCKS, asynchronous DNS resolver and uPNP

    Copyright (c) 2020-2021 The Creators of Simphone

    See the file COPYING.LESSER.txt for copying permission.
**/

#include "config.h"
#include "spth.h"

#include "logger.h"
#include "table.h"
#include "error.h"
#include "utils.h"
#include "system.h"
#include "crypto.h"
#include "sssl.h"
#include "socket.h"
#include "network.h"
#include "mainline.h"
#include "param.h"
#include "proto.h"
#include "server.h"
#include "client.h"
#include "api.h"

#define SIM_MODULE "network"

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>

#ifndef _WIN32
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#ifndef INADDR_NONE
#define INADDR_NONE 0xFFFFFFFF
#endif
#endif

char *network_convert_ip (unsigned ip) {
  struct in_addr in;
  in.s_addr = htonl (ip);
  return inet_ntoa (in);
}

#define network_check_stopped() (simself.state == CLIENT_STATE_STOPPED)

simbool sim_network_check_local (unsigned ip) {
  static const unsigned network_reserved_ips[][2] = {
    { 0x00000000, 0x00FFFFFF }, /*      0.0.0.0 - 0.255.255.255   */
    { 0x0A000000, 0x0AFFFFFF }, /*     10.0.0.0 - 10.255.255.255  */
    { 0x64400000, 0x647FFFFF }, /*   100.64.0.0 - 100.127.255.255 */
    { 0x7F000000, 0x7FFFFFFF }, /*    127.0.0.0 - 127.255.255.255 */
    { 0xA9FE0000, 0xA9FEFFFF }, /*  169.254.0.0 - 169.254.255.255 */
    { 0xAC100000, 0xAC1FFFFF }, /*   172.16.0.0 - 172.31.255.255  */
    { 0xC0000000, 0xC00000FF }, /*    192.0.0.0 - 192.0.0.255     */
    { 0xC0000200, 0xC00002FF }, /*    192.0.2.0 - 192.0.2.255     */
    { 0xC0586300, 0xC05863FF }, /*  192.88.99.0 - 192.88.99.255   */
    { 0xC0A80000, 0xC0A8FFFF }, /*  192.168.0.0 - 192.168.255.255 */
    { 0xC6120000, 0xC613FFFF }, /*   198.18.0.0 - 198.19.255.255  */
    { 0xC6336400, 0xC63364FF }, /* 198.51.100.0 - 198.51.100.255  */
    { 0xCB007100, 0xCB0071FF }, /*  203.0.113.0 - 203.0.113.255   */
    { 0xE0000000, 0xFFFFFFFF }  /*    224.0.0.0 - 255.255.255.255 */
  };
  unsigned i;
  for (i = 0; i < SIM_ARRAY_SIZE (network_reserved_ips); i++)
    if (ip >= network_reserved_ips[i][0] && ip <= network_reserved_ips[i][1])
      return true;
  return false;
}

unsigned sim_network_get_default (void) {
  unsigned ip = 0;
  int fd = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP);
  if (fd >= 0) {
    struct sockaddr_in sin;
    socklen_t len = sizeof (sin);
    memset (&sin, 0, sizeof (sin));
    sin.sin_family = AF_INET, sin.sin_addr.s_addr = htonl (0x8080808), sin.sin_port = htons (53);
    if (! connect (fd, (struct sockaddr *) &sin, sizeof (sin)))
      if (! getsockname (fd, (struct sockaddr *) &sin, &len))
        ip = ntohl (sin.sin_addr.s_addr);
    close_socket (fd);
  }
  return ip;
}

unsigned sim_network_parse_ip_default (const char *ipstr, unsigned *ip) {
  unsigned mainip = ipstr ? ntohl (inet_addr (ipstr)) : 0;
  if (mainip == INADDR_NONE)
    mainip = 0;
  if (ip) {
    if (! mainip) {
      /* this prevents intranet address from leaking to the internet DHT.
         such a leak would cause multiple ips to be found for the same node */
      *ip = mainip = sim_network_get_default ();
    } else
      *ip = 0;
  }
  return mainip;
}

simtype sim_network_parse_host_port (const char *hostport, int *port) {
  char *s = hostport ? strchr (hostport, ':') : NULL;
  if (port)
    *port = s ? atoi (s + 1) : 0;
  return s ? string_copy_len (hostport, s - hostport) : nil ();
}

unsigned sim_network_parse_ip_port (const char *ipport, int *port) {
  simtype ipstr = sim_network_parse_host_port (ipport, port);
  unsigned ip = sim_network_parse_ip (port || ipstr.typ != SIMNIL ? ipstr.ptr : ipport);
  string_free (ipstr);
  return ip;
}

int sim_network_parse_ips_ports (const simtype arrayip, const simtype arrayport, unsigned length,
                                 unsigned *ips, int *ports) {
  int ok = 1;
  unsigned i, j;
  if (ips) {
    memset (ips, 0, sizeof (*ips) * length);
    for (i = 0; i < arrayip.len && i < length; i++)
      ips[i] = sim_network_parse_ip (arrayip.arr[i + 1].ptr);
  } else
    i = 0;
  if (ports) {
    memset (ports, 0, sizeof (*ports) * length);
    for (j = 0; j < arrayport.len && j < length; j++)
      if (arrayport.arr[j + 1].num > 0 && arrayport.arr[j + 1].num < 0x10000) {
        ports[j] = (int) arrayport.arr[j + 1].num;
      } else
        ok = -1;
  } else
    j = 0;
  return ok * (int) (i > j ? i : j);
}

int network_connect_socks_ (simsocket sock, unsigned *ip, int port, const char *host, int command) {
  int err;
  unsigned len = host ? 9 + strlen (host) + 1 : 9;
  struct { /* SOCKS4a header */
    simbyte version, command;
    short port;
    unsigned ip;
    char userid;
  } *header = sim_new (len);
  header->version = 4;
  header->command = (simbyte) command;
  header->port = htons (port);
  header->ip = htonl (*ip);
  header->userid = 0;
  if (host) {
    header->ip = htonl (1);
    strcpy (&header->userid + 1, host);
  }
  if ((err = socket_send_all_ (sock, (simbyte *) header, len)) == SIM_OK) {
    int rcvtimeout = sock->rcvtimeout, timeout;
    for (timeout = param_get_number_default ("socket.socks", rcvtimeout * 2); timeout > 0; timeout -= rcvtimeout) {
      if (timeout < rcvtimeout)
        sock->rcvtimeout = timeout;
      if ((err = socket_recv_all_ (sock, SOCKET_RECV_TCP, pointer_new_len (header, 8))) != SIM_SOCKET_RECV_TIMEOUT)
        break;
    }
    sock->rcvtimeout = rcvtimeout;
    if (err != SIM_OK) {
      LOG_WARN_ ("socks $%d recv %d\n", sock->fd, err);
    } else if (header->version || header->command != 90) {
      LOG_DEBUG_ ("socks $%d error %d\n", sock->fd, header->command);
      err = SIM_SOCKET_CONNECT;
    } else if (host)
      *ip = ntohl (header->ip);
  } else
    LOG_ERROR_ ("socks $%d send %d\n", sock->fd, err);
  sim_free (header, len);
  return err;
}

#ifndef _WIN32
#include <netdb.h>
#endif

#if HAVE_LIBCARES
#include <ares.h>

static struct in_addr *network_dns_new (const simtype array, int *length) {
  unsigned i, n = 0;
  struct in_addr *servers = sim_new (sizeof (*servers) * array.len);
  for (i = 1; i <= array.len; i++) {
    unsigned ip = inet_addr (array.arr[i].ptr);
    if (ip != INADDR_NONE)
      servers[n++].s_addr = ip;
  }
  *length = n;
  return servers;
}
#else
#define ARES_SUCCESS 0
#define ARES_ENONAME 1
#endif

static void network_dns_callback (void *arg, int status, int timeouts, struct hostent *host) {
  simtype *ips = arg;
  if (status == ARES_SUCCESS) {
    int i, n = 0;
    char **p = host->h_addr_list;
    while (*p++)
      ++n;
    if (n) {
      *ips = array_new_numbers (n);
      p = host->h_addr_list;
      for (i = 1; i <= n; i++)
        ips->arr[i] = number_new (ntohl (*(unsigned *) *p++));
    } else
      *ips = number_new (ARES_ENONAME); /* impossible */
  } else
    *ips = number_new (status);
}

int network_dns_resolve_ (simsocket sock, int seconds, const char *host, int port, simbool dht, simtype *ips) {
  int dns = dht ? param_get_number ("net.dns.dht") : param_get_number ("net.dns.alt");
  *ips = nil ();
  if (dns > 0 && param_get_number ("net.tor.port") > 0) {
    struct _socket tmp;
    int err = host ? socket_open (&tmp, SOCKET_NO_SESSION) : SIM_NO_ERROR;
    if (err == SIM_OK) {
      unsigned ip = 0;
      err = socket_connect_socks_ (&tmp, NULL, &ip, port, host, 0xF0, false); /* TOR only - normal socks4a won't work */
      SOCKET_INIT_STAT (&tmp);
      socket_close (&tmp, NULL);
      if (err == SIM_OK) {
        *ips = array_new_type (number_new (ip));
        simself.flags |= SIM_STATUS_FLAG_DNS;
        LOG_CODE_DEBUG_ (network_log_flags (SIM_MODULE, SIM_LOG_DEBUG));
      }
    }
    return err;
  } else {
#if HAVE_LIBCARES
    static const int network_dns_options[] = {
      0, ARES_OPT_TRIES | ARES_OPT_TIMEOUTMS, ARES_OPT_TRIES | ARES_OPT_TIMEOUTMS | ARES_OPT_SERVERS
    };
    int err, nfds;
    if ((err = ares_library_init (ARES_LIB_INIT_ALL)) == ARES_SUCCESS) {
      ares_channel channel;
      struct ares_options options;
      simtype array = param_get_strings ("net.dns.servers");
      options.servers = network_dns_new (array, &options.nservers);
      options.tries = param_get_number ("net.dns.retry");
      options.timeout = param_get_number ("net.dns.timeout");
      if ((dns = abs (dns)) == 3) {
        options.nservers = 0;
        dns = 1;
      }
      err = ARES_ECONNREFUSED; /* no DNS servers */
      if (dns)
        while (dns < (options.nservers ? 3 : 2))
          if ((err = ares_init_options (&channel, &options, network_dns_options[dns++])) == ARES_SUCCESS) {
            simbool done = false;
            ares_gethostbyname (channel, host, AF_INET, network_dns_callback, ips);
            while (! done && seconds >= 0) {
              struct timeval timeout, tv;
              timeout.tv_sec = seconds, timeout.tv_usec = 0;
              ares_timeout (channel, seconds ? &timeout : NULL, &timeout);
              if (seconds && (timeout.tv_sec || timeout.tv_usec))
                if ((seconds -= timeout.tv_sec + (timeout.tv_usec != 0)) <= 0)
                  seconds = -1;
              while (! done && (int) timeout.tv_sec-- >= 0) {
                fd_set *rset, *wset;
                unsigned size = sizeof (*rset);
#if ! HAVE_LIBPTH && ! defined(_WIN32)
                size *= socket_max_limit[SOCKET_MAX_SOCKETS] / FD_SETSIZE + 1;
#endif
                memset (rset = sim_new (size), 0, size);
                memset (wset = sim_new (size), 0, size);
                FD_ZERO (rset);
                FD_ZERO (wset);
                if ((sock && sock->err != SIM_OK) || network_check_stopped ())
                  ares_cancel (channel);
                tv.tv_sec = (int) timeout.tv_sec >= 0, tv.tv_usec = tv.tv_sec ? 0 : timeout.tv_usec;
                if ((nfds = ares_fds (channel, rset, wset)) == 0) {
                  done = true;
                } else if (pth_select_ (nfds, rset, wset, NULL, &tv) >= 0) {
                  ares_process (channel, rset, wset);
#ifndef _WIN32
                } else if (errno == EINTR) {
                  timeout.tv_sec++;
#endif
                } else {
                  err = ERROR_BASE_CARES - socket_get_errno ();
                  done = true;
                }
                sim_free (rset, size);
                sim_free (wset, size);
              }
            }
            ares_destroy (channel);
            if (ips->typ == SIMARRAY_NUMBER) {
              err = ARES_SUCCESS;
              break;
            }
            if (err == ARES_SUCCESS && ips->typ == SIMNUMBER)
              err = (int) ips->num;
            LOG_DEBUG_ ("resolve $%d %s error %d\n", sock ? sock->fd : -1, host, err);
            if ((sock && sock->err != SIM_OK) || network_check_stopped ()) {
              err = ERROR_BASE_CARES - (! sock || sock->err == SIM_OK ? SIM_SOCKET_CANCELLED : sock->err);
              break;
            }
          }
      sim_free (options.servers, sizeof (*options.servers) * array.len);
      ares_library_cleanup ();
      if (ips->typ == SIMNUMBER)
        *ips = nil ();
    }
    if (err != ARES_SUCCESS)
      return ERROR_BASE_CARES - err;
#else
    struct hostent *h;
    PTH_UNPROTECT_PROTECT_ (h = gethostbyname (host));
    if (! h)
      return ERROR_BASE_CARES - h_errno;
    network_dns_callback (ips, ARES_SUCCESS, 0, h);
    if (ips->typ == SIMNUMBER)
      *ips = nil ();
#endif /* HAVE_LIBCARES */
    simself.flags |= SIM_STATUS_FLAG_DNS;
    LOG_CODE_DEBUG_ (network_log_flags (SIM_MODULE, SIM_LOG_DEBUG));
    return SIM_OK;
  }
}

#ifdef HAVE_LIBMINIUPNPC

#if HAVE_LIBMINIUPNPC
#include <miniupnpc/miniupnpc.h>
#include <miniupnpc/upnpcommands.h>
#include <miniupnpc/upnperrors.h>
#else
#include <miniupnpc/include/miniupnpc.h>
#include <miniupnpc/include/upnpcommands.h>
#include <miniupnpc/include/upnperrors.h>
#endif

/* return codes of network_upnp_get_ */
#define NETWORK_UPNP_GET_NO_HTTP (-3) /* no mapping of this port exists */
#define NETWORK_UPNP_GET_ERROR (-2)   /* no mapping of this port exists */
#define NETWORK_UPNP_GET_DELETE (-1)  /* overwrite different mapping of this port */
#define NETWORK_UPNP_GET_OK 0         /* identical mapping of this port already exists */
#define NETWORK_UPNP_GET_NOK 1        /* different mapping of this port should not be overwritten */

struct network_upnp_queue { /* uPNP queue item */
  pth_message_t header;     /* message queue item */
  int cmd;                  /* port number (zero to delete port mapping) or NETWORK_CMD_xxx */
};

static const char *network_upnp_command_names[] = { "retry", "restart", "start", "get", "stop", "unset", "set" };

static struct miniupnpc {
  int connected; /* the rest are valid only if connected >= 1 */
  struct UPNPUrls urls;
  struct IGDdatas data;
  char lanip[40];
} network_upnp;

static pth_t tid_upnp = NULL;
static pth_msgport_t network_upnp_queue = NULL;
static pth_event_t network_upnp_event = NULL;

static simnumber network_upnp_tick = 0, network_upnp_ip_tick = 0;
static unsigned network_upnp_ip = 0, network_upnp_old_ip = 0;
static int network_upnp_port = 0, network_upnp_https = 0, network_upnp_udp = 0;
static int network_upnp_local_port = 0, network_upnp_https_port = 0;

int network_set_port (int port) {
  int err = SIM_NO_ERROR;
  if (port < NETWORK_CMD_RETRY || port >= 0x10000) {
    err = SIM_SOCKET_BAD_PORT;
  } else if (network_upnp_queue) {
    struct network_upnp_queue *item = sim_new (sizeof (*item));
    item->cmd = port;
    if (port == NETWORK_CMD_IP) {
      network_upnp_ip_tick = system_get_tick ();
    } else
      network_upnp_tick = system_get_tick ();
    if ((err = pth_queue_put (network_upnp_queue, &item->header)) != SIM_OK)
      sim_free (item, sizeof (*item));
  } else if (port >= 0)
    network_upnp_port = port;
  return err;
}

int network_get_port (unsigned *ip) {
  if (ip)
    *ip = network_upnp_ip;
  return simself.flags & SIM_STATUS_FLAG_UPNP ? network_upnp_port : 0;
}

int network_get_ip (void) {
  int err = SIM_OK, timeout = param_get_number ("net.upnp.iptime");
  if (! pth_msgport_pending (network_upnp_queue))
    if (! network_upnp_ip || ! timeout || (simnumber) system_get_tick () - network_upnp_ip_tick >= timeout * 1000)
      err = network_set_port (NETWORK_CMD_IP);
  return err;
}

static void log_upnp_service_ (const struct IGDdatas_service *service, const char *name, const char *url) {
  if (url)
    LOG_DEBUG_ ("upnp IGD %s %s\n", name, url);
  if (*service->controlurl)
    LOG_DEBUG_ ("upnp IGD %s control %s\n", name, service->controlurl);
  if (*service->eventsuburl)
    LOG_DEBUG_ ("upnp IGD %s event %s\n", name, service->eventsuburl);
  if (*service->scpdurl)
    LOG_DEBUG_ ("upnp IGD %s scpd %s\n", name, service->scpdurl);
  if (*service->servicetype)
    LOG_DEBUG_ ("upnp IGD %s service %s\n", name, service->servicetype);
}

static void network_upnp_init_ (struct miniupnpc *upnp) {
  struct UPNPDev *devices = (void *) -1, *device;
  simtype ipstr = param_get_string ("net.upnp.ip"), url = string_copy (param_get_pointer ("net.upnp.url"));
  simtype path = string_copy (param_get_pointer ("net.upnp.path")), lanip;
  int port = param_get_number ("nat.upnp.port"), ttl = param_get_number ("net.upnp.ttl");
  int delay = param_get_number ("net.upnp.delay"), connected = 0;
  if (! ipstr.len) {
    unsigned ip, mainip = sim_network_parse_ip_default (param_get_pointer ("main.ip"), &ip);
    ipstr = string_copy (network_convert_ip (ip ? ip : mainip));
  } else /* this ignores any interface name and allows only for an ip address in net.upnp.ip */
    ipstr = sim_network_parse_ip (ipstr.ptr) ? string_copy_string (ipstr) : nil ();
  memset (&upnp->urls, 0, sizeof (upnp->urls)); /* for debug logging */
  memset (&upnp->data, 0, sizeof (upnp->data));
  memset (upnp->lanip, 0, sizeof (upnp->lanip)); /* fix badly used strncpy in upnpDiscover */
  errno = PTH_UNPROTECT (0);
  socket_set_errno (0);
  if (! url.len) {
    int ret;
#if MINIUPNPC_API_VERSION >= 14
    devices = upnpDiscover (delay, ipstr.ptr, path.ptr, port, false, (simbyte) ttl, &ret);
#else
    devices = upnpDiscover (delay, ipstr.ptr, path.ptr, port, false, &ret);
#endif
    if (PTH_PROTECT_ (ret) != UPNPDISCOVER_SUCCESS)
      LOG_WARN_ ("upnp discover failed %d (errno = %d:%d)\n", ret, socket_get_errno (), errno);
    for (device = devices; device; device = device->pNext) {
      LOG_INFO_ ("upnp device %s\n", device->descURL);
#if MINIUPNPC_API_VERSION >= 9
      LOG_DEBUG_ ("upnp service %u %s\n", device->scope_id, device->st);
#endif
#if MINIUPNPC_API_VERSION >= 14
      LOG_DEBUG_ ("upnp usn %s\n", device->usn);
#endif
    }
    if (! devices)
      LOG_INFO_ ("upnp device\n");
    errno = PTH_UNPROTECT (0);
    socket_set_errno (0);
    if (! network_check_stopped ())
      connected = UPNP_GetValidIGD (devices, &upnp->urls, &upnp->data, upnp->lanip, sizeof (upnp->lanip) - 1);
    freeUPNPDevlist (devices);
  } else
    connected = UPNP_GetIGDFromUrl (url.ptr, &upnp->urls, &upnp->data, upnp->lanip, sizeof (upnp->lanip) - 1);
  upnp->connected = PTH_PROTECT_ (connected);
  if (devices) {
    if (connected != 1)
      LOG_NOTE_ ("upnp IGD failed %d (errno = %d:%d)\n", connected, socket_get_errno (), errno);
    LOG_INFO_ ("upnp IGD lanaddr %s -> %s\n", upnp->lanip, upnp->urls.controlURL);
#if MINIUPNPC_API_VERSION >= 8
    LOG_DEBUG_ ("upnp IGD url %s\n", upnp->urls.rootdescURL);
#endif
    LOG_DEBUG_ ("upnp IGD presentation %s\n", upnp->data.presentationurl);
    log_upnp_service_ (&upnp->data.first, "WANIPConnection", upnp->urls.ipcondescURL);
    log_upnp_service_ (&upnp->data.second, "WANPPPConnection", NULL);
    log_upnp_service_ (&upnp->data.CIF, "WANCommonInterfaceConfig", upnp->urls.controlURL_CIF);
    log_upnp_service_ (&upnp->data.IPv6FC, "WANIPv6FirewallControl", upnp->urls.controlURL_6FC);
  }
  lanip = param_get_string ("net.upnp.lanaddr");
  if (sim_network_parse_ip (lanip.len ? lanip.ptr : ipstr.ptr)) {
    memset (upnp->lanip, 0, sizeof (upnp->lanip));
    strncpy (upnp->lanip, lanip.len ? lanip.ptr : ipstr.ptr, sizeof (upnp->lanip) - 1);
  }
  LOG_INFO_ ("upnp lanaddr %s\n", upnp->lanip);
  string_free (path);
  string_free (url);
  string_free (ipstr);
}

static void network_upnp_uninit (struct miniupnpc *upnp) {
  if (upnp->connected >= 1)
    FreeUPNPUrls (&upnp->urls);
  upnp->connected = 0;
}

static unsigned network_upnp_get_ip_ (void) {
  int ret;
  unsigned ip = 0;
  char externalip[40];
  errno = PTH_UNPROTECT (0);
  socket_set_errno (0);
  ret = UPNP_GetExternalIPAddress (network_upnp.urls.controlURL, network_upnp.data.first.servicetype, externalip);
  if (PTH_PROTECT_ (ret) == UPNPCOMMAND_SUCCESS) {
    LOG_INFO_ ("upnp ip %s\n", externalip);
    ip = sim_network_parse_ip (externalip);
  } else
    LOG_NOTE_ ("upnp ip failed %d (errno = %d:%d): %s\n", ret, socket_get_errno (), errno, strupnperror (ret));
  network_upnp_ip_tick = system_get_tick ();
  return ip;
}

static int network_upnp_get_ (int level, int port, int newport, const char *protocol, const char *description) {
  int ret;
  char newportstr[12], setport[6], setip[40], setdescription[80];
  sprintf (newportstr, "%d", newport);
  errno = PTH_UNPROTECT (0);
  socket_set_errno (0);
  ret = UPNP_GetSpecificPortMappingEntry (network_upnp.urls.controlURL, network_upnp.data.first.servicetype,
                                          newportstr, protocol,
#if MINIUPNPC_API_VERSION >= 10
                                          NULL,
#endif
                                          setip, setport, setdescription, NULL, NULL);
  if (PTH_PROTECT_ (ret) != UPNPCOMMAND_SUCCESS) {
#if SIM_LOG_LEVEL <= SIM_LOG_NOTE
    LOG_ANY_ (level, "upnp get %s %d failed %d (errno = %d:%d): %s\n",
              protocol, newport, ret, socket_get_errno (), errno, strupnperror (ret));
#endif
    return ret == UPNPCOMMAND_HTTP_ERROR ? NETWORK_UPNP_GET_NO_HTTP : NETWORK_UPNP_GET_ERROR;
  }
  ret = strcmp (setip, network_upnp.lanip) || atoi (setport) != port ? NETWORK_UPNP_GET_NOK : NETWORK_UPNP_GET_OK;
  if (*description) {
    if (strcmp (setdescription, description)) {
      ret = NETWORK_UPNP_GET_NOK;
    } else if (ret != NETWORK_UPNP_GET_OK)
      ret = NETWORK_UPNP_GET_DELETE;
  }
  LOG_DEBUG_ ("upnp get %s %d:%d -> %s:%s %s '%s'\n", protocol, newport, port, setip, setport,
              ret == NETWORK_UPNP_GET_DELETE ? "del" : ret == NETWORK_UPNP_GET_OK ? "ok" : "nok", setdescription);
  return ret;
}

static int network_upnp_set_ (int port, int newport, const char *protocol, const char *description) {
  int ret;
  char portstr[12], newportstr[12], protocoldescription[80];
  sprintf (newportstr, "%d", newport);
  errno = PTH_UNPROTECT (0);
  socket_set_errno (0);
  if (description) {
    sprintf (portstr, "%d", port);
    if (! *description)
      sprintf (protocoldescription, "BitTorrent (%s)", protocol);
    ret = UPNP_AddPortMapping (network_upnp.urls.controlURL, network_upnp.data.first.servicetype, newportstr, portstr,
                               network_upnp.lanip, *description ? description : protocoldescription, protocol, 0, NULL);
  } else
    ret = UPNP_DeletePortMapping (network_upnp.urls.controlURL, network_upnp.data.first.servicetype, newportstr,
                                  protocol, NULL);
  if (PTH_PROTECT_ (ret) != UPNPCOMMAND_SUCCESS) {
    LOG_NOTE_ ("upnp %s %s %d failed %d (errno = %d:%d): %s\n",
               description ? "add" : "delete", protocol, newport, ret, socket_get_errno (), errno, strupnperror (ret));
  } else if (description) {
    LOG_DEBUG_ ("upnp add %s %d -> %d '%s'\n", protocol, newport, port, description);
  } else
    LOG_DEBUG_ ("upnp delete %s %d\n", protocol, newport);
  return ret;
}

static int network_upnp_set_port_ (int *setport, int port, int newport, const char *protocol, const char *description) {
  int ret = NETWORK_UPNP_GET_NOK;
  if (! network_check_stopped ())
    ret = network_upnp_get_ (SIM_LOG_DEBUG, port, newport, protocol, description);
  if (ret != NETWORK_UPNP_GET_NOK) {
    if (ret == NETWORK_UPNP_GET_DELETE && ! network_check_stopped ())
      network_upnp_set_ (0, newport, protocol, NULL);
    if (! network_check_stopped () && network_upnp_set_ (port, newport, protocol, description) == UPNPCOMMAND_SUCCESS)
      if (! *setport)
        *setport = newport;
    ret = NETWORK_UPNP_GET_NOK;
    if (! network_check_stopped ())
      ret = network_upnp_get_ (SIM_LOG_NOTE, port, newport, protocol, description);
  }
  return ret;
}

static simbool network_upnp_unset_port_ (int port, int newport, const char *protocol, const char *description,
                                         simbool noquit) {
  if (! network_check_stopped () || noquit) {
    int ret = network_upnp_get_ (SIM_LOG_NOTE, port, newport, protocol, description);
    if (ret == NETWORK_UPNP_GET_NO_HTTP)
      noquit = false;
    if (ret != NETWORK_UPNP_GET_NOK && (! network_check_stopped () || noquit))
      if (network_upnp_set_ (0, newport, protocol, NULL) == UPNPCOMMAND_HTTP_ERROR)
        noquit = false;
  }
  return noquit;
}

static void network_upnp_reset_ (int port, int httpsport, const char *description) {
  if (network_upnp.connected >= 1) {
    simbool noquit = ! port;
    if (network_upnp_port && network_upnp_port != port && network_upnp_local_port)
      noquit = network_upnp_unset_port_ (network_upnp_local_port, network_upnp_port, "TCP", description, noquit);
    if (network_upnp_https && (! port || network_upnp_https_port != httpsport) && network_upnp_https_port)
      if (network_upnp_https != network_upnp_port || network_upnp_https_port != network_upnp_local_port)
        noquit = network_upnp_unset_port_ (network_upnp_https_port, network_upnp_https, "TCP", description, noquit);
    if (network_upnp_udp && network_upnp_udp != port)
      network_upnp_unset_port_ (network_upnp_udp, network_upnp_udp, "UDP", description, noquit);
  }
  network_upnp_local_port = network_upnp_https = network_upnp_udp = 0;
}

static void *thread_upnp_ (void *arg) {
  simbool done = false;
  LOG_API_DEBUG_ ("%d\n", param_get_number ("net.upnp.time"));
  while (! done && pth_queue_wait_ (network_upnp_event, -1) > 0) {
    struct miniupnpc upnp;
    simtype desc = string_copy (param_get_pointer ("net.upnp.description"));
    struct network_upnp_queue *item = (struct network_upnp_queue *) pth_queue_get (network_upnp_queue);
    int port = item->cmd, cmd = port - NETWORK_CMD_RETRY, setport = 0, restart;
    sim_free (item, sizeof (*item));
    if ((unsigned) cmd >= SIM_ARRAY_SIZE (network_upnp_command_names))
      cmd = SIM_ARRAY_SIZE (network_upnp_command_names) - 1;
    if (port == NETWORK_CMD_START || port == NETWORK_CMD_RESTART || port == NETWORK_CMD_RETRY)
      port = network_upnp_local_port ? network_upnp_local_port : network_upnp_port;
    if (network_check_stopped () && port)
      port = NETWORK_CMD_QUIT;
    LOG_INFO_ ("upnp %s %d\n", network_upnp_command_names[cmd], port);
    cmd = ! cmd ? -1 : cmd + NETWORK_CMD_RETRY != NETWORK_CMD_RESTART;
    switch (port) {
      default:
        network_upnp_init_ (&upnp);
        if (network_upnp.connected < 1 || upnp.connected < 1 ||
            strcmp (upnp.urls.controlURL, network_upnp.urls.controlURL)) {
          network_upnp_reset_ (0, 0, desc.ptr);
          if (network_upnp_ip)
            network_upnp_old_ip = network_upnp_ip;
          network_upnp_ip = 0;
          LOG_INFO_ ("upnp ip RESET %s\n", network_convert_ip (network_upnp_old_ip));
        }
        network_upnp_uninit (&network_upnp);
        memcpy (&network_upnp, &upnp, sizeof (upnp));
      case 0:
        if (network_upnp.connected >= 1) {
          int ret = NETWORK_UPNP_GET_OK, newport = port, retry, retries, step = param_get_number ("net.upnp.step");
          int httpsport = server_get_port () == SIM_PROTO_PORT ? SIM_PROTO_PORT : port;
          network_upnp_reset_ (port, httpsport, desc.ptr);
          if (port) {
            int ret2 = network_upnp_set_port_ (&network_upnp_https, httpsport, SIM_PROTO_PORT, "TCP", desc.ptr);
            network_upnp_https_port = httpsport;
            if ((ret = ret2) == NETWORK_UPNP_GET_OK) {
              network_upnp_https = SIM_PROTO_PORT;
              LOG_INFO_ ("upnp port %d -> %d\n", SIM_PROTO_PORT, network_upnp_https_port);
            }
            if (port != SIM_PROTO_PORT)
              ret = network_upnp_set_port_ (&setport, port, newport, "TCP", desc.ptr);
            if (ret2 == NETWORK_UPNP_GET_OK)
              ret = NETWORK_UPNP_GET_OK;
            ret2 = network_upnp_set_port_ (&network_upnp_udp, port, newport, "UDP", desc.ptr);
            if (ret2 == NETWORK_UPNP_GET_OK) {
              network_upnp_udp = newport;
              LOG_INFO_ ("upnp udp %d\n", newport);
            }
          }
          if (ret != NETWORK_UPNP_GET_OK && step) {
            retry = 2;
            for (retries = param_get_number ("net.upnp.retry"); retries; retries--) {
              for (newport = 0; newport < 1024; retry += 2)
                newport = (unsigned short) (port + step * retry - 1);
              if ((ret = network_upnp_set_port_ (&setport, port, newport, "TCP", desc.ptr)) == NETWORK_UPNP_GET_OK)
                break;
            }
          }
          network_upnp_local_port = port;
          if (ret != NETWORK_UPNP_GET_OK) {
            LOG_NOTE_ ("upnp port = %d\n", setport);
            if (setport)
              port = setport;
          } else
            port = setport = newport;
        }
        simself.flags = setport ? simself.flags | SIM_STATUS_FLAG_UPNP : simself.flags & ~SIM_STATUS_FLAG_UPNP;
        LOG_CODE_DEBUG_ (network_log_flags (SIM_MODULE, SIM_LOG_DEBUG));
        network_upnp_port = port;
        LOG_INFO_ ("upnp port %d -> %d (https = %d -> %d, udp = %d, tcp = %d)\n", port, network_upnp_local_port,
                   network_upnp_https, network_upnp_https_port, network_upnp_udp, setport);
      case NETWORK_CMD_IP:
        if (! network_check_stopped () && port) {
          if (network_upnp.connected >= 1) {
            unsigned ip = network_upnp_ip;
            if ((network_upnp_ip = network_upnp_get_ip_ ()) != 0)
              if ((cmd = network_upnp_ip == ip || network_upnp_ip == network_upnp_old_ip) == 0)
                network_upnp_old_ip = network_upnp_ip;
          } else if (! cmd && ! network_upnp.connected && (restart = param_get_number ("net.upnp.restart")) != 0) {
            do {
              pth_sleep_ (1);
            } while (restart-- && ! network_check_stopped ());
            network_set_port (NETWORK_CMD_RETRY);
            cmd = 1;
          }
        }
        if (cmd <= 0) {
          main_reinit ();
          ssl_reinit_ ();
        }
        if (! network_check_stopped ())
          break;
      case NETWORK_CMD_QUIT:
        network_upnp_uninit (&network_upnp);
        done = true;
    }
    string_free (desc);
  }
  LOG_API_DEBUG_ ("\n");
  return pth_thread_exit_ (true);
}

int network_periodic (int cmd) {
  int err = SIM_NO_ERROR, timeout = param_get_number ("net.upnp.time");
  if (timeout && ! network_check_stopped () && network_upnp_tick)
    if (cmd != NETWORK_CMD_IP || (simnumber) system_get_tick () - network_upnp_tick >= timeout * 1000)
      err = network_set_port (cmd != NETWORK_CMD_IP ? cmd : NETWORK_CMD_START);
  return err;
}

int network_init (void) {
  int err = SIM_OK;
  if (! network_upnp_queue && param_get_number ("net.tor.port") <= 0 && param_get_number ("net.upnp.time"))
    if ((err = pth_queue_new (&network_upnp_queue, &network_upnp_event, -1)) == SIM_OK) {
#if HAVE_LIBPTH
      network_upnp_tick = system_get_tick ();
#else
      err = network_set_port (NETWORK_CMD_START);
#endif
      if (err == SIM_OK) {
        int step = param_get_number ("net.upnp.step"), minstep = param_get_min ("net.upnp.step", 1);
        if (! step) {
          step = (unsigned) random_get_number (random_public, param_get_max ("net.upnp.step", 32768) - minstep + 1);
          param_set_number ("net.upnp.step", minstep + step, SIM_PARAM_PERMANENT);
          if ((err = param_save ()) != SIM_OK)
            event_send_name (NULL, SIM_EVENT_ERROR, SIM_EVENT_ERROR_FILE_SAVE, number_new (err));
        }
        network_upnp.connected = 0;
        err = pth_thread_spawn (thread_upnp_, NULL, &tid_upnp, -1);
      }
      if (err != SIM_OK)
        pth_queue_free (&network_upnp_queue, &network_upnp_event, sizeof (struct network_upnp_queue));
    }
  return err;
}

int network_uninit_ (void) {
  int err = pth_thread_join_ (&tid_upnp, NULL, thread_upnp_, -1);
  if (err == SIM_OK)
    pth_queue_free (&network_upnp_queue, &network_upnp_event, sizeof (struct network_upnp_queue));
  network_upnp_ip_tick = network_upnp_tick = network_upnp_old_ip = network_upnp_ip = network_upnp.connected = 0;
  network_upnp_https_port = network_upnp_local_port = network_upnp_udp = network_upnp_https = network_upnp_port = 0;
  return err;
}

#else

int network_set_port (int port) { return SIM_NO_ERROR; }
int network_get_port (unsigned *ip) { return ip ? (*ip = 0) : 0; }
int network_get_ip (void) { return SIM_OK; }
int network_periodic (int cmd) { return SIM_NO_ERROR; }
int network_init (void) { return SIM_OK; }
int network_uninit_ (void) { return SIM_OK; }

#endif /* HAVE_LIBMINIUPNPC */

void network_log_flags (const char *module, int level) {
  static const char *network_flags_names[] = { "none", "out", "in", "in+out" };
  static unsigned network_flags_value = 0;
  unsigned flags = simself.flags;
  if (! module || (flags & ~SIM_STATUS_FLAG_IP) != network_flags_value)
    log_any_ (module, level, "%sdht:%s%s udp:%s%s tcp:%s%s ssl:%s\n", module ? "my flags " : "",
              network_flags_names[flags & 3], flags & SIM_STATUS_FLAG_BOOT ? "+boot" : "",
              network_flags_names[flags >> 2 & 3], flags & SIM_STATUS_FLAG_DNS ? "+dns" : "",
              network_flags_names[flags >> 4 & 3], flags & SIM_STATUS_FLAG_UPNP ? "+upnp" : "",
              network_flags_names[flags >> 6 & 3]);
  if (module)
    network_flags_value = flags & ~SIM_STATUS_FLAG_IP;
}
