/**
    sending side of network protocol, including out-of-band status probes (validation of contact ip addresses)

    Copyright (c) 2020-2022 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 "contact.h"
#include "param.h"
#include "proto.h"
#include "limit.h"
#include "proxy.h"
#include "proxies.h"
#include "nat.h"
#include "server.h"
#include "client.h"
#include "msg.h"
#include "xfer.h"
#include "audio.h"
#include "api.h"

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

#define SIM_MODULE "client"

static struct client_probe {
  struct client_probe *next; /* next probe in linked list - must be first element of structure */
  pth_message_t header;      /* message queue item */
  simcontact contact;        /* contact */
  simtype host;              /* hostname (only when adding it) */
  unsigned senderip;         /* originator ip address of probe (if CLIENT_PROBE_DHT) or zero */
  unsigned ip;               /* ip address to connect to */
  int port;                  /* port number to connect to */
  int type;                  /* CLIENT_PROBE_xxx (see client.h) */
  struct _socket sock;       /* socket to create for connecting */
} *client_probe_list = NULL; /* first probe in linked list */

static pth_msgport_t client_probe_queue;
static pth_event_t client_probe_event;

int client_probe_count = 0;

static const char *client_probe_names[] = { "", "OFF", "ON", "ACCEPT", "DHT", "REVERSE" };
static const char *client_state_names[] = { "TMP", "OUT", "TMPIN", "IN" };

static simclient client_list = NULL; /* first client in linked list */
static pth_t tid_queue, tid_logon;
static simbool client_logon_flag;
static int client_proxy_error;
static unsigned client_connect_sequence;

#define CLIENT_LOOKUP_STATE_NAME(client) \
  client_state_names[((client)->flags & (CLIENT_FLAG_ACCEPTED | CLIENT_FLAG_CONNECTED)) / CLIENT_FLAG_CONNECTED]
#define CLIENT_LOOKUP_PROBE_NAME(type) client_probe_names[(type) -CLIENT_PROBE_CONNECT]

#define CLIENT_CASE_FLAG_NAME(sock) ((sock)->err != SIM_OK ? '!' : ((sock)->flags) & SOCKET_FLAG_LOCAL ? '%' : '$')

simclient client_acquire (simclient client) {
  if (client) {
    if (client->count > 0) {
      client->count++;
    } else
      LOG_ERROR_ ("acquire%d $%d '%s'\n", client->count, client->sock->fd, client->contact->nick.str);
  }
  return client;
}

void client_release (simclient client) {
  if (! client)
    return;
  if (! --client->count) {
    LOG_DEBUG_ ("disconnect%d $%d '%s'\n", client->count, client->sock->fd, client->contact->nick.str);
    if (sim_list_delete (&client_list, client)) {
      LOG_XTRA_SIMTYPE_ (client->buffer, 0, "buffer $%d delete ", client->sock->fd);
      client->contact->clients--;
      array_free (client->buffer);
      socket_close (client->sock, client->contact);
      sim_free (client->sock, sizeof (*client->sock));
      string_free (client->insecure);
      sim_free (client, sizeof (*client));
    } else
      LOG_ERROR_ ("zombie client%d $%d '%s'\n", client->count, client->sock->fd, client->contact->nick.str);
  } else if (client->count < 0)
    LOG_ERROR_ ("disconnect%d $%d '%s'\n", client->count, client->sock->fd, client->contact->nick.str);
}

int client_count (int *cancelled, simcontact contact, simclient *oldest) {
  int count = 0;
  simunsigned tick = 0;
  simclient client;
  if (oldest)
    *oldest = NULL;
  for (client = client_list; client; client = client->next)
    if (client->sock->fd != INVALID_SOCKET) {
      count += nat_get_sockets (client) + 1;
#ifndef _WIN32
      if (cancelled && client->sock->err != SIM_OK)
        *cancelled += nat_get_sockets (client) + 1;
#endif
      if (client->sock->err == SIM_OK && (! tick || client->tick < tick) && ! client->contact->connected)
        if (client->flags & CLIENT_FLAG_CONNECTED && oldest && client->contact != contact) {
          tick = client->tick;
          *oldest = client;
        }
    }
  return count;
}

simnumber client_add_status (simtype table, simcontact contact) {
  int info = contact != contact_list.system ? param_get_number ("client.info") : 0, status = SIM_STATUS_AWAY;
  int edit = CONTACT_CHECK_RIGHT_EDIT (contact) ? abs (param_get_number ("msg.edit")) : 0, flags = 0;
  table_add_number (table, SIM_CMD_STATUS_EDIT, edit);
  if (simself.status != SIM_STATUS_IDLE)
    status = simself.status >= SIM_STATUS_OFF ? simself.status : SIM_STATUS_OFF;
  if (contact == contact_list.system && (simself.status == SIM_STATUS_IDLE || simself.status == SIM_STATUS_HIDE))
    status = SIM_STATUS_ON;
  table_add_pointer (table, SIM_CMD_STATUS_STATUS, contact_status_names[status - SIM_STATUS_MIN]);
  if (contact->flags & CONTACT_FLAG_VERIFIED)
    flags |= CONTACT_FLAG_VERIFY;
  if (! (contact->msgs.flags & CONTACT_MSG_OVERFLOW)) {
    if (CONTACT_CHECK_RIGHT_UTF (contact))
      flags |= CONTACT_FLAG_UTF;
    if (CONTACT_CHECK_RIGHT_XFER (contact))
      flags |= CONTACT_FLAG_XFER;
    if (CONTACT_CHECK_RIGHT_AUDIO (contact))
      flags |= CONTACT_FLAG_AUDIO;
  }
  if (param_get_number ("rights.typing") > 0)
    flags |= CONTACT_FLAG_TYPING;
  if ((contact->flags & (CONTACT_FLAG_TYPING | CONTACT_FLAG_TYPE_Y)) == (CONTACT_FLAG_TYPING | CONTACT_FLAG_TYPE_Y))
    if (CONTACT_CHECK_RIGHT_TYPE (contact))
      flags |= CONTACT_FLAG_TYPE;
  if (contact->flags & CONTACT_FLAG_ECHO_Y)
    flags |= CONTACT_FLAG_ECHO_CANCEL_Y;
  if (contact->flags & CONTACT_FLAG_ECHO_N)
    flags |= CONTACT_FLAG_ECHO_CANCEL_N;
  if (! param_get_number ("xfer.wipe"))
    flags |= CONTACT_FLAG_XFER_CANCEL;
  table_add (table, SIM_CMD_STATUS_RIGHTS, sim_convert_flags_to_strings (flags, SIM_ARRAY (contact_right_names)));
  if (info & 1)
    table_add (table, SIM_CMD_STATUS_NICK, string_copy_string (contact_list.me->nick));
  if (info & 2)
    table_add (table, SIM_CMD_STATUS_LINE, CONTACT_GET_INFO (contact_list.me));
  return contact->xfer.cid ? contact->xfer.cid : (contact->xfer.cid = xfer_new_handle (contact, -1) >> 1);
}

simclient client_new (simsocket sock, simcontact contact, int flags) {
  simclient client = sim_new (sizeof (*client));
  memset (client, 0, sizeof (*client));
  client->next = client_list;
  client->sock = sock;
  client->contact = contact;
  client->xfer.fd = -1;
  client->flags = flags;
  client->count = 1;
  client->call.state = AUDIO_CALL_HANGUP;
  client->call.quality = -1;
  client->sndtick = random_get_number (random_public, 0x3EFFFFFF) + 0x1000000;
  client->pongtick = client->tick = system_get_tick ();
  client->insecure = nil ();
  client->buffer = array_new_tables (1);
  client->buffer.len = 0;
  contact->clients++;
  return client_list = sock->client = client;
}

simtype client_new_cmd (const char *cmd, const char *key, simtype value, const char *key2, simtype value2) {
  simtype table = table_new (2);
  table_add_pointer (table, SIM_CMD, cmd);
  if (key)
    table_add (table, key, value);
  if (key2)
    table_add (table, key2, value2);
  return table;
}

simtype client_new_status (simclient client) {
  simtype status = client_new_cmd (SIM_CMD_STATUS, NULL, nil (), NULL, nil ()), host = param_get_string ("client.host");
  simcontact contact = client->contact;
  client_add_status (status, contact);
  LOG_CODE_DEBUG_ (server_log_status (SIM_MODULE, SIM_LOG_DEBUG, client->sock, contact, 0, NULL, status, 0, -1));
  if (contact != contact_list.system) {
    if (! (contact_list.test & 0x10000000))
      table_add (status, SIM_CMD_STATUS_NICK, string_copy_string (contact_list.me->nick));
    table_add (status, SIM_CMD_STATUS_LINE, CONTACT_GET_INFO (contact_list.me));
    if (! host.len) {
      table_add (status, SIM_CMD_STATUS_HOSTS, array_new_strings (0));
    } else if (host.str[0] != '.' || host.str[1])
      table_add (status, SIM_CMD_STATUS_HOSTS, string_copy_string (host));
  }
  return status;
}

int client_send_status (simclient client, int sendmode) {
  int err = SIM_OK;
  if (! client) {
    for (client = client_list; client; client = client->next)
      if (sendmode != CLIENT_SEND_STATUS_INFO || client->contact->flags & CONTACT_FLAG_INFO)
        if (client->flags & CLIENT_FLAG_CONNECTED && client->sock->err == SIM_OK)
          client_send_status (client, sendmode);
  } else if ((err = client_send (client, client_new_status (client))) == SIM_OK && sendmode != CLIENT_SEND_STATUS_OFF)
    client->contact->flags &= ~CONTACT_FLAG_INFO;
  return err;
}

static void client_send_table (simclient client, simtype table, const simtype cmd) {
  if (table.typ != SIMTABLE) {
    client->tick = system_get_tick ();
  } else if (! string_check_diff (cmd, SIM_CMD_PONG)) {
    simnumber now, ping, pong = table_detach_number (table, SIM_CMD_PING_TICK);
    if (pong && (ping = table_get_number (table, SIM_CMD_PING_PONG)))
      table_set_number (table, SIM_CMD_PING_PONG, ping + ((now = system_get_tick ()) > pong ? pong - now : 0));
  } else if (! string_check_diff (cmd, SIM_CMD_PING) && table_get (table, SIM_CMD_PING_PONG).typ != SIMNIL) {
    simnumber now = system_get_tick (), tick = client->sock->created;
    table_set_number (table, SIM_CMD_PING_PONG, now - tick >= 0 ? now - tick + 1 : 0);
  } else if (! server_check_idle (cmd))
    client->tick = system_get_tick ();
}

int client_send (simclient client, simtype table) {
  array_append (&client->buffer, table);
  return ! (client->flags & CLIENT_FLAG_BUFFERING) && client->msgqueue ? msg_put (client, 0) : SIM_OK;
}

int client_send_ (simclient client, simtype table) {
  int err = SIM_OK, size, usec = table.typ == SIMARRAY_ARRAY ? socket_max_limit[SOCKET_MAX_WAIT] : -1;
  simtype cmd = table.typ != SIMTABLE ? nil () : table_get_string (table, SIM_CMD);
  if (! (client->flags & CLIENT_FLAG_BUFFERING)) {
    simbool done = ! string_check_diff (cmd, SIM_CMD_TRAVERSED) || ! string_check_diff (cmd, SIM_CMD_REVERSED);
    while (client->buffer.len || table.typ != SIMNIL) {
      if ((err = socket_lock_ (client->sock, usec)) != SIM_OK) {
        if (usec >= 0)
          err = SIM_OK;
        break;
      }
      if (client->flags & CLIENT_FLAG_BUFFERING) {
        err = client_send_ (client, table);
        table = nil ();
        done = true;
      } else if (! done && usec < 0 && client->buffer.len) {
        simtype oldcmd = nil (), newcmd;
        unsigned i = 1, max = socket_size_max (client->sock, SOCKET_SEND_TCP);
        if (table.typ == SIMTABLE) {
          array_append (&client->buffer, table_copy_strings (table, table.len));
          table_free (table), table = nil ();
        }
        for (size = client->buffer.len <= 1 ? -1 : 0; i <= client->buffer.len; i++) {
          simtype tmp = array_detach (client->buffer, i), many, array;
          client_send_table (client, tmp, newcmd = table_get_string (tmp, SIM_CMD));
          if (size >= 0) {
            if (! size) {
              if (! CLIENT_CHECK_VERSION (client, 3)) {
                many = client_new_cmd (SIM_CMD_MANY, SIM_CMD_MANY_MSG, array_new_tables (0), NULL, nil ());
                oldcmd = pointer_new (SIM_CMD_MSG);
              } else
                table_add (many = table_new (1), SIM_CMD_MANY_MSG, array_new_tables (0));
              size = table_size (many, 0);
              array = array_new_tables (client->buffer.len);
              array.len = 0;
            }
            if (! string_check_diff_len (oldcmd, newcmd.str, newcmd.len)) {
              table_delete (tmp, SIM_CMD);
            } else if (CLIENT_CHECK_VERSION (client, 3))
              oldcmd = pointer_new_len (newcmd.str, newcmd.len);
            if ((size += table_size (tmp, 0)) < (int) max) {
              array_append (&array, tmp);
              if (i < client->buffer.len)
                continue;
            } else {
              table_add (tmp, SIM_CMD, oldcmd);
              if (array.len)
                client->buffer.arr[i--] = tmp;
            }
            if (array.len <= 1) {
              if (array.len)
                tmp = array_detach (array, 1);
              array_free (array);
              table_free (many);
            } else
              table_set (tmp = many, SIM_CMD_MANY_MSG, array);
          }
          LOG_XTRA_SIMTYPE_ (tmp, 0, "send $%d ", client->sock->fd);
          if (i == client->buffer.len) {
            client->buffer.len = 0;
          } else
            array_detach_end (&client->buffer);
          err = socket_send_table_ (client->sock, tmp, SOCKET_SEND_TCP, NULL);
          table_free (tmp);
          break;
        }
      } else if (table.typ != SIMNIL) {
        client_send_table (client, table, cmd);
        if (table.typ == SIMTABLE)
          LOG_XTRA_ ("send $%d %s\n", client->sock->fd, cmd.str);
        err = socket_send_table_ (client->sock, table, usec < 0 ? SOCKET_SEND_TCP : SOCKET_SEND_AUDIO, NULL);
        done = true;
      }
      socket_unlock (client->sock, client->sock->fd);
      if (done || err != SIM_OK)
        break;
    }
    if (! string_check_diff (cmd, SIM_CMD_TRAVERSED) || ! string_check_diff (cmd, SIM_CMD_REVERSED)) {
      client->flags |= CLIENT_FLAG_BUFFERING;
    } else if (err != SIM_OK && socket_cancel (client->sock, err) != SIM_CRYPT_BAD_TABLE)
      client->flags |= CLIENT_FLAG_ERROR; /* fail client server loop */
  } else if (cmd.typ != SIMNIL && string_check_diff (cmd, SIM_CMD_MSG)) {
    err = client_send (client, table_copy_strings (table, table.len));
  } else if (table.typ != SIMNIL)
    LOG_XTRA_ ("buffer $%d drop %s\n", client->sock->fd, cmd.str);
  audio_free_packet (table);
  return err;
}

void client_send_proxy (void) {
  simclient client;
  for (client = client_list; client; client = client->next)
    if (client->flags & CLIENT_FLAG_CONNECTED && client->sock->err == SIM_OK) {
      int port;
      unsigned ip;
      const char *addr;
      if (proxy_get_ip_proxy (&addr, &ip, &port) && ! sim_network_check_local (ip) &&
          (! addr || strcmp (client->contact->addr, addr))) {
        client_send_cmd (client, SIM_CMD_REQUEST_PROXY, SIM_CMD_REQUEST_PROXY_IP, string_copy (network_convert_ip (ip)),
                         SIM_CMD_REQUEST_PROXY_PORT, number_new (port));
      } else if (! proxy_check_required (true)) {
        simnumber ports = (port = network_get_port (NULL)) == 0 ? abs (param_get_number ("main.port")) : port;
        client_send_cmd (client, SIM_CMD_REQUEST_PROXY, SIM_CMD_REQUEST_PROXY_PORT, number_new (ports), NULL, nil ());
      }
    }
}

int client_send_udp (simclient client, simtype table, simnumber sequence, unsigned ip, int port) {
  int err = client->sock->err;
  if (err == SIM_OK) {
    const int bits = SOCKET_SEND_UDP | SOCKET_SEND_AUDIO;
    if ((err = socket_send_udp (client->sock, table, sequence, ip, port, bits, NULL)) == SIM_SOCKET_EXCEEDED) {
      socket_cancel (client->sock, err);
      client->flags |= CLIENT_FLAG_ERROR;
    } else
      err = SIM_OK;
  }
  audio_free_packet (table);
  client->tick = system_get_tick ();
  return err;
}

int client_recv_udp (const simtype input, unsigned ip, int port) {
  int err = SIM_SOCKET_BAD_PACKET;
  simbool udp = param_get_number ("client.udp"), nat = param_get_number ("nat.udp");
  simclient client = audio_status.client;
  if (client && client != AUDIO_CLIENT_TEST) {
    simnumber seq = client->flags & CLIENT_FLAG_ACCEPTED ? (simnumber) 1 << 62 : 0;
    err = server_recv_udp (client, input, seq - audio_get_param (AUDIO_PARAM_RATE), ip, port);
  }
  for (client = client_list; client && err != SIM_OK; client = client->next)
    if (client->flags & CLIENT_FLAG_CONNECTED) {
      simnumber seq = client->flags & CLIENT_FLAG_ACCEPTED ? ((simnumber) 1 << 62) - 1 : -1;
      if (! udp || (err = server_recv_udp (client, input, seq, ip, port)) != SIM_OK)
        if (nat && socket_check_client (client->sock))
          if (client->call.state == AUDIO_CALL_TALKING || client->call.state == AUDIO_CALL_OUTGOING)
            err = proxy_recv_udp (client, input, ip, port);
    }
  return err == SIM_OK ? err : proxy_recv_udp (NULL, input, ip, port);
}

int client_recv_ (simclient client, simtype *table) {
  int err = socket_recv_table_ (client->sock, server_proto_client, server_proto_audio, table, NULL);
  if (err == SIM_OK) {
    if (table->typ == SIMARRAY_ARRAY || ! server_check_idle (table_get_string (*table, SIM_CMD)))
      client->tick = system_get_tick ();
    client->contact->seen[CONTACT_SEEN_RECEIVE] = time (NULL);
  } else if (event_test_error_crypto (client->contact, client->contact->addr, err) != SIM_CRYPT_BAD_TABLE)
    client->flags |= CLIENT_FLAG_ERROR;
  return err;
}

simbool client_check_local (unsigned ip, simbool notsure) {
  unsigned netip;
  struct _socket tmp;
  network_get_port (&netip);
  if (ip == netip)
    return true;
  if (notsure && ip != simself.ipout && ip != simself.ipin)
    if (! table_get_key_number (simself.ipclient, pointer_new_len (&ip, sizeof (ip))))
      notsure = false;
  if (socket_open (&tmp, NULL) == SIM_OK) {
    if (socket_listen_port (tmp.fd, -1, ip, 0, NULL) == SIM_OK)
      notsure = true;
    socket_close (&tmp, NULL);
  }
  return notsure;
}

static int client_check_locked (void) {
  int locks = 0;
  simclient client;
  if (param_get_number ("nat.lock"))
    for (client = client_list; client; client = client->next)
      locks += nat_check_locked (client);
  return locks;
}

static int client_connect_socket_ (simsocket sock, unsigned *ip, int port, const char *host, int type, simbool local) {
  if (! local && type != CLIENT_PROBE_CONNECT) {
    int locks = client_check_locked (), locked;
    if (locks) {
      LOG_NOTE_ ("handshake $%d: WAIT %d\n", sock->fd, locks);
      while (sock->err == SIM_OK && locks) {
        pth_sleep_ (1);
        if ((locked = client_check_locked ()) != locks) {
          locks = locked;
          LOG_NOTE_ ("handshake $%d: WAIT %d\n", sock->fd, locks);
        }
      }
      LOG_NOTE_ ("handshake $%d: WAIT %d\n", sock->fd, sock->err == SIM_OK ? 0 : -1);
    }
  }
  return socket_connect_ (sock, ip, port, local ? NULL : host, local);
}

#define CLIENT_CHECK_VERIFY() (param_get_number ("client.verify") && audio_get_param (AUDIO_PARAM_SPEED) <= 0)

static int client_handshake_ (simsocket sock, simcontact contact, unsigned *ip, int port, int port2,
                              const char *host, int type, simbool local) {
  simnumber tick = system_get_tick ();
  simclient client = sock->client;
  simbool probed = false, oob = type != CLIENT_PROBE_CONNECT;
  unsigned proxyip, proxyownip = 0;
  int err = contact == contact_list.me && param_get_number ("net.tor.port") > 0 ? SIM_CLIENT_NOT_FOUND : SIM_OK;
  int protover = -1;
  if (! oob)
    client->proxyudport = port;
  local = local || host ? -local : sim_network_check_local (*ip) || client_check_local (*ip, false);
  if (err == SIM_OK)
    err = client_connect_socket_ (sock, ip, port, host, type, local < 0);
  if (err != SIM_OK && local >= 0) {
    if (sock->err == SIM_OK && ! local)
      proxy_probe (contact->addr, *ip, port, PROXY_PROBE_NOCONTACT);
  retry:
    if (*ip && port != port2)
      err = socket_reopen (sock, client, contact, err);
    if (err == SIM_OK)
      if ((err = client_connect_socket_ (sock, ip, port = port2, NULL, type, local < 0)) != SIM_OK)
        if (sock->err == SIM_OK && ! local)
          proxy_probe (contact->addr, *ip, port, PROXY_PROBE_NOCONTACT);
  }
  if (err == SIM_OK) {
    if ((err = proxy_handshake_client_ (sock, contact->addr, *ip, port, &proxyownip, NULL, &protover)) != SIM_OK) {
      LOG_INFO_ ("handshake $%d at %s:%d '%s' error: %s\n",
                 sock->fd, network_convert_ip (*ip), port, contact->nick.str, convert_error (err, true));
      if (sock->err == SIM_OK && ! proxy_get_proxy (NULL, NULL, NULL) && proxy_check_required (true) && ! local) {
        proxy_probe (contact->addr, *ip, port, PROXY_PROBE_NOCONTACT);
        if (protover >= 0 && ! probed) {
          proxy_probe (contact->addr, *ip, port, PROXY_PROBE_DHT);
          probed = true;
        }
      }
    } else if (socket_check_client (sock) && ! probed && ! local) {
      proxy_probe (contact->addr, *ip, port, PROXY_PROBE_IP);
      probed = true;
    }
    if (err == SIM_OK) {
      const char *addr, *handshake = oob ? SIM_HANDSHAKE_TYPE_EC : SIM_HANDSHAKE_TYPE_RSA;
      int flag = simself.flags & SIM_STATUS_FLAG_TCP_OUT ? SIM_REPLY_FLAG_VERIFY_OK : 0, fl, proxyport;
      simunsigned myage = contact == contact_list.me ? status_get_tick () : 0;
      simunsigned nonce = simself.nonce[1], mynonce = simself.nonce[0];
      simtype version = table_new (9);
      if (contact_list.test & 0x2000000)
        flag = SIM_REPLY_FLAG_VERIFY_NOK;
      if (contact != contact_list.me) {
        mynonce = random_get_number (random_public, 0xFFFFFFFF);
      } else if (! oob) { /* can be removed to implement connection to self (not myself) */
        sock->flags &= ~SOCKET_FLAG_CLIENT;
        handshake = SIM_HANDSHAKE_TYPE_EC;
        oob = true;
      }
      server_add_versions (version, contact, SIM_PROTO_VERSION_MAX, mynonce, myage, oob, false);
      if (! local) {
        table_add (version, SIM_REQUEST_TO, string_copy (network_convert_ip (*ip)));
        table_add_number (version, SIM_REQUEST_TO_PORT, port);
      }
      if (type != CLIENT_PROBE_REVERSE) {
        if (proxy_get_ip_proxy (&addr, &proxyip, &proxyport)) {
          if (! (contact_list.test & 0x1000000) || param_get_number ("client.verify")) {
            table_add (version, SIM_REQUEST_PROXY_IP, string_copy (network_convert_ip (proxyip)));
            table_add_number (version, SIM_REQUEST_PROXY_PORT, proxyport);
          }
          if (contact->auth >= CONTACT_AUTH_ACCEPTED && ! sim_network_check_local (proxyip))
            if (contact != contact_list.me && CLIENT_CHECK_VERIFY () && (! addr || strcmp (contact->addr, addr)))
              flag |= SIM_REQUEST_FLAG_VERIFY;
        }
        table_add (version, SIM_REQUEST_FLAGS, sim_convert_flags_to_strings (flag, SIM_ARRAY (server_flag_names)));
        server_add_ip (version, sock, oob, false);
        if ((contact == contact_list.me && ! proxyport) || param_get_number ("proxy.require") < -1)
          table_delete (version, SIM_REQUEST_PORT);
      }
      contact->tick = system_get_tick ();
      if (! oob) {
        client->flags |= CLIENT_FLAG_FORWARD;
        table_add (version, SIM_REQUEST_CODECS, audio_get_codecs ());
      }
      table_add_number (version, SIM_REQUEST_ID, client_add_status (version, contact));
      LOG_CODE_DEBUG_ (server_log_status (SIM_MODULE, SIM_LOG_DEBUG, sock, contact, 0, NULL, version, 0, -1));
      if (contact->flags & CONTACT_FLAG_NEW && type != CLIENT_PROBE_REVERSE)
        table_add (version, SIM_CMD_STATUS_NICK, string_copy_string (contact_list.me->nick));
      if ((err = crypt_handshake_client_ (sock, &version, server_proto_handshake, handshake)) == SIM_OK) {
        simnumber ver = table_get_number (version, SIM_REPLY);
        contact->flags &= ~CONTACT_FLAG_NEW;
        if (ver < SIM_PROTO_VERSION_MIN || ver > SIM_PROTO_VERSION_MAX) {
          LOG_NOTE_ ("handshake $%d failed (version %lld) '%s'\n", sock->fd, ver, contact->nick.str);
          err = SIM_CLIENT_BAD_VERSION;
        } else {
          simtype flags = table_get (version, SIM_REPLY_FLAGS);
          if (flags.typ == SIMARRAY_STRING)
            flags = number_new (sim_convert_strings_to_flags (flags, SIM_ARRAY (server_flag_names)));
          if (flags.typ == SIMNUMBER) {
            simtype ips = table_get_array_string (version, SIM_REPLY_IP);
            LOG_CODE_DEBUG_ (server_log_status (SIM_MODULE, SIM_LOG_DEBUG, sock, contact,
                                                ver, ips.len ? ips.arr[1].ptr : NULL, version, tick, oob));
            if (! oob) {
              simtype ports = table_get_array_number (version, SIM_REPLY_PORT);
              client->proxyip = *ip, client->proxyport = port;
              if (sim_network_parse_ips_ports (ips, ports, SIM_ARRAY_SIZE (client->param.ip) - 1,
                                               client->param.ip + 1, client->param.port + 1) < 0)
                LOG_WARN_SIMTYPE_ (ports, 0, "invalid $%d global ports '%s' ", sock->fd, contact->nick.str);
              if ((client->param.ip[0] = client->param.ip[1]) != contact->location && contact->location) {
                LOG_DEBUG_ ("country $%d %s '%s'\n", sock->fd,
                            network_convert_ip (client->param.ip[1]), contact->nick.str);
                contact->location = 0;
              }
              ports = table_get_array_number (version, SIM_REPLY_LOCAL_PORT);
              if (sim_network_parse_ips_ports (table_get_array_string (version, SIM_REPLY_LOCAL_IP), ports,
                                               SIM_ARRAY_SIZE (client->param.localip),
                                               client->param.localip, client->param.localport) < 0)
                LOG_WARN_SIMTYPE_ (ports, 0, "invalid $%d local ports '%s' ", sock->fd, contact->nick.str);
              client->param.nonce[0] = mynonce, client->param.nonce[1] = table_get_number (version, SIM_REPLY_RANDOM);
              table_delete (version, SIM_CMD_STATUS_STATUS);
            }
            ips = table_detach_array_string (version, SIM_REPLY_BAD);
            server_set_versions (oob ? NULL : client, contact, ips);
          }
          if (flags.typ != SIMNUMBER) { /* peer rejected my version, she will close connection immediately */
            LOG_DEBUG_ ("handshake $%d succeeded (version %lld) '%s'\n", sock->fd, ver, contact->nick.str);
            err = SIM_CLIENT_BAD_VERSION;
          } else if (contact != contact_list.me) {
            simnumber now = system_get_tick (), duration = (now - tick) / 1000 + 1;
            simtype logon = table_get (version, SIM_REPLY_LOGON);
            if (contact->handshake < duration && duration == (int) duration)
              contact->handshake = (int) duration;
            if (logon.typ == SIMNUMBER)
              contact->logon = logon.num > 0 && logon.num == (int) logon.num ? (int) logon.num : 0;
            if (! socket_check_client (sock)) {
              LOG_XTRA_ ("country $%d %s '%s'\n", sock->fd, network_convert_ip (*ip), contact->nick.str);
              contact->location = *ip;
              if (! oob)
                client->param.ip[0] = *ip;
            }
            if (! contact->verify && flag & SIM_REQUEST_FLAG_VERIFY && flags.num & SIM_REPLY_FLAG_VERIFY_OK) {
              contact->verify = now + (param_get_number ("client.verified") + contact->handshake) * 1000;
              LOG_DEBUG_ ("verify (%lld seconds) '%s'\n", (contact->verify - now) / 1000, contact->nick.str);
            }
            server_set_status (contact, client, version, 0, 0);
            msg_recv_ack (contact, table_get_number (version, SIM_REPLY_ACK));
          } else if (table_get_number (version, SIM_REPLY_RANDOM) == (simnumber) nonce) {
            simnumber count = table_get_key_number (simself.ipclient, pointer_new_len (ip, sizeof (*ip)));
            LOG_DEBUG_ ("connected $%d to #%lld self %s:%d (#%lld)\n", sock->fd,
                        nonce, network_convert_ip (*ip), port, mynonce);
            if (! socket_check_client (sock)) {
              sim_set_table_number (&simself.ipclient, string_copy_len (ip, sizeof (*ip)), count + 1);
              SOCKET_INIT_STAT (sock);
              sock->created = 0;
            }
            sock->flags |= SOCKET_FLAG_HANDSHAKE;
            err = SIM_CLIENT_SELF;
          } else {
            simunsigned age = table_get_number (version, SIM_REPLY_AGE);
            if (server_set_status (contact, NULL, version, age, myage) == SIM_SERVER_OFFLINE)
              server_logoff (SIM_SERVER_OFFLINE, SIM_STATUS_INVISIBLE);
            msg_recv_ack (contact, table_get_number (version, SIM_REPLY_ACK));
            contact_list.me->seen[CONTACT_SEEN_RECEIVE] = time (NULL);
          }
          if (err == SIM_OK) {
            unsigned ownip = sim_network_parse_ip (table_get_pointer (version, SIM_REPLY_FROM));
            if (param_get_number ("net.tor.port") <= 0 && ! sim_network_check_local (ownip)) {
              simself.flags &= ~SIM_STATUS_FLAG_IP;
              simself.ipout = ownip;
              LOG_DEBUG_ ("my ip $%d = %s '%s'\n", sock->fd, network_convert_ip (ownip), contact->nick.str);
            }
            if (! oob) {
              client->ownip = ownip ? ownip : proxyownip;
              client->version = (int) ver;
              if ((err = crypt_rsa_handshake_client_ (sock, contact, server_proto_handshake, version)) == SIM_OK) {
                fl = (int) flags.num & (SIM_REPLY_FLAG_CONNECT | SIM_REPLY_FLAG_VERIFY_OK | SIM_REPLY_FLAG_VERIFY_NOK);
                client->flags |= fl;
              }
            } else {
              sock->flags |= SOCKET_FLAG_HANDSHAKE;
              if (type == CLIENT_PROBE_LOGON || type == CLIENT_PROBE_ACCEPT || type == CLIENT_PROBE_DHT) {
                simnumber cid = table_get_number (version, SIM_REPLY_ID);
                simbool pending = flags.num & SIM_REPLY_FLAG_MSG && contact->auth >= CONTACT_AUTH_ACCEPTED;
                if (pending || contact->flags & CONTACT_FLAG_INFO || xfer_check_pending (contact, cid))
                  msg_connect (contact, false);
              }
            }
          }
          if (! socket_check_client (sock) && err == SIM_OK && flags.num & SIM_REPLY_FLAG_PROXY && ! local)
            proxy_probe (contact->addr, *ip, port, PROXY_PROBE_CONTACT);
        }
      } else if (sock->err == SIM_OK && ! local)
        proxy_probe (contact->addr, *ip, port, PROXY_PROBE_NOCONTACT);
      table_free (version);
      event_test_error_crypto (contact, contact->addr, err);
    }
    if (err != SIM_OK && err != SIM_CLIENT_SELF) {
      LOG_INFO_ ("handshake $%d at %s:%d error %d (%lld ms) '%s'\n", sock->fd,
                 network_convert_ip (*ip), port, err, system_get_tick () - tick, contact->nick.str);
      if (local >= 0)
        goto retry;
    }
  }
  return err;
}

static int client_handshake_local_ (simsocket sock, simcontact contact, int type) {
  if (proxy_find_server (contact->addr) && contact != contact_list.me) {
    simclient client = sock->client;
    int port = abs (param_get_number ("main.port")), err;
    unsigned ip = sim_network_parse_ip_default (param_get_pointer ("proxy.local"), &ip);
    sock->flags |= SOCKET_FLAG_LOCAL;
    if ((err = client_handshake_ (sock, contact, &ip, port, port, NULL, type, true)) == SIM_OK)
      return SIM_CLIENT_LOCAL;
    if ((err = socket_reopen (sock, client, contact, err)) != SIM_OK)
      return err;
    ip = sim_network_get_default ();
    if ((err = client_handshake_ (sock, contact, &ip, port, port, NULL, type, true)) == SIM_OK)
      return SIM_CLIENT_LOCAL;
    if ((err = socket_reopen (sock, client, contact, err)) != SIM_OK)
      return err;
  }
  return SIM_CLIENT_NOT_FOUND;
}

static int client_handshake_probe_ (simsocket sock, simcontact contact, unsigned ip, int port, int port2, int type) {
  simbool repeat = false, auth = type != CLIENT_PROBE_CONNECT || param_get_number ("contact.strangers") < 3;
  int err = SIM_CLIENT_NOT_FOUND, i;
  simclient client = sock->client;
  if (contact->auth < (auth ? CONTACT_AUTH_ACCEPTED : CONTACT_AUTH_NEW) || contact_list.test & 0x8000000)
    return SIM_CONTACT_BLOCKED;
  if (! ip && ! port) {
    unsigned oldip = 0, oldport = 0;
    if ((err = client_handshake_local_ (sock, contact, type)) != SIM_CLIENT_NOT_FOUND)
      return err;
    if (contact->ips) {
      repeat = true;
      ip = oldip = contact->ip->ip, port = oldport = contact->ip->port;
      err = client_handshake_ (sock, contact, &ip, port, port2, NULL, type, false);
    }
    for (i = contact->hosts.len; i && err != SIM_OK; i--) {
      simtype host;
      if (contact->ips && (contact->ip->ip != oldip || contact->ip->port != oldport) && type == CLIENT_PROBE_CONNECT)
        return SIM_CLIENT_UNKNOWN;
      if (repeat && (err = socket_reopen (sock, client, contact, err)) != SIM_OK)
        return err;
      ip = 0;
      if ((host = sim_network_parse_host_port (contact->hosts.arr[i].ptr, &port)).ptr && port) {
        err = client_handshake_ (sock, contact, &ip, port, port2, host.ptr, type, false);
        repeat = true;
      }
      string_free (host);
    }
    for (i = oldip != 0; err != SIM_OK && i < contact->ips && i < param_get_number ("contact.ips"); i++) {
      if ((contact->ip->ip != oldip || contact->ip->port != oldport) && type == CLIENT_PROBE_CONNECT)
        return SIM_CLIENT_UNKNOWN;
      if (repeat && (err = socket_reopen (sock, client, contact, err)) != SIM_OK)
        return err;
      ip = contact->ip[i].ip, port = contact->ip[i].port;
      err = client_handshake_ (sock, contact, &ip, port, port2, NULL, type, false);
      repeat = true;
    }
    if (err != SIM_OK && contact->ips && type == CLIENT_PROBE_CONNECT)
      if (contact->ip->ip != oldip || contact->ip->port != oldport)
        return SIM_CLIENT_UNKNOWN;
  } else {
    err = client_handshake_ (sock, contact, &ip, port, port2, NULL, type, false);
    if (type == CLIENT_PROBE_LOGON && contact == contact_list.me)
      if (err == SIM_CLIENT_SELF || param_get_number ("net.tor.port") <= 0)
        proxy_set_score (contact, err, err == SIM_CLIENT_SELF ? PROXY_SCORE_OK : PROXY_SCORE_NOK);
  }
  if (err == SIM_OK && param_get_number ("proxy.require") <= 1 && ! proxy_find_server (contact->addr))
    contact_set_ip (contact, ip, port, CONTACT_IP_ADD);
  return err;
}

int client_probe (simcontact contact, unsigned ip, int port, const char *host, int type) {
  int err = SIM_CLIENT_CANCELLED, i = 0, senderip = 0;
  struct client_probe *probe = sim_new (sizeof (*probe)), *p;
  if (simself.state != CLIENT_STATE_RUNNING || simself.status == SIM_STATUS_OFF)
    if (type != CLIENT_PROBE_LOGOFF && type != CLIENT_PROBE_ACCEPT)
      goto quit;
  if (contact_list.test & 0x2000000 && type == CLIENT_PROBE_REVERSE)
    goto quit;
  err = SIM_OK;
  if (ip || port) {
    LOG_DEBUG_ ("probe %s:%p %s:%d '%s'\n", CLIENT_LOOKUP_PROBE_NAME (type), probe,
                network_convert_ip (ip), port, contact->nick.str);
  } else if (contact)
    LOG_DEBUG_ ("probe %s:%p '%s'\n", CLIENT_LOOKUP_PROBE_NAME (type), probe, contact->nick.str);
  if (type == CLIENT_PROBE_DHT) {
    senderip = *(const int *) host;
    host = NULL;
    if (contact_set_ip (contact, ip, port, CONTACT_IP_FIND)) {
      LOG_DEBUG_ ("probe %s:%p %s:%d SKIP '%s'\n", CLIENT_LOOKUP_PROBE_NAME (type), probe,
                  network_convert_ip (ip), port, contact->nick.str);
      goto quit;
    }
  }
  probe->next = probe; /* not used */
#ifndef DONOT_DEFINE
  if ((type == CLIENT_PROBE_LOGON && ! (ip || port)) || type == CLIENT_PROBE_DHT || type == CLIENT_PROBE_REVERSE)
    if (contact && ! host && (ip || port || ! proxy_find_server (contact->addr)))
      for (i = 0; i <= 1; i++)
        for (p = i ? contact->probes : client_probe_list; p; p = p->next)
          if (p->contact == contact && p->host.typ == SIMNIL) {
            if (p->type != CLIENT_PROBE_LOGON && p->type != CLIENT_PROBE_DHT)
              if (p->type != CLIENT_PROBE_REVERSE || type != CLIENT_PROBE_REVERSE)
                continue;
            if (p->ip != ip || p->port != port)
              if (! (ip || port) || p->ip || p->port || ! contact_set_ip (contact, ip, port, CONTACT_IP_FIND))
                continue;
            LOG_DEBUG_ ("probe %s:%p %s:%d SKIP (%s:%p %d) '%s'\n", CLIENT_LOOKUP_PROBE_NAME (type), probe,
                        network_convert_ip (ip), port, CLIENT_LOOKUP_PROBE_NAME (p->type), p, p->port,
                        contact->nick.str);
            goto quit;
          }
#endif
  probe->contact = contact;
  probe->host = host ? string_copy (host) : nil ();
  probe->senderip = senderip;
  probe->ip = ip;
  probe->port = port;
  probe->type = type;
  socket_new (&probe->sock);
  if ((err = pth_queue_put (client_probe_queue, &probe->header)) == SIM_OK) {
    if (i) {
      for (p = contact->probes; p && p->next; p = p->next) {}
      sim_list_append (&contact->probes, p, probe);
    }
    return SIM_OK;
  }
  string_free (probe->host);
quit:
  sim_free (probe, sizeof (*probe));
  return err;
}

static void client_probe_free (struct client_probe *probe) {
  if (sim_list_delete (&client_probe_list, probe)) {
    --client_probe_count;
    LOG_DEBUG_ ("stop $%d %s:%p '%s'\n", probe->sock.fd, CLIENT_LOOKUP_PROBE_NAME (probe->type), probe,
                probe->contact->nick.str);
    socket_close (&probe->sock, probe->contact);
    string_free (probe->host);
    sim_free (probe, sizeof (*probe));
  } else
    LOG_ERROR_ ("zombie probe $%d %s:%p '%s'\n", probe->sock.fd,
                CLIENT_LOOKUP_PROBE_NAME (probe->type), probe, probe->contact->nick.str);
}

static void client_probe_cancel (const struct client_probe *probe, simcontact contact, int error) {
  struct client_probe *p;
  if (contact)
    main_search_stop (contact->addr);
  for (p = client_probe_list; p; p = p->next)
    if (! contact || (p != probe && p->contact == contact)) {
      if (p->host.typ != SIMNIL)
        if (error == SIM_CLIENT_CONNECTED || error == SIM_CLIENT_ONLINE || error == SIM_SERVER_DROPPED)
          continue;
      socket_cancel (&p->sock, error);
      LOG_DEBUG_ ("cancel $%d %s:%p (error %d) '%s'\n", p->sock.fd, CLIENT_LOOKUP_PROBE_NAME (p->type), p, error,
                  p->contact->nick.str);
    }
}

static void client_probe_cancel_ip (unsigned senderip, int error) {
  struct client_probe *probe;
  for (probe = client_probe_list; probe; probe = probe->next)
    if (probe->senderip == senderip) {
      socket_cancel (&probe->sock, error);
      LOG_DEBUG_ ("cancel $%d %p (error %d) '%s'\n", probe->sock.fd, probe, error, probe->contact->nick.str);
    }
}

static void *thread_probe_ (void *arg) {
  struct client_probe *probe = arg;
  int err, flags, fd = probe->sock.fd, port = probe->contact->ip->port;
  simunsigned cpu = probe->senderip ? sim_system_cpu_get (SYSTEM_CPU_TIME_CYCLES, NULL) : 0, tick = system_get_tick ();
  unsigned ip;
  LOG_API_DEBUG_ ("$%d '%s'\n", fd, probe->contact->nick.str);
  if (probe->host.typ == SIMNIL) {
    err = client_handshake_probe_ (&probe->sock, probe->contact, probe->ip, probe->port, SIM_PROTO_PORT, probe->type);
  } else if ((err = client_handshake_ (&probe->sock, probe->contact, &probe->ip, probe->port, SIM_PROTO_PORT,
                                       probe->host.ptr, probe->type, false)) == SIM_OK) {
    LOG_DEBUG_ ("host %s:%u '%s'\n", probe->host.str, probe->port, probe->contact->nick.str);
    contact_add_host (probe->contact, probe->host.ptr, probe->port);
  }
  if (err != SIM_OK && err != SIM_CLIENT_SELF && probe->sock.err == SIM_OK && probe->type == CLIENT_PROBE_DHT)
    if (probe->contact->ips && probe->contact->ip->ip != probe->ip && port != probe->port && port != SIM_PROTO_PORT)
      if (socket_reopen (&probe->sock, probe->sock.client, probe->contact, SIM_OK) == SIM_OK)
        err = client_handshake_probe_ (&probe->sock, probe->contact, probe->ip, port, port, probe->type);
  if (err == SIM_OK || err == SIM_CLIENT_LOCAL) {
    if (err == SIM_OK) {
      LOG_DEBUG_ ("ip $%d = %s:%d '%s'\n", fd,
                  network_convert_ip (probe->contact->ip->ip), probe->contact->ip->port, probe->contact->nick.str);
    } else
      LOG_DEBUG_ ("ip $%d '%s'\n", fd, probe->contact->nick.str);
    client_probe_cancel (probe, probe->contact, SIM_CLIENT_ONLINE);
    err = SIM_OK;
  } else if (probe->sock.err == SIM_OK && err != SIM_CLIENT_SELF)
    if (probe->type == CLIENT_PROBE_LOGON || probe->type == CLIENT_PROBE_ACCEPT)
      contact_set_status (probe->contact, SIM_STATUS_INVISIBLE, number_new (probe->contact->flags));
  ip = probe->senderip;
  flags = probe->sock.flags;
  client_probe_free (probe);
  LOG_API_DEBUG_ ("$%d error %d (%lld ms)\n", fd, err, system_get_tick () - tick);
  if (ip && main_cputime_test (cpu, ip, system_get_tick (), flags & SOCKET_FLAG_CONNECT ? 1024 : 4))
    client_probe_cancel_ip (ip, SIM_LIMIT_DHT_DISABLE);
  return pth_thread_exit_ (true);
}

static void *thread_queue_ (void *arg) {
  LOG_API_DEBUG_ ("\n");
  while (pth_queue_wait_ (client_probe_event, -1) > 0) {
    struct client_probe *probe;
    const int offset = (char *) &((struct client_probe *) 0)->header - (char *) 0;
    if (simself.state == CLIENT_STATE_STOPPED) {
      while ((probe = (struct client_probe *) pth_queue_get (client_probe_queue)) != NULL) {
        string_free (((struct client_probe *) ((char *) probe - offset))->host);
        sim_free ((char *) probe - offset, sizeof (*probe));
      }
      break;
    }
    probe = (struct client_probe *) ((char *) pth_queue_get (client_probe_queue) - offset);
    if (probe == probe->contact->probes)
      probe->contact->probes = probe->next;
    probe->next = client_probe_list;
    client_probe_list = probe;
    ++client_probe_count;
    while (probe->sock.err == SIM_OK && simself.state != CLIENT_STATE_STOPPED) {
      if (client_probe_count <= socket_max_limit[SOCKET_MAX_PROBES] && socket_open (&probe->sock, NULL) == SIM_OK)
        break;
      pth_sleep_ (1);
    }
    if (probe->sock.err != SIM_OK || simself.state == CLIENT_STATE_STOPPED) { /* handle cancel request */
      LOG_DEBUG_ ("cancelled $%d %s:%p (error %d) '%s'\n", probe->sock.fd,
                  CLIENT_LOOKUP_PROBE_NAME (probe->type), probe, probe->sock.err, probe->contact->nick.str);
      client_probe_free (probe);
    } else {
      simtype str = string_copy (network_convert_ip (probe->ip));
      LOG_DEBUG_ ("start $%d %s:%p '%s'\n", probe->sock.fd, CLIENT_LOOKUP_PROBE_NAME (probe->type), probe,
                  probe->contact->nick.str);
      if (_pth_thread_spawn (thread_probe_, probe, NULL, str, probe->port) != SIM_OK)
        client_probe_free (probe);
    }
  }
  return pth_thread_exit_ (false);
}

void client_loop_ (simclient client, simbool nat) {
  int count = 1;
  client->flags |= CLIENT_FLAG_CONNECTED;
  while (client->sock->err == SIM_OK && client->contact->clients != 1) {
    if (client->contact->clients != count)
      LOG_DEBUG_ ("loop enter $%d (count = %d) '%s'\n", client->sock->fd,
                  client->contact->clients, client->contact->nick.str);
    count = client->contact->clients;
    pth_usleep_ (100000);
  }
  if (client->sock->err == SIM_OK) {
    if (nat) {
      nat_init (client);
    } else if (! (client->flags & CLIENT_FLAG_ACCEPTED))
      client_send_cmd (client, SIM_CMD_REVERSE, NULL, nil (), NULL, nil ());
    server_loop_ (client);
    nat_uninit_ (client);
  } else /* the other thread has cancelled this loop */
    LOG_DEBUG_ ("loop exit $%d (error %d) '%s'\n", client->sock->fd, client->sock->err, client->contact->nick.str);
}

int client_connect_ (simclient client, simcontact contact) {
  unsigned seq = ++client_connect_sequence;
  int err, ret = main_search_start (contact->addr, seq, MAIN_MODE_ACTIVE);
  simbool search = main_get_status () != MAIN_DHT_STOPPED;
  simclient tmp;
  simnumber tick = system_get_tick ();
  network_get_ip ();
  err = limit_test_client_ (NULL, contact);
  while (err == SIM_OK && (err = socket_open (client->sock, client)) == SIM_OK) {
    int fd = client->sock->fd;
    LOG_DEBUG_ ("search%u $%d started (%lld ms) '%s'\n", seq, fd, system_get_tick () - tick, contact->nick.str);
    if (simself.state != CLIENT_STATE_RUNNING || simself.status == SIM_STATUS_OFF) {
      err = SIM_CLIENT_CANCELLED;
      break;
    }
    err = client_handshake_probe_ (client->sock, contact, 0, 0, SIM_PROTO_PORT, CLIENT_PROBE_CONNECT);
    if (err == SIM_OK || err == SIM_CLIENT_LOCAL) {
      if ((err = contact == contact_list.me ? SIM_CLIENT_BAD_VERSION : SIM_OK) != SIM_OK)
        break;
      if ((tmp = client_find (contact, CLIENT_FLAG_ACCEPTED | CLIENT_FLAG_CONNECTED)) != NULL) {
        if (client->flags & SIM_REPLY_FLAG_CONNECT && strcmp (contact->addr, contact_list.me->addr) >= 0) {
          LOG_DEBUG_ ("search%u $%d:$%d disconnected (%lld ms) '%s'\n", seq, fd, tmp->sock->fd,
                      system_get_tick () - tick, contact->nick.str);
          break;
        }
        LOG_DEBUG_ ("search%u $%d:$%d connected (%lld ms) '%s'\n", seq, fd, tmp->sock->fd,
                    system_get_tick () - tick, contact->nick.str);
        socket_cancel (tmp->sock, SIM_CLIENT_DROPPED);
      } else
        LOG_DEBUG_ ("search%u $%d connected (%lld ms) '%s'\n", seq, fd, system_get_tick () - tick, contact->nick.str);
      client_probe_cancel (NULL, contact, SIM_CLIENT_CONNECTED);
      client_loop_ (client, socket_check_client (client->sock) != 0);
      break;
    }
    if ((tmp = client_find (contact, CLIENT_FLAG_ACCEPTED | CLIENT_FLAG_CONNECTED)) != NULL) {
      LOG_DEBUG_ ("search%u $%d:$%d succeeded (%lld ms) '%s'\n", seq, fd, tmp->sock->fd,
                  system_get_tick () - tick, contact->nick.str);
      err = SIM_OK;
      break;
    }
    socket_close (client->sock, contact);
    if (err == SIM_CLIENT_UNKNOWN) {
      LOG_DEBUG_ ("search%u $%d restarted (%lld ms) '%s'\n", seq, fd, system_get_tick () - tick, contact->nick.str);
      err = SIM_OK;
    } else if (search) {
      unsigned ip = contact->ips ? contact->ip->ip : 0, port = contact->ips ? contact->ip->port : 0;
      int timeout = param_get_number ("client.timeout");
      LOG_DEBUG_ ("search%u $%d error %d (%lld ms) '%s'\n", seq, fd, err, system_get_tick () - tick, contact->nick.str);
      if (timeout && (timeout -= (int) (system_get_tick () - tick) / 1000) <= 0)
        timeout = 1;
      while (--timeout && (ret == MAIN_SEARCH_FAILED || (search = main_search_test (contact->addr, seq)) != false)) {
        if (client->sock->err != SIM_OK) {
          err = client->sock->err;
          timeout = 0;
          break;
        }
        pth_sleep_ (1);
        if (contact->ips && (contact->ip->ip != ip || contact->ip->port != port))
          break;
        if (client_find (contact, CLIENT_FLAG_ACCEPTED | CLIENT_FLAG_CONNECTED))
          break;
        if (ret == MAIN_SEARCH_FAILED)
          ret = main_search_start (contact->addr, seq, MAIN_MODE_ACTIVE);
      }
      if (timeout) {
        err = SIM_OK;
        if ((tmp = client_find (contact, CLIENT_FLAG_ACCEPTED | CLIENT_FLAG_CONNECTED)) != NULL) {
          LOG_DEBUG_ ("search%u $%d:$%d reconnected (%lld ms) '%s'\n", seq, fd, tmp->sock->fd,
                      system_get_tick () - tick, contact->nick.str);
          break;
        }
      }
    }
    if (err != SIM_OK) {
      LOG_DEBUG_ ("search%u $%d failed %d (%lld ms) '%s'\n", seq, fd, err,
                  system_get_tick () - tick, contact->nick.str);
      if (client->sock->err == SIM_OK)
        contact_set_status (contact, SIM_STATUS_INVISIBLE, number_new (contact->flags));
      err = SIM_CLIENT_NOT_FOUND;
    } else
      err = limit_test_client_ (NULL, contact);
  }
  if (ret == MAIN_SEARCH_FAILED && err == SIM_CLIENT_NOT_FOUND) {
    contact->dht.search = system_get_tick ();
    contact->dht.active = true;
  }
  return err;
}

simclient client_find (simcontact contact, int flags) {
  simclient client;
  for (client = client_list; client; client = client->next)
    if (client->contact == contact && client->flags & flags)
      if (client->sock->err == SIM_OK || flags & CLIENT_FLAG_MSG)
        break;
  return client;
}

simclient client_find_connecting (simcontact contact, simbool connected) {
  simclient client;
  for (client = client_list; client; client = client->next)
    if (client->contact == contact && ! (client->flags & (CLIENT_FLAG_ACCEPTED | CLIENT_FLAG_CONNECTED)))
      if (! connected || client->flags & CLIENT_FLAG_FORWARD)
        break;
  return client;
}

simbool client_find_local (unsigned ip, int port) {
  unsigned localip;
  int localport;
  simclient client;
  struct client_probe *probe;
  for (probe = client_probe_list; probe; probe = probe->next)
    if (socket_get_addr (&probe->sock, &localip, &localport) == SIM_OK && ip == localip && port == localport) {
      probe->sock.flags |= SOCKET_FLAG_LOCAL;
      return true;
    }
  for (client = client_list; client; client = client->next)
    if (socket_get_addr (client->sock, &localip, &localport) == SIM_OK && ip == localip && port == localport) {
      client->sock->flags |= SOCKET_FLAG_LOCAL;
      return true;
    }
  return false;
}

simnumber client_get_stat (simcontact contact, unsigned j, simbool oldstat) {
  simclient client;
  simnumber s = 0;
  for (client = client_list; client; client = client->next)
    if (client->contact == contact && client->flags & CLIENT_FLAG_CONNECTED) {
      if (! oldstat) {
        s += SOCKET_GET_STAT (client->sock, j);
      } else if (socket_check_client (client->sock))
        s += socket_get_stat (client->sock, j, true);
    }
  return s;
}

simtype client_get_state (simclient client, unsigned *proxyip) {
  unsigned ip = *proxyip = 0;
  const char *status = nat_get_status (client), *ipstr;
  if (socket_check_client (client->sock)) {
    ip = client->proxyip;
  } else if (socket_check_server (client->sock)) {
    proxy_get_proxy (&ip, NULL, NULL);
  } else
    return pointer_new (*status ? status + 1 : CLIENT_LOOKUP_STATE_NAME (client));
  ipstr = network_convert_ip (*proxyip = ip);
  return string_concat (*status ? status + 1 : CLIENT_LOOKUP_STATE_NAME (client), "/", ipstr, NULL);
}

simnumber client_get_param (int param) {
  simnumber ret = 0;
  simclient client;
  if (param == CLIENT_PARAM_XFER_NAT) {
    for (client = client_list; client; client = client->next)
      ret += socket_check_server (client->sock) && client->flags & CLIENT_FLAG_TRAVERSING;
  } else if (param == CLIENT_PARAM_XFER_SIZE)
    for (client = client_list; client; client = client->next)
      ret += (client->xfer.size + 4095) & -4096;
  return ret;
}

void client_set_param (simclient client, int sample, int state) {
  int oldstate;
  if (! client) {
    if (state == CLIENT_PARAM_XFER_NAT) {
      for (client = client_list; client; client = client->next)
        xfer_check_unlock (client);
    } else if (state == CLIENT_PARAM_XFER_SIZE)
      for (client = client_list; client; client = client->next)
        client->xfer.files = 0;
    return;
  }
  oldstate = client->call.state;
  if (state == AUDIO_CALL_HANGUP) {
    client->call.time = 0;
  } else if (((oldstate == AUDIO_CALL_HANGUP && (state == AUDIO_CALL_INCOMING || state == AUDIO_CALL_OUTGOING)) ||
              ((oldstate == AUDIO_CALL_INCOMING || oldstate == AUDIO_CALL_OUTGOING) && state == AUDIO_CALL_TALKING)))
    client->call.time = time (NULL);
  if (state != AUDIO_CALL_INCOMING && state != AUDIO_CALL_OUTGOING) {
    int rcvtimeout = param_get_number ("socket.recv") - (client->flags & CLIENT_FLAG_ACCEPTED ? 2 : 0);
    client->sock->rcvtimeout = client->rcvtimeout && client->rcvtimeout < rcvtimeout ? client->rcvtimeout : rcvtimeout;
  } else
    client->sock->rcvtimeout = param_get_number ("client.ring") - (client->flags & CLIENT_FLAG_ACCEPTED ? 2 : 0);
  if ((state == AUDIO_CALL_OUTGOING && oldstate != AUDIO_CALL_OUTGOING) ||
      (state == AUDIO_CALL_TALKING && oldstate == AUDIO_CALL_INCOMING)) {
    client->contact->connected++;
    LOG_DEBUG_ ("connected%d $%d '%s'\n", client->contact->connected, client->sock->fd, client->contact->nick.str);
  }
  if (state == AUDIO_CALL_HANGUP && (oldstate == AUDIO_CALL_OUTGOING || oldstate == AUDIO_CALL_TALKING)) {
    LOG_ANY_ (client->contact->connected > 0 ? SIM_LOG_DEBUG : SIM_LOG_ERROR, "disconnected%d $%d '%s'\n",
              client->contact->connected, client->sock->fd, client->contact->nick.str);
    client->contact->connected -= client->contact->connected > 0;
  }
  client->call.sample = sample;
  if (state != oldstate) {
    client->flags &= ~CLIENT_FLAG_UDP;
    client->call.udport = 0;
    event_send_audio (client->contact, audio_state_names[oldstate], audio_state_names[client->call.state = state]);
  }
  if (state == AUDIO_CALL_HANGUP && client_proxy_error != SIM_OK)
    client_cancel_proxy (client_proxy_error);
}

int client_set_socket_ (simclient client, simsocket sock) {
  int err;
  simsocket oldsock = client->sock, local = SOCKET_GET_STAT_SOCKET (oldsock);
  simcustomer customer = proxy_customer_acquire (socket_check_server (oldsock));
  void *lock = sock->lock;
  ssl_free (sock);
  sock->udport = oldsock->udport;
  sock->ip = oldsock->ip;
  sock->flags = oldsock->flags;
  sock->rcvtimeout = oldsock->rcvtimeout;
  sock->created = oldsock->created;
  sock->pinged = oldsock->pinged;
  sock->lock = oldsock->lock;
  sock->master = oldsock->master;
  oldsock->lock = lock;
  oldsock->master = NULL;
  oldsock->crypt.decrypt = oldsock->crypt.encrypt = NULL;
  oldsock->crypt.ivs = nil ();
  server_set_qos (sock, oldsock->qos.tos);
  server_set_qos (oldsock, 0);
  socket_close (oldsock, SOCKET_NO_SESSION);
  client->sock = sock;
  audio_reset (client);
  client->flags &= ~CLIENT_FLAG_BUFFERING;
  err = client_send_ (client, nil ());
  while (local->fd != INVALID_SOCKET)
    pth_usleep_ (10000);
  SOCKET_ADD_STAT (sock, local);
  SOCKET_ADD_STAT (&sock->old, &local->old);
  sim_free (oldsock, sizeof (*oldsock));
  proxy_customer_release (customer, SIM_OK);
  return err;
}

void client_cancel_proxy (int error) {
#if HAVE_LIBSPEEX
  simclient client;
  if (client_proxy_error != error && proxy_get_proxy (NULL, NULL, NULL))
    LOG_DEBUG_ ("proxy disconnect (error %d)\n", error);
  if (client_proxy_error != SIM_PROXY_BLACKLISTED && client_proxy_error != SIM_PROXY_BLOCKED)
    client_proxy_error = error;
  for (client = client_list; client; client = client->next)
    if (client->call.state != AUDIO_CALL_HANGUP && socket_check_server (client->sock))
      return;
  client_proxy_error = SIM_OK;
#endif
  proxy_cancel_proxy (NULL, error);
}

simclient client_cancel_contact (simcontact contact, int error) {
  simclient client, cancelled = NULL;
  for (client = client_list; client; client = client->next)
    if (client->contact == contact && client->sock->err == SIM_OK) {
      if (error == SIM_NAT_REVERSE_CANCELLED && ! (client->flags & CLIENT_FLAG_REVERSE))
        continue;
      if (error == SIM_SERVER_DROPPED && ! (client->flags & (CLIENT_FLAG_ACCEPTED | CLIENT_FLAG_CONNECTED)))
        if (client->flags & (CLIENT_FLAG_FORWARD | CLIENT_FLAG_REVERSE))
          continue;
      socket_cancel (client->sock, error);
      cancelled = client;
      LOG_DEBUG_ ("drop $%d %s (error %d) '%s'\n", client->sock->fd,
                  client->flags & CLIENT_FLAG_REVERSE ? "REV" : CLIENT_LOOKUP_STATE_NAME (client),
                  error, contact->nick.str);
    }
  /* do not need to connect since i am already connected.
     this prevents probe thread from setting contact status to invisible, if it fails */
  if (error != SIM_NAT_REVERSE_CANCELLED)
    client_probe_cancel (NULL, contact, error);
  return cancelled;
}

int client_cancel_probes (int status) {
  int count = 0;
  struct client_probe *probe;
  for (probe = client_probe_list; probe; probe = probe->next)
    if (status == SIM_STATUS_INVISIBLE || (status == SIM_STATUS_OFF) ^ (probe->type == CLIENT_PROBE_LOGOFF)) {
      socket_cancel (&probe->sock, SIM_CLIENT_OFFLINE);
    } else
      ++count;
  return count;
}

int client_cancel_local (simcustomer local, simsocket sock) {
  static const char zero = 0;
  int err;
  simclient client;
  for (client = client_list; client && ! sock; client = client->next)
    if (socket_check_server (client->sock) == local)
      sock = client->sock;
  if (! sock)
    return SIM_SOCKET_NO_ERROR;
  if ((err = socket_select_writable (sock->fd)) > 0)
    if ((err = send (sock->fd, &zero, sizeof (zero), 0)) == sizeof (zero))
      return SIM_OK;
  if (err)
    err = socket_get_errno ();
  return err ? err : SIM_SOCKET_BLOCKED;
}

void client_cancel (simclient client, int error) {
  if (! client) {
    for (client = client_list; client; client = client->next)
      client_cancel (client, error);
    return;
  }
  if (client->sock->err == SIM_OK)
    if ((client->flags & (CLIENT_FLAG_CONNECTED | CLIENT_FLAG_BUFFERING)) == CLIENT_FLAG_CONNECTED) {
      simtype table = client_new_cmd (SIM_CMD_BYE, NULL, nil (), NULL, nil ());
      socket_send (client->sock, table, SOCKET_SEND_TCP, NULL);
      table_free (table);
      client->flags &= ~CLIENT_FLAG_ERROR;
    }
  socket_cancel (client->sock, error); /* client will be released when thread exits because of cancelled socket */
  LOG_DEBUG_ ("disconnecting%d $%d (error %d) '%s'\n", client->count, client->sock->fd, error,
              client->contact->nick.str);
}

static void client_periodic_typing (simclient client, simnumber tick) {
  int timeout;
  if (client->contact->flags & CONTACT_FLAG_TYPE && (timeout = abs (param_get_number ("rights.typing")) * 1000) != 0) {
    if (client->typingping && tick - client->typingping >= timeout) {
      simnumber flags = client->contact->flags & ~CONTACT_FLAG_TYPE;
      contact_set_status (client->contact, client->contact->status, number_new (flags));
      LOG_DEBUG_ ("typing timeout (%lld ms)\n", tick - client->typingping);
    }
    if (! client->typingping && tick - client->pongtick >= timeout) {
      client->typingping = tick;
      client_send_cmd (client, SIM_CMD_PING, SIM_CMD_PING_PONG, number_new (0),
                       SIM_CMD_PING_PONG_NUMBER, number_new (SIM_CMD_PING_NUMBER_TYPING));
    }
  }
}

void client_periodic (simclient client, simnumber tick) {
  if (client->msgqueue) {
    simcontact contact = client->contact;
    simnumber reap = contact->auth >= CONTACT_AUTH_ACCEPTED ? param_get_number_min ("client.reap", 60) : -1;
    simunsigned timeout = (reap += reap > 0 && client->flags & CLIENT_FLAG_ACCEPTED ? 2 : 0) * 1000;
    client_periodic_typing (client, tick);
    msg_send_ack (client, tick);
    if (reap && ! contact->connected)
      if (reap < 0 ? ! xfer_check_pending (contact, client->xfer.cid) : (simunsigned) tick >= client->tick + timeout)
        client_cancel (client, SIM_CLIENT_IDLE);
    nat_periodic (client, tick);
  }
}

void client_logoff (int status) {
  unsigned i;
  for (i = contact_list.first; i; i = ((simcontact) contact_list.array.arr[i].ptr)->next) {
    simcontact contact = contact_list.array.arr[i].ptr;
    simclient client = contact->client;
    if (client) {
      simtype table = nil ();
      if (status == SIM_STATUS_ON) {
        client_send_status (client, CLIENT_SEND_STATUS_OFF);
        contact = NULL;
      } else if (! (client->flags & CLIENT_FLAG_BUFFERING))
        if (socket_send (client->sock, table = client_new_status (client), SOCKET_SEND_TCP, NULL) == SIM_OK) {
          client->flags &= ~CLIENT_FLAG_ERROR;
          contact = NULL;
        }
      table_free (table);
    }
    if (contact && contact->auth >= CONTACT_AUTH_ACCEPTED && contact->ips && contact != contact_list.me)
      if (status != SIM_STATUS_OFF && (client || contact->status > SIM_STATUS_OFF))
        client_probe (contact, contact->ip->ip, contact->ip->port, NULL, CLIENT_PROBE_LOGOFF);
  }
}

void client_logon (simbool now) {
  if (! now) {
    client_logon_flag = true;
    main_dht_logon ();
  } else if (simself.state == CLIENT_STATE_RUNNING && simself.status != SIM_STATUS_OFF) {
    unsigned i = contact_list.first;
    for (;;) {
      simcontact contact;
      for (;;) {
        if (! i) { /* poke all clients so they will receive errors and disconnect soon */
          client_send_status (NULL, CLIENT_SEND_STATUS_OFF);
          return;
        }
        i = (contact = contact_list.array.arr[i].ptr)->next;
        if (contact->auth >= CONTACT_AUTH_ACCEPTED && contact->status > SIM_STATUS_OFF && ! contact->client)
          break;
      }
      client_probe (contact, 0, 0, NULL, CLIENT_PROBE_LOGON);
    }
  }
}

static int client_logon_sleep_ (int count, int seconds, simbool wake) {
  while (seconds-- && simself.state != CLIENT_STATE_STOPPED) {
    count++;
    pth_sleep_ (1);
    contact_periodic ();
    if (wake && client_logon_flag)
      break;
  }
  /*while (! table_init_hash ((unsigned) random_get_number (random_session, 0xFFFFFFFF))) {}*/
  xfer_periodic (true);
  network_periodic (NETWORK_CMD_IP);
  if (count > param_get_number ("contact.save")) {
    if (event_test_error (NULL, SIM_EVENT_ERROR_FILE_SAVE, contact_list_save ()) == SIM_OK) {
      proxy_list_save ();
      random_save ();
    }
    count = 0;
  }
  return simself.state != CLIENT_STATE_STOPPED ? count : -1;
}

static int client_logon_loop_ (int count, simbool all) {
  unsigned i = contact_list.first;
  while (simself.state != CLIENT_STATE_STOPPED) {
    simcontact contact;
    do {
      if (! i)
        return count;
      i = (contact = contact_list.array.arr[i].ptr)->next;
    } while (all != (contact->tick != -1) || contact->auth < CONTACT_AUTH_ACCEPTED || contact->client);
    while (main_search_start (contact->addr, 0, MAIN_MODE_PASSIVE) == MAIN_SEARCH_FAILED)
      if ((count = client_logon_sleep_ (count, param_get_number ("main.research"), false)) == -1)
        break;
    client_probe (contact, 0, 0, NULL, CLIENT_PROBE_LOGON);
  }
  return count;
}

static simcontact client_logon_get_next (simcontact contact, simbool all) {
  if (contact->auth < CONTACT_AUTH_ACCEPTED || contact->client)
    return NULL;
  if (! all && ! xfer_check_pending (contact, 0) && ! (contact->flags & CONTACT_FLAG_NEW))
    return NULL;
  if (contact->tick && contact->tick != -1) {
    simunsigned tick = system_get_tick ();
    if ((simunsigned) contact->tick <= tick && tick < (simunsigned) contact->tick + contact->logon * 1000)
      return NULL;
  }
  return contact;
}

static unsigned client_logon_walk (unsigned idx, simbool all) {
  int port;
  unsigned ip;
  simcontact contact = NULL;
  if (! idx)
    idx = contact_list.first;
  if (idx) {
    simbool sent = false;
    while (client_logon_get_next (contact = contact_list.array.arr[idx].ptr, all) == NULL) {
      if (all && contact->auth >= CONTACT_AUTH_ACCEPTED && contact != contact_list.me && contact->client) {
        const char *addr;
        if (! sent && ! contact->verify && proxy_get_ip_proxy (&addr, &ip, &port) && ! sim_network_check_local (ip))
          if (CLIENT_CHECK_VERIFY () && (! addr || strcmp (contact->addr, addr))) {
            client_send_cmd (contact->client, SIM_CMD_REQUEST_VERIFY, SIM_CMD_REQUEST_VERIFY_IP,
                             string_copy (network_convert_ip (ip)), SIM_CMD_REQUEST_VERIFY_PORT, number_new (port));
            if (contact->client->flags & SIM_REPLY_FLAG_VERIFY_OK) {
              simnumber tick = system_get_tick ();
              contact->verify = tick + (param_get_number ("client.verified") + contact->handshake) * 1000;
              LOG_DEBUG_ ("verify $%d (%lld seconds) '%s'\n", contact->client->sock->fd,
                          (contact->verify - tick) / 1000, contact->nick.str);
              sent = true;
            } else if (! (contact->client->flags & SIM_REPLY_FLAG_VERIFY_NOK))
              sent = true;
          }
        if (nat_get_connected (contact->client) == CLIENT_FLAG_CONNECTED)
          client_send_cmd (contact->client, SIM_CMD_REQUEST_PROXY, NULL, nil (), NULL, nil ());
      }
      if ((idx = contact->next) == 0)
        break;
    }
    if (idx && contact->auth >= CONTACT_AUTH_ACCEPTED && ! contact->client) {
      if (! contact->dht.search)
        contact->dht.search = system_get_tick () + param_get_number ("main.research") * 1000;
      client_probe (contact, 0, 0, NULL, CLIENT_PROBE_LOGON);
      if (all && contact == contact_list.me && ! (contact_list.test & 0x4000000))
        if (proxy_get_proxy (&ip, &port, NULL) && audio_get_param (AUDIO_PARAM_SPEED) <= 0)
          client_probe (contact, ip, port, NULL, CLIENT_PROBE_LOGON);
    }
  }
  return idx ? contact->next : 0;
}

static void *thread_logon_ (void *arg) {
  int count = 0, sec;
  unsigned i, j = i = contact_list.first;
  LOG_API_DEBUG_ ("\n");
  if (! i)
    LOG_FATAL_ (SIM_OK, "contact list not initialized\n");
  client_logon (false);
  while (simself.state != CLIENT_STATE_STOPPED) {
    if (client_logon_flag) { /* explicit logon request (fast logon) */
      LOG_DEBUG_ ("start LOGON loop\n");
      for (i = 1; i <= contact_list.array.len; i++) { /* first mark contacts i have something to send to */
        simcontact contact = contact_list.array.arr[i].ptr;
        if (contact->auth >= CONTACT_AUTH_ACCEPTED && ! contact->client)
          if (xfer_check_pending (contact, 0) || contact->flags & CONTACT_FLAG_NEW)
            contact->tick = -1;
      }
      count = client_logon_loop_ (count, true);  /* try to connect to these */
      count = client_logon_loop_ (count, false); /* try to connect to all others */
      LOG_DEBUG_ ("stop LOGON loop\n");
      client_logon_flag = false;
      if (simself.state == CLIENT_STATE_STOPPED)
        break;
      j = i = contact_list.first;
    }
    sec = param_get_number ("client.logon");
    count = client_logon_sleep_ (count, sec > 1 ? (int) random_get_number (random_public, sec) + sec / 2 : 1, true);
    if (sec > 0 && ! client_logon_flag) {
      j = client_logon_walk (j, false); /* search for contacts i have something to send to */
      i = client_logon_walk (i, true);  /* search for all contacts at the same time */
    }
  }
  return pth_thread_exit_ (false);
}

int client_init_ (void) {
  int err = SIM_CLIENT_INIT;
  LOG_DEBUG_ ("init\n");
  if (simself.state == CLIENT_STATE_STOPPED && (err = socket_init ()) == SIM_OK) {
    client_proxy_error = SIM_OK;
    client_connect_sequence = 0;
    if ((err = pth_queue_new (&client_probe_queue, &client_probe_event, -1)) == SIM_OK)
      if ((err = server_init_ (true)) == SIM_SERVER_PORT && simself.status != SIM_STATUS_OFF) {
        simself.status = SIM_STATUS_OFF;
        event_send_name (NULL, SIM_EVENT_ERROR, SIM_EVENT_ERROR_INIT_LOCAL, number_new (SIM_SERVER_PORT));
      }
    if ((err == SIM_OK && (simself.status != SIM_STATUS_OFF || (err = server_uninit_ (false)) == SIM_OK)) ||
        err == SIM_SERVER_PORT) {
      if ((err = pth_thread_spawn (thread_queue_, NULL, &tid_queue, -1)) == SIM_OK) {
        simself.ipclient = table_new (31);
        random_get (random_public, pointer_new_len (&simself.nonce, sizeof (simself.nonce)));
        simself.nonce[0] = (unsigned) simself.nonce[0], simself.nonce[1] = (unsigned) simself.nonce[1];
        LOG_DEBUG_ ("my nonce #%lld #%lld\n", simself.nonce[0], simself.nonce[1]);
        if ((err = pth_thread_spawn (thread_logon_, NULL, &tid_logon, -1)) == SIM_OK) {
          simself.state = CLIENT_STATE_RUNNING;
          if ((err = proxy_list_init_ ()) == SIM_OK) {
            network_init ();
            return SIM_OK; /* if uPNP thread won't start, fuck it */
          }
          simself.state = CLIENT_STATE_STOPPED;
          pth_thread_join_ (&tid_logon, NULL, thread_logon_, -1);
        }
        client_probe (NULL, 0, 0, NULL, CLIENT_PROBE_LOGOFF);
        pth_thread_join_ (&tid_queue, NULL, thread_queue_, -1);
        table_free (simself.ipclient), simself.ipclient = nil ();
      }
      server_uninit_ (true);
      random_init (random_private);
    }
    if (err != SIM_OK) {
      pth_queue_free (&client_probe_queue, &client_probe_event, sizeof (struct client_probe));
      proxy_uninit ();
      limit_uninit ();
      socket_uninit ();
    }
  } else
    LOG_ERROR_ ("init error %d\n", err);
  return err;
}

int client_uninit_ (simbool init) {
  const char *done = "completed";
  int err = SIM_API_EXIT, customers;
  LOG_CODE_DEBUG_ (pth_log_threads (SIM_MODULE, SIM_LOG_DEBUG));
  if (simself.state != CLIENT_STATE_STOPPED) {
    client_cancel (NULL, SIM_CLIENT_OFFLINE);
    if (init) {
      simnumber tick = system_get_tick ();
      int logoff = param_get_number ("client.logoff") * 1000;
      err = SIM_OK;
      do {
        if (! client_cancel_probes (SIM_STATUS_OFF) && ! pth_msgport_pending (client_probe_queue))
          if (! limit_count_customer (PROXY_TYPE_LOCAL) && ! client_list)
            break;
        if (logoff >= 0 && (simnumber) system_get_tick () - tick >= logoff) {
          done = "aborted";
          break;
        }
        pth_usleep_ (10000);
        if (simself.state == CLIENT_STATE_STOPPED)
          err = SIM_API_EXIT;
      } while (err == SIM_OK);
    }
    simself.state = CLIENT_STATE_STOPPED;
    client_cancel_probes (simself.status = SIM_STATUS_OFF); /* logoff finished - no connections after this point */
    client_probe (NULL, 0, 0, NULL, CLIENT_PROBE_LOGOFF);
    /* kill customers only after logoff to prevent cancelling remote's probe (see below) */
    customers = proxy_customer_cancel (NULL, SIM_CLIENT_OFFLINE);
    /* make sure contact list is saved even in case of deadlock below (which shouldn't happen) */
    event_test_error (NULL, SIM_EVENT_ERROR_FILE_SAVE, contact_list_save ());
    if (err == SIM_OK) {
      unsigned i;
      /* close server port only after logoff to make sure peers won't get a fake logoff (invisible) event before the real one */
      server_uninit_ (true);
      network_set_port (NETWORK_CMD_QUIT);
      if (client_list || client_probe_list || customers) {
        LOG_WARN_ ("logoff %s (%d clients, %d probes, %d customers)\n", done,
                   client_count (NULL, NULL, NULL), client_probe_count, customers);
        LOG_CODE_DEBUG_ (client_log_clients (SIM_MODULE, SIM_LOG_DEBUG, false));
        LOG_CODE_DEBUG_ (client_log_probes (SIM_MODULE, SIM_LOG_DEBUG));
        /* customers, clients and probes point to contacts so they cannot be left running wild just before freeing contact list */
        client_probe_cancel (NULL, NULL, SIM_SOCKET_CANCELLED);
        while (client_list || client_probe_list || proxy_customer_cancel (NULL, SIM_SOCKET_CANCELLED))
          pth_usleep_ (10000);
      }
      xfer_save ();
      main_uninit_ (true);
      if (pth_thread_join_ (&tid_logon, NULL, thread_logon_, -1) == SIM_OK)
        if (pth_thread_join_ (&tid_queue, NULL, thread_queue_, -1) == SIM_OK) {
          pth_queue_free (&client_probe_queue, &client_probe_event, sizeof (struct client_probe));
          table_free (simself.ipclient), simself.ipclient = nil ();
        }
      proxy_list_uninit_ ();
      network_uninit_ ();
      limit_uninit ();
      for (i = 1; i <= contact_list.array.len; i++)
        msg_close (contact_list.array.arr[i].ptr);
      socket_uninit ();
      table_init_hash (0);
    } else {
      LOG_WARN_ ("logoff terminated (%d clients, %d probes, %d customers)\n",
                 client_count (NULL, NULL, NULL), client_probe_count, customers);
      xfer_save ();
      proxy_list_save ();
    }
    random_save ();
  } else {
    LOG_WARN_ ("logoff terminated (%d clients, %d probes)\n", client_count (NULL, NULL, NULL), client_probe_count);
    client_cancel (NULL, SIM_CLIENT_OFFLINE);
  }
  if (err != SIM_OK) {
    simclient client, next;
    LOG_CODE_DEBUG_ (client_log_clients (SIM_MODULE, SIM_LOG_DEBUG, false));
    for (client = client_list; client; client = next) {
      msg_stop_thread_ (client_acquire (client));
      next = client->next;
      client_release (client);
    }
  } else
    random_init (random_private);
  simself.oldflags = simself.flags = 0;
  LOG_CODE_DEBUG_ ((network_log_flags (SIM_MODULE, SIM_LOG_DEBUG), pth_log_threads (SIM_MODULE, SIM_LOG_DEBUG)));
  return err;
}

void client_log_clients (const char *module, int level, simbool sessions) {
  simclient client;
  for (client = client_list; client; client = client->next) {
    if (! sessions && (client->buffer.len || client->flags & CLIENT_FLAG_BUFFERING)) {
      log_any_ (module, level, "%s:", client->contact->nick.str);
      if (client->flags & CLIENT_FLAG_BUFFERING)
        log_any_ (module, level, " BUFFERING");
      log_simtype_ (module, level, client->buffer, 0, " ");
    }
  }
  for (client = client_list; client; client = client->next) {
    simsocket sock = client->sock;
    struct _ssl_master *master = sock->master;
    log_any_ (module, level, "%c%d:", CLIENT_CASE_FLAG_NAME (sock), sock->fd);
    if (! sock->udport && socket_check_server (sock)) {
      log_any_ (module, level, "$%d", socket_check_server (sock)->sock->fd);
    } else
      log_any_ (module, level, "%d", sock->udport);
    log_any_ (module, level, " %s %s%s%s %s", client->contact->nick.str,
              client->flags & CLIENT_FLAG_REVERSE ? "REV" : CLIENT_LOOKUP_STATE_NAME (client),
              nat_get_status (client), SOCKET_CHECK_PROXY (sock) ? "/PXY" : "",
              client->call.state == AUDIO_CALL_HANGUP ? "hup" : audio_state_names[client->call.state]);
    if (! sessions) {
      simnumber seen;
      if (client->msgqueue && pth_msgport_pending (client->msgqueue))
        log_any_ (module, level, " [%d]", pth_msgport_pending (client->msgqueue));
      if ((seen = (system_get_tick () - client->tick) / 1000) >= 0)
        log_any_ (module, level, " <%lld:%02d:%02d>", seen / 3600, (int) (seen / 60 % 60), (int) (seen % 60));
      if (! module)
        log_any_ (module, level, " @%020llu", client->contact->id);
      log_any_ (module, level, " [%d/%d %d/%d %lld]", client->xfer.namesent, client->xfer.names,
                client->xfer.namercvd, client->contact->xfer.names, client->xfer.files ? client->xfer.files - 1 : 0);
      if (client->count != 1)
        log_any_ (module, level, " *%d", client->count);
    } else {
      int i;
      struct _socket_stat tmp;
      log_any_ (module, level, " 0x%04X %s:%s", client->flags, sock->crypt.rcvcipher, sock->crypt.sndcipher);
      if (! module && master && master->key.typ != SIMNIL) {
        simtype key = pointer_new_len (master->premaster, sizeof (master->premaster) / 2);
        simtype addr = pointer_new_len (master->random, sizeof (master->random));
        key = sim_crypt_md_hash (sim_crypt_md_new (CRYPT_MD_WHIRLPOOL), key, addr, master->key);
        addr = sim_contact_convert_to_address (pointer_new_len (key.str, CONTACT_ADDRESS_LENGTH), 'W');
        addr.str[addr.len - 4] = 0;
        string_free (key);
        log_any_ (module, level, " %s", addr.str);
        string_free (addr);
      }
      sock = SOCKET_GET_STAT_SOCKET (sock);
      SOCKET_INIT_STAT (&tmp);
      SOCKET_ADD_STAT (&tmp, sock);
      for (i = 0; i <= ! ! socket_check_client (sock); i++) {
        log_any_ (module, level, "%s (%lld.%03d+%lld.%03d : %lld.%03d+%lld.%03d)", i ? " ->" : "",
                  tmp.rcvbytes / 1000, (int) (tmp.rcvbytes % 1000),
                  tmp.rcvheaders / 1000, (int) (tmp.rcvheaders % 1000),
                  tmp.sndbytes / 1000, (int) (tmp.sndbytes % 1000),
                  tmp.sndheaders / 1000, (int) (tmp.sndheaders % 1000));
        SOCKET_ADD_STAT (&tmp, &sock->old);
      }
      if (client->version != SIM_PROTO_VERSION_MAX)
        log_any_ (module, level, " %%%d", client->version);
    }
    log_any_ (module, level, "\n");
  }
}

void client_log_probes (const char *module, int level) {
  struct client_probe *probe;
  for (probe = client_probe_list; probe; probe = probe->next) {
    log_any_ (module, level, "%c%d %s %s %s:%d", CLIENT_CASE_FLAG_NAME (&probe->sock), probe->sock.fd,
              probe->contact->nick.str, CLIENT_LOOKUP_PROBE_NAME (probe->type),
              probe->host.typ != SIMNIL ? (char *) probe->host.str : network_convert_ip (probe->ip), probe->port);
    if (! module)
      log_any_ (module, level, " @%020llu", probe->contact->id);
    log_any_ (module, level, "\n");
  }
}
