
 "use strict";

 const console         = require('gifer-console');
 const MCServ          = require('net');
 const Service         = require('./service.js');
 const util            = require('util');
 const CMD             = require('../client/modules/cmd/cmd').CMD;
 const ClientToServCMD = require('./clientCMD').ClientToServCMD;
 const _serverCMD      = require('./serverCMD');
 const OS              = require('os');
 const tlsMCServ       = require('tls');
 const constants       = require('constants');
 const zlib            = require('zlib');
 const tokenStore      = require('./tokenStore');
 const DbWork          = require('./dbwork');

 process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

 // Команди, тіло яких не пишемо в лог у жодному вигляді — ні plaintext, ні base64 від gzip.
 // Набір спільний для всіх трьох синків: команда, метаданна в одному й тілесна в іншому — це
 // витік із двома шансами на помилку замість одного.
 const SECRET_CMD = [
     CMD.sc_token_created,            // токен від СЕРВЕРА
     CMD.cs_login_by_token,           // токен назад
     CMD.cs_login,                    // Pass + ServPass
     CMD.cs_register_new_user,        // пароль нового користувача
     CMD.cs_set_account_password,     // OldPass / NewPass
     CMD.cs_join_txt_channel,         // пароль конференції
     CMD.cs_create_txt_channel,       // пароль конференції
     CMD.cs_adm_create_user,          // пароль нового користувача, адмінська
     CMD.cs_adm_set_user_password,    // зміна пароля користувача, адмінська
     CMD.cs_adm_get_ad_users_list,    // пароль LDAP-акаунта
     CMD.cs_adm_add_auto_conf,        // пароль автоконференції
     CMD.cs_adm_update_auto_conf,     // пароль автоконференції
     CMD.cs_adm_change_mychat_server  // пароль сервера
 ];

 function isSecretCMD(cmdNumb){
     return SECRET_CMD.indexOf(cmdNumb) !== -1;
 }

 let CLIENT_TO_SERVER = 'client-to-server-cmd';
 let SERVER_TO_CLIENT = 'server-to-client-cmd';
 let ServToClientCMD  = _serverCMD.ServToClientCMD;
 let uui              = "bnQ4NTR0ODl1YzMgNTQ4IHVqMzB1NCByeTU5LTQ4ZmRrd3MgaWFqd2gzNGJ5NnQ4OS1pMWssWk9LSiBOYmF5emYgJSBedHNqemEoX3dlaFQ1ODcgICAyIEhFRC05SVNKQVUqKEFTR0hmNjc4OVIzSiBuZmVfKHVoZXFUNThRIDczSC8vU2VydmljZS5SY";

 let ProtocolVersion  = MCServer.ProtocolVersion;

 let currentConnect  = null;
 let CRLF            = "\r\n";
 let CR              = "\r";
 let LF              = "\n";
 let wcc             = null;
 let socketIO        = null;
 let errorBuffer     = null;
 let tryUpdateClient = null;
 let MagicPacket     = '\u0017\u0006';
 let SendingFlags    = '00';
 let hardwareID      = "";
 let clientInfo      = {};
 let directSendElectronClient = null;

 let legacyServers = global.MCServer.legacyList.concat([Service.MCServer.ClientVersion]);
 let serverVersion = null;
 var serverHWID    = "";

 MCServer.ClientVersion = legacyServers[legacyServers.length - 1];

 // ============================================================
 
 Service.LoadJSONFile(MCPathes.Service + 'textsource/errors/client/' + MCServer.Lang + '.errors.json', function(data)   {
     let text = data;

     if (text){
         global.MCServer.ErrorText = JSON.parse(text.toString());
     }

     console.log("Client version: " + MCServer.ClientVersion);
     global.jPackage && console.log("Client Build: " + global.jPackage.buildNumber);
 }, 'utf8');

 // === ClientToServCMD ========================================

 // Платформний тип клієнта. Єдине джерело для cs_hello і для контексту в web_client_connect.js
 function getClientType(){
     switch (OS.platform()){
         case "win32":  return CMD.ClientType.WIN32_NEXT;
         case "linux":  return CMD.ClientType.LINUX;
         case "darwin": return CMD.ClientType.MACOS; // 7.8 было "mac", стало "macos"
     }

     return '';
 }

 ClientToServCMD[CMD.cs_hello] = function(__socket){
     let mac    = '';
     let clientType = getClientType();

     try {
         let where = "";
         let intrfslst = OS.networkInterfaces();

         switch (OS.platform()){
             case "win32":  where = "Ethernet"; break;
             case "linux":  where = "eth0";     break;
             case "darwin": where = "en0";      break;

             default: where = "eth0";
         }

         // console.log("adapter list: " + JSON.stringify(intrfslst));

         if (intrfslst[where] && intrfslst[where].length){
             mac = intrfslst[where][0].mac.toUpperCase().replace(/[:]/g, "-");
         } else {
             mac = intrfslst[Object.keys(intrfslst)[0]][0].mac.toUpperCase().replace(/[:]/g, "-");
         }
     } catch (e){
         console.error(e.message);
         console.error(e.stack);
     }

     clientInfo = {
         // "osType"       : osType,
         // "HardwareID"   : osType + '-' + hardwareID,       // timastamp+XY HardwareID клиента
         // "Ver"          : serverVersion,                   // версия клиентского приложения
         ProtocolVer       : ProtocolVersion,                 // версия протокола клиента
         Client            : clientType,                      //__socket.UserInfo.ClientType,    // тип клиента, который подключается к серверу. win32 - Windows-клиент.
         Packed            : true,                            // использовать сжатие трафика или нет. По умолчанию - false
         MAC               : mac,                             // MAC адрес клиента
         HardwareID        : clientType + '-' + hardwareID,   // timastamp+XY HardwareID клиента
         NetName           : OS.hostname(),                   // сетевое имя компьютера
         Ver               : MCServer.ClientVersion,          // версия клиентского приложения
         OS                : global._linuxName || `${OS.type()} ${OS.release()}`, // название и версия операционной системы клиента
         Secured           : __socket.UserInfo.Secured || "", // использовать или нет зашифрованное соединение. Если пустая строка - шифрования нет
         UTC               : 0,                               // UTC смещение времени подключающегося клиента
         NodeRemoteIP      : __socket.UserInfo.clientIP,
         NodeUserAgent     : __socket.UserInfo.NodeUserAgent,
         NodeReferral      : __socket.UserInfo.NodeReferral,
         // MacUpdatePacketVer    : __socket.ServerInfo.MacUpdatePacketVer,
         // LinuxUpdatePacketVer  : __socket.ServerInfo.LinuxUpdatePacketVer,
         // Linux32UpdatePacketVer: __socket.ServerInfo.Linux32UpdatePacketVer,
         Interfaces        : []                               // список сетевых интерфейсов клиента
     };

     console.log(clientInfo, console.logLevel.L_High);

     global.isBrodcast = false;

     return MagicPacket + CMD.cs_hello + SendingFlags +
         JSON.stringify(clientInfo) + CRLF;
 };

 // === ServToClientCMD ========================================

 ServToClientCMD[CMD.sc_hello]                = function(_data){
     var socket = this;

     process.nextTick(function(){
         let isMac   = OS.platform() === "darwin";
         let isLinux = OS.platform() === "linux";
         let isWin32 = OS.platform() === "win32";

         socket.ServerInfo = _data;

         socket.UserInfo.HWID     = socket.ServerInfo.HWID;
         socket.UserInfo.ServerID = socket.ServerInfo.ID;

         MCServer.ServerID    = socket.UserInfo.ServerID;

         serverHWID           = socket.ServerInfo.HWID;
         MCServer.PortNode    = socket.ServerInfo.PortNode;
         MCServer.HTTPS       = socket.ServerInfo.HTTPS;
         MCServer.PortFTP     = socket.ServerInfo.PortFTP;
         MCServer.CompanyName = socket.ServerInfo.Registered;
         MCServer.ServerName  = socket.ServerInfo.ServName;
         MCServer.LicenseFree = socket.ServerInfo.Free;

         MCServer.CID         = socket.ServerInfo.CID;

         console.info("ServerID: " + socket.UserInfo.ServerID);
         console.log('Hello:\n' + JSON.stringify(socket.ServerInfo), console.logLevel.L_Full);

         // Гейт стоїть перед УСІМ if-ланцюгом, бо відправок cs_hello дві: очевидна в кінці ланцюга
         // і схована в linux-гілці /usr/lib/, де клієнт оновитись не може. Гейт лише перед другою
         // пропустив би пакетну linux-інсталяцію на парольний вхід із sentinel замість пароля.
         // sc_accept_connection може прилетіти раніше, ніж завершиться читання з БД, тому cs_hello
         // не йде, доки не виставлено authType
         resolveTokenLogin(socket).then(function (proceed) {

             // Токен непридатний і клієнт перезапускається — cs_hello не шлемо взагалі, інакше
             // sc_accept_connection відправить cs_login із sentinel замість пароля
             if (proceed === false) return;

             if (isWin32 && socket.ServerInfo.Win32NextUpdatePacketVer &&  // обновления для виндуняки
                 // Service.versionToInt(socket.ServerInfo.ServerVer, true)          > Service.versionToInt(MCServer.ClientVersion, true) &&
                 Service.versionToInt(socket.ServerInfo.Win32NextUpdatePacketVer, true) > Service.versionToInt(MCServer.ClientVersion, true)
             ){
                 tryUpdateClient({
                     PortNode : MCServer.PortNode,
                     HTTPS    : MCServer.HTTPS
                 });
             } else

             if (isMac && socket.ServerInfo.MacUpdatePacketVer &&  // обновления для мак-аки
                 // Service.versionToInt(socket.ServerInfo.ServerVer, true)          > Service.versionToInt(MCServer.ClientVersion, true) &&
                 Service.versionToInt(socket.ServerInfo.MacUpdatePacketVer, true) > Service.versionToInt(MCServer.ClientVersion, true)
             ){
                 tryUpdateClient({
                     PortNode : MCServer.PortNode,
                     HTTPS    : MCServer.HTTPS
                 });
             } else

             if (isLinux && socket.ServerInfo.LinuxUpdatePacketVer &&  // обновления для линуксятины
                 // Service.versionToInt(socket.ServerInfo.ServerVer, true)            > Service.versionToInt(MCServer.ClientVersion, true) &&
                 Service.versionToInt(socket.ServerInfo.LinuxUpdatePacketVer, true) > Service.versionToInt(MCServer.ClientVersion, true)
             ){
                 if (process.argv[0].indexOf("/usr/lib/") === 0){
                     socket.ServerToClientMultiCMD(CMD.sc_error, JSON.stringify({ "ErrNum" : 147, "Params" : [] }));

                     socket.SendDataToServer(ClientToServCMD[CMD.cs_hello](socket));
                 } else {
                     tryUpdateClient({
                         PortNode : MCServer.PortNode,
                         HTTPS    : MCServer.HTTPS
                     });
                 }
             } else

             if (
                 legacyServers.indexOf(socket.ServerInfo.ServerVer) !== -1 ||
                 Service.versionToInt(socket.ServerInfo.ServerVer) === Service.versionToInt(MCServer.ClientVersion) ||
                 (
                     Service.versionToInt(legacyServers[0]) <= Service.versionToInt(MCServer.ClientVersion) &&
                     Service.versionToInt(legacyServers[legacyServers.length - 1]) >= Service.versionToInt(MCServer.ClientVersion)
                 )
             ){
                 socket.SendDataToServer(ClientToServCMD[CMD.cs_hello](socket));
             } else {
                 socket.ServerToClientMultiCMD(CMD.sc_error, JSON.stringify({ "ErrNum" : 81, "Params" : [MCServer.ClientVersion, socket.ServerInfo.ServerVer] }));
             }

         });
     });
 };

 // Резолв робиться ТІЛЬКИ коли renderer повернув заглушку. Без цієї умови сесія, у якій користувач
 // ввів пароль руками (зокрема після зміни пароля акаунта), пішла б токеном — найімовірніше мертвим,
 // замість щойно введеного живого пароля. Поле називається pass, не Pass.
 // Помилка читання = «токена немає»: конект не вішаємо.
 // Резолюється в true = «ланцюг sc_hello можна продовжувати», false = «клієнт перезапускається,
 // cs_hello не слати».
 function resolveTokenLogin(socket){

     if (!socket.UserInfo || socket.UserInfo.pass !== tokenStore.getSentinel()) return Promise.resolve(true);

     // Пошук без ключа: рядок один, звірку serv_id робить споживач. Числового uin тут ще немає,
     // а UserInfo.login може бути нікнеймом
     return DbWork.do.reconnectToken.get()
         .then(function (rows) {
             let row = (rows || [])[0];

             if (!row) return tokenVerdict(socket, "no stored token");

             if (row.serv_id !== socket.ServerInfo.ID) return tokenVerdict(socket, "token belongs to another server");

             let expiresAt = tokenStore.parseServerDT(row.expired);
             let now       = tokenStore.parseServerDT(socket.ServerInfo.CurrentDT);

             if (!expiresAt || !now || expiresAt.getTime() <= now.getTime()) return tokenVerdict(socket, "token expired");

             let token;

             try {
                 token = tokenStore.decrypt(row.token, row.enc);
             } catch (e){
                 return tokenVerdict(socket, "token decrypt failed: " + (e && e.message ? e.message : e));
             }

             socket.UserInfo.Token    = token;
             socket.UserInfo.authType = CMD.cs_login_by_token;

             console.log("(token) resolve: OK, login by token", console.logLevel.INFO);

             return true;
         })
         .catch(function (err) {
             // Помилка читання трактується як «токена немає» — і теж веде в глухий кут
             return tokenVerdict(socket, "reconnect_token read failed: " + (err && err.message ? err.message : err));
         });
 }

 // Глухий кут: renderer віддав sentinel, тобто пароля в нього немає, а токен непридатний.
 // Виходу без перезапуску тут немає — після рестарту ce_load_autoconnect_server віддасть
 // порожній пароль і користувач побачить форму логіна.
 function tokenVerdict(socket, reason){
     console.log("(token) resolve: token unusable, reason - " + reason, console.logLevel.INFO);

     let started = dropTokenAndRestart(reason);

     // Рестарт уже йде (друга помилка на ту саму спробу) — ланцюг теж не продовжуємо
     if (started === false) return false;

     return started
         .then(function () {
             return false;
         })
         .catch(function () {
             // Токен зняти не вдалось, рестарту не буде: продовжуємо звичайним ланцюгом, щоб
             // користувач отримав хоч якусь відповідь від сервера, а не мовчазний конект
             return true;
         });
 }

 // Один рестарт, а не серія: сервер відповідає на кожну спробу, тож друга 215 заходить в обробник,
 // поки перший delete ще в польоті. Засувка ставиться синхронно, до будь-якого await.
 let deadTokenLatch = false;
 let _restartApp    = null;

 function dropTokenAndRestart(reason){

     if (deadTokenLatch) return false;

     deadTokenLatch = true;

     console.important("(token) dead token, dropping and restarting client. Reason: " + reason);

     return DbWork.do.reconnectToken.dropAll()
         .catch(function () {
             return DbWork.do.reconnectToken.dropAll(); // одна повторна спроба
         })
         .then(function () {
             if (_restartApp){
                 _restartApp();
             } else {
                 console.error("(token) restart callback is not injected, client not restarted");
             }
         })
         .catch(function (err) {
             // Перезапуск із живим мертвим токеном у БД дав би нескінченні рестарти, а не один
             console.immediately("FATAL (token) can't delete dead token: " + (err && err.message ? err.message : err));

             deadTokenLatch = false; // єдине місце, де засувка знімається

             throw err;
         });
 }

 // ServToClientCMD[CMD.sc_halt]  = null;

 // === ClientConnect ==========================================

 function TInBufData(sid, wsEmit){
     let Buffers = [];
     let sID     = sid;
     let Self    = this;

     this.AddDataToInBuf = function(_data){
         if (wsEmit){
             wsEmit.emit(SERVER_TO_CLIENT, CMD.Ping + CR + Service.ObjToString(_data) + CR);
         } else {
             if (directSendElectronClient) {
                 directSendElectronClient(_data);
             } else {
                 Buffers.push(_data);
             }
         }

         console.info('Added data to InBuffer, total length:' + Buffers.length, console.logLevel.L_Full);
     };

     this.ClearInBufData = function(){
         if (Buffers.length > 0) {
             Buffers = [];

             console.info('Clearing InBuffer for SessionID: ' + sID, console.logLevel.L_Full);
         }
     };

     /**
      * @return {string}
      */
     this.GetAllFromInBuf = function(needClear){
         let res = Service.ArrayToString(Buffers);

         if (needClear) {
             Self.ClearInBufData();
         }

         return res;
     };

     /**
      * @return {string}
      */
     this.GetAllFromInBufAsArray = function(needClear){
         let res = Buffers;

         if (needClear) {
             Self.ClearInBufData();
         }

         return res;
     };

     /**
      * @return {string}
      */
     this.GetFromInBuf = function(maxSize){
         let res = "";
         let item = "";

         if (Buffers.length === 1){
             res = Service.ObjToString(Buffers[0]) + CR;

             Buffers.shift();
         } else
         while (Buffers.length > 0){
             item = Service.ObjToString(Buffers[0]);

             res = res + item + CR;

             Buffers.shift();

             if (res.length >= maxSize){
                 break;
             }
         }

         return res;
     };

     this.ClearHistory = function(){
         let i = 0;

         while (i < Buffers.length){
             if (Buffers[i] && Buffers[i].logs) {
                 Buffers.shift();
             } else
             if (Buffers[i]){
                 i ++;
             }
         }
     };

     this.drop = function () {
         Self = null;
     }
 }

 function ClientConnect (OptSettings) {
     var Self          = this;
     var __socket      = null;
     let __buffer      = "";
     let __testConnectSocket = null;

     this.socketIO     = OptSettings.io;

     delete OptSettings.io;

     if (console.GetLogLevel() >= console.logLevel.L_Extended){
         // Було Service.extend(OptSettings, {pass:"*****"}) — а extend це Object.assign, тобто лог
         // затирав живий пароль, і на L_Extended на сервер летіло "*****" замість пароля.
         // redactSecrets віддає копію й накриває ще ServPass із SecretAnswer
         console.log('User try auth and send:\n' + Service.redactSecrets(OptSettings));
     }

     //=======================================================

     let ConnectOpt = updateData(OptSettings);

     //=======================================================
      
     function updateData(data) {
         // Self.NT = uui;

         return Service.extend({
             login                : 0,
             pass                 : "",
             host                 : '192.168.10.100',
             port                 : 2004,

             sessionID            : "11111111111111",
             authType             : CMD.Login,
             ServPass             : "",

             ClientType           : "web",

             PingDelay            : 25,
             CurrentPing          : 1,

             clientIP             : "",

             Secured              : false,
             HWID                 : "",
             Gender               : 1,                      // пол пользователя, 0 - неизвестен, 1 - мужской, 2 - женский
             Firstname            : "",                     // имя пользователя
             Lastname             : "",                     // фамилия пользователя
             Surname              : "",                     // отчество пользователя
             Avatar               : 0,                      // номер аватара
             Email                : "bugs@nsoft-s.com",     // адрес электронной почты
             SecretQuestionNumber : 0,                      // номер секретного вопроса для восстановления пароля
             SecretAnswer         : "",                     // ответ на секретный вопрос
             State                : 0                       // текущий статус пользователя (0 - свободен)
         }, data || {});
     }

     function ProcessingReceivedData(){
         if (__buffer.indexOf(CRLF) !== -1) {
             let data = Service.GetSubstring(__buffer, CRLF);

             __buffer = Service.GetSubstringFrom(__buffer, CRLF);

             if (data !== ''){
                 let CMD_numb = data.slice(2, 6) + '';
                 let flags    = data.slice(6, 8);
                 let DataObj  = data.slice(8, data.length);
                 let zipped   = Service.bitOperation.getBit(parseInt(flags, 16), 7) === 1; // 8-й бит - архивируются ли данные

                 console.log("Received CMD from server:" + CMD_numb, console.logLevel.L_Full);

                 if (DataObj.length > 0) {
                     // Лог стоїть ПЕРЕД gunzip, а cs_hello вмикає стиснення: фільтрація за вмістом
                     // побачила б лише base64, з якого секрет відновлюється тривіально. Тому за кодом команди.
                     if (isSecretCMD(CMD_numb)){
                         console.log("Received DATA from server: CMD " + CMD_numb + ", length " + DataObj.length + " (hidden)", console.logLevel.L_Full);
                     } else {
                         console.log("Received DATA from server:" + DataObj, console.logLevel.L_Full);
                     }

                     if (zipped){
                         try {
                             DataObj = zlib.gunzipSync(Buffer.from(DataObj, 'base64'));
                         } catch (e) {
                             console.error('ProcessingReceivedData, unzip data: ' + e.messge);

                             Self.CloseConnection(true);
                         }
                     }

                     DataObj = Service.StringToObj(DataObj);
                 }

                 if (ServToClientCMD[CMD_numb]) {
                     ServToClientCMD[CMD_numb].apply(Self, [DataObj]);
                 } else
                 if (Self.ServerToClientMultiCMD.apply(Self, [CMD_numb, DataObj]) === false){
                     console.error('Server send unknown command: ' + CMD_numb);
                 }

                 if (__buffer.length > 0){
                     process.nextTick(function(){
                         ProcessingReceivedData();
                     });
                 }
             } else {
                 console.warn("Processing Received DATA is empty");
             }
         }
     }

     function enableSSL(socket) {
         return tlsMCServ.connect({
             socket            : socket,
             // secureProtocol    : "TLSv1_2_method",
             servername        : "MyChatServer",
             secureContext     : tlsMCServ.createSecureContext({
                 secureOptions   : constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3
             }),
             rejectUnauthorized : false,
         });
     }

     function closeOnError(err, drop) {
         err = err || {};

         Self.ServerToClientMultiCMD(CMD.sc_error, JSON.stringify({ "ErrNum" : 224, "Params" : [err.message || err.code || "unknown connection error"] }));

         // setTimeout(function () {
             Self.CloseConnection(drop);
         // }, 20);
     }

     function setHandlers (socketHandler, ssl) {
         if (ssl){
             socketHandler.on('secureConnect', function () {
                 console.log("SSL connections! " + socketHandler.getProtocol());
             });
         }

         socketHandler.setTimeout(200000);

         socketHandler.on('data',  function(data) {
             __buffer += data.toString();

             ProcessingReceivedData();
         });
         socketHandler.on('end',   function() {
             let err = Self.UsersInBuffer.GetFromInBuf();

             if (err){
                 try {
                     err = Service.isString(err) ? JSON.parse(err) : err;
                 } catch (e){}

                 errorBuffer = err;
             }

             console.important('\"End\" connections to MyChat server');
         });
         socketHandler.on('close', function() {//err
             console.important('Client disconnected. Close sID: ' + ConnectOpt.sessionID);

             Self.CloseConnection();
         });
         socketHandler.on('error', function(err) {
             console.DublicateToFile(require('./profile').getProfilePath() + "/logs/" + (new Date()).myFormat('yyyy') + "/" + (new Date()).myFormat('mm') + "/" + (new Date()).myFormat('dd') + "/node.log");

             if (err.message.indexOf("timeout") === -1) {
                 closeOnError(err);
             }

             console.important('Socket error: ' + err.message);
         });
         socketHandler.on('timeout', function() {
             console.DublicateToFile(require('./profile').getProfilePath() + "/logs/" + (new Date()).myFormat('yyyy') + "/" + (new Date()).myFormat('mm') + "/" + (new Date()).myFormat('dd') + "/node.log");

             socketHandler.destroy();
             
             closeOnError({
                 code   : "connection timeout",
                 message: "setHandlers: connection timeout"
             });
         });
     }

     function doConnect(ssl, host, port) {
         let socket = MCServ.createConnection(port, host, function() {
             console.log('Connected to MyChat Server. Sending signature.' + (ssl ? " Secured" : "Normal"));

             if (ssl) {
                 this.write('mc' + serverVersion + CRLF + '{"Secured":"TLSv1.2"}' + CRLF);
             } else {
                 this.write('mc' + serverVersion + CRLF + '{"Secured":""}');
             }
         });

         __socket = ssl ? enableSSL(socket) : socket;

         setHandlers(__socket, ssl);
     }

     function killTestSocket() {
         if (__testConnectSocket){
             console.info("Drop test connection socket!");

             try {
                 __testConnectSocket.destroy();
             } catch (e) {}
         }

         __testConnectSocket = null;
     }

     function testAndConnect(_params) {
         let {host, port, altHost, altPort, altInUse} = _params;
         let useHost = altInUse ? altHost : host;
         let usePort = altInUse ? altPort : port;
         
         console.log("Test connect, host: " + useHost + ":" + usePort);

         __testConnectSocket = Service.testServerConnect(useHost, usePort, function (err, _data) {
             var errTxt = err
                 ? err.message
                     ? err.message
                     : err.ErrNum
                         ? global.MCServer.ErrorText[err.ErrNum].replace("%s", err.Params[0])
                         : err
                 : "";

             if (err && _params.altHost && _params.altPort && !_params.altInUse) {
                 console.error("Error test connection1: " + errTxt);
                 console.error("Try alternative connection");

                 killTestSocket();

                 _params.altInUse = true;
                 MCServer.useAltAddr = true;

                 testAndConnect(_params);
             } else
                 
             if (err){
                 console.error("Error test connection2: " + errTxt);

                 killTestSocket();
                 
                 closeOnError({
                     code    : err.code,
                     message : err.message + " & " + host + ":" + port
                 });
             } else {
                 let serverInfo = Service.StringToObj(_data.replace('mctest', ''));

                 killTestSocket();

                 serverInfo.ipNode   = useHost;
                 MCServer  .ipNode   = useHost;
                 serverInfo.PortNode = serverInfo.NodePort;
                 ProtocolVersion     = serverInfo.ProtocolVer;
                 serverVersion       = serverInfo.ServerVer;

                 console.log("Server version: " + serverInfo.ServerVer);

                 if (Service.versionToInt(serverInfo.ServerVer) < Service.versionToInt(MCServer.MinimumVersion)){
                     Self.ServerToClientMultiCMD(CMD.sc_error, JSON.stringify({ "ErrNum" : 81, "Params" : [MCServer.MinimumVersion, serverInfo.ServerVer] }));
                 } else

                 if (__socket === null) {
                     doConnect(ConnectOpt.Secured && !serverInfo.Free, useHost, usePort);
                 }
             }
         }, altHost && altPort && !_params.altInUse ? null : function (data) {
             let CMD_numb = data.slice(2, 6) + '';
             let DataObj  = data.slice(8, data.length);

             try {
                 Self.ServerToClientMultiCMD(CMD_numb, DataObj);
                 Self.CloseConnection(true);
             } catch (e) {}

             killTestSocket();
         });
     }

     // ===========================================================

     this.UsersInBuffer = new TInBufData(ConnectOpt.sessionID, ConnectOpt.isWs ? this.socketIO : null);

     this.UserInfo = ConnectOpt;

     this.updateUserInfo = function (info) {
         ConnectOpt = updateData(info);
     };

     this.RightSet = null;

     this.i = 0; // ітератор месаджів поточного підключення

     this.SendDataToServer = function (_data, CallBack){
         if (__socket && __socket.writable){
             if (_data && (_data !== "")){
                 // Код команди в готовому пакеті стоїть після MagicPacket (2 символи)
                 let __cmd = Service.isString(_data) ? _data.slice(2, 6) : '';

                 if (isSecretCMD(__cmd)){
                     console.log('Sending Data To Server: CMD ' + __cmd + ', length ' + _data.length + ' (hidden)', console.logLevel.L_Full);
                 } else {
                     console.log('Sending Data To Server: ' + _data, console.logLevel.L_Full);
                 }

                 if (Service.isString(_data)){
                     Self.i++;

                     if (_data.length === 10) {
                         _data = _data.replace(CRLF, JSON.stringify({i:Self.i}) + CRLF);
                     } else
                     if (_data.length >10 ){
                         _data = _data.replace("{", "{\"i\":" + Self.i + ",");
                     }
                 }

                 process.nextTick(function() { // Андрей из будущего - не трогай!!!!!1!!!1 если это убрать то пакеты, при включенном шифровании, будут застревать!!!

                     if (__socket && __socket.writable) {
                         __socket.write(Service.convertToEntities(_data));
                     }
                 });

                 if (CallBack){
                     CallBack.apply(Self);
                 }
             } else {
                 console.error('SendDataToServer. Nothing send to server, _data is empty');
             }
         }
     };
     
     this.CloseConnection = function(drop){
         let _sID = ConnectOpt.sessionID;

         currentConnect = null;
         Self.isClosed  = true;

         // Зі звіркою власника: CloseConnection спрацьовує двічі (closeOnError → __socket.end() →
         // socketHandler.on('close')), і без неї запізнілий колбек від старого сокета зняв би
         // операцію видачі, яка вже належить новому з'єднанню
         tokenStore.cancelForConnection(Self);

         Self.onDisconnect  && Self.onDisconnect.call(Self);
         Self.UsersInBuffer && Self.UsersInBuffer.ClearInBufData();

         if (__socket){
             try {
                 __socket.end();
             } catch (e){}

             if (drop){
                 try {
                     __socket.destroy();
                 } catch (e){}

                 Self.UsersInBuffer.ClearInBufData();
                 Self.UsersInBuffer.drop();

                 Self.UsersInBuffer = null;
             }

             console.important('Close socket with SessionID: ' + _sID);
         }

         __socket = null;

         killTestSocket();
     };

     this.checkRights = function(rule){
         return this.RightSet[rule] && this.RightSet[rule] === '1';
     };

     this.ServerToClientMultiCMD = function(cmd, _data) {
         let res = false;

         if (Service.InObject(cmd, CMD)) {
             let out  = {};
             out[cmd] = _data;

             console.log('Received cmd: ' + cmd, console.logLevel.L_Extended);

             Self.UsersInBuffer.AddDataToInBuf(out);

             res = true;
         }

         return res;
     };

     this.onDisconnect = null;

     this.SessionID = ConnectOpt.sessionID;

     // === TEST & CONNECT ========================================

     MCServer.useAltAddr = false;
     
     testAndConnect({
         host    : ConnectOpt.host,
         port    : ConnectOpt.port,
         altHost : ConnectOpt.altHost,
         altPort : ConnectOpt.altPort
     });
 }

 // ===========================================================

 function findMac(startFrom){
     let mac = "";

     try {
         let where = "";
         let intrfslst = OS.networkInterfaces();

         // console.log("adapter list: " + JSON.stringify(intrfslst));

         if (intrfslst[where] && intrfslst[where].length){
             mac = intrfslst[where][0].mac.toUpperCase().replace(/[:]/g, "-");
         } else {
             mac = intrfslst[Object.keys(intrfslst)[0]][0].mac.toUpperCase().replace(/[:]/g, "-");
         }
     } catch (e){
         console.error(e.message);
         console.error(e.stack);
     }

     return mac;
 }

 function MakeClientConnection (options){
     currentConnect = new ClientConnect(options);
     // currentConnect.SessionID = options.sessionID;

     return currentConnect;
 }

 function KILL_ALL_CONNECTION(drop){
     if (currentConnect){
         currentConnect.CloseConnection(drop);

         console.important('All connections was dropped!');
     }
 }

 function GetConnectionInfo(){
     return currentConnect ? currentConnect.UserInfo : null;
 }

 function SendDataFromClient (id, CMD_numb, Data, SendBackToClient){
     if (currentConnect) {
         // if (CMD_numb === CMD.cs_ping && currentConnect.UserInfo.CurrentPing < currentConnect.UserInfo.PingDelay && !Data){
         //     currentConnect.UserInfo.CurrentPing ++;
         //
         //     console.log('Skip cs_ping', console.logLevel.L_Full);
         // } else {
             if (Service.isString(Data)){
                Data = Data.split(CR);
             }

             if (CMD_numb === CMD.cs_ping) {
                 currentConnect.UserInfo.CurrentPing = 1;
             } else {
                 console.log('SendDataFromClient CMD:' + CMD_numb, console.logLevel.L_Extended);
             }

             if (!currentConnect.UserInfo.LoggedIn && CMD_numb === CMD.cs_ping){
                 return;
             }

             if (Data && Data.length > 0) {
                 Data.forEach(function(item, idx, arr){
                     arr[idx] = item === 'true' ? true : (item === 'false' ? false : item);
                 });
             } else {
                 console.warn('CMD:' + CMD_numb + ' Client data is EMPTY!', console.logLevel.L_Full);
             }

             if (ClientToServCMD[CMD_numb]){
                 let _out = ClientToServCMD[CMD_numb].apply(currentConnect, Data);

                 // Data тут — уже позиційний масив (Data.split(CR) вище), імена полів існують лише
                 // всередині білдера, тож редакція за ключами не застосовна: пишемо код і арність.
                 // Рівень L_Extended нижчий за решту синків, тобто цей вмикається раніше за всі.
                 if (isSecretCMD(CMD_numb)) {
                     console.log('SendDataFromClient CMD:' + CMD_numb + ' args: ' + (Data ? Data.length : 0) + ' (hidden)', console.logLevel.L_Extended);
                 } else
                 if (CMD_numb !== CMD.cs_ping) {
                     console.log('SendDataFromClient CMD:' + CMD_numb + ' Data: ' + Data, console.logLevel.L_Extended);
                 } else {
                     console.log('SendDataFromClient CMD:' + CMD_numb + ' Data: ' + Data, console.logLevel.L_Full);
                 }

                 if (CMD_numb === CMD.cs_quit){
                     currentConnect.SendDataToServer(_out, function () {
                         setTimeout(currentConnect.CloseConnection, 200);
                     });
                 } else {
                     currentConnect.SendDataToServer(_out);
                 }
             } else {
                 console.error('Unknown CMD: ' + CMD_numb);
             }
         // }
     } else {
         console.error('No client connection, receive CMD: ' + CMD_numb);

         if (SendBackToClient && Service.isFunction(SendBackToClient)){
             var out = getFromErrorBuffer() || {};

             if (Service.isString(out)){
                 out += '{"9999": {}}';
             } else {
                 out[CMD.sc_drop_connect] = {};

                 out = JSON.stringify(out);
             }

             SendBackToClient(CMD.Ping + CR + out);
         }
     }
 }

 function checkUploadRights(id) {
     let res = false;

     if (currentConnect) {
         res = currentConnect.checkRights(CMD.RS.QSendImages) ||
               currentConnect.checkRights(CMD.RS.QImagesPrivates);
     }

     return res;
 }

 function checkRightsByIds(id, rights) {
     let res = false;

     if (currentConnect && rights.length) {
         rights.forEach(function(item){
             res = res || currentConnect.checkRights(item);
         });
     }

     return res;
 }

 function checkRightsByIdsStrict(id, rights) {
     let res = false;

     if (currentConnect && rights.length) {
         rights.forEach(function(item){
             res = res && currentConnect.checkRights(item);
         });
     }

     return res;
 }

 function getFromErrorBuffer(sID){
     let res = {};

     if (errorBuffer){
         res = errorBuffer;

         errorBuffer = null;
     }

     return res;
 }

 function GetFirstSID(){
     return currentConnect ? currentConnect.SessionID : null;
 }

 function isAdminSid(sid, uin) {
     let user = GetConnectionInfo(sid);

     return user && user.ClientType === CMD.ClientType.WEB_ADMIN && user.UIN == uin;
 }

 // =======================================================

 exports.getNT                = function (){ return uui + "W5kb21IYXNoKDUxMik7IC8vJ2VyZ2lvdWhlcmdmaWg0NTB0OGdmN3k0dDA4aGJlcml1Z2Z2YjM4NDVndDVydGhiOHVyZ2hyZ2hqdSc7IC8vIFNlcnZpY2UuUmFuZG9tSGFzaCgyNTYpOyAvLyBzdXBlcnNlY3JldGtleQ=="; };
 exports.MakeClientConnection = MakeClientConnection; 
 exports.GetConnection        = function (){ return currentConnect || null };
 exports.SendDataToServer     = SendDataFromClient;
 exports.isAdminSid           = isAdminSid;
 exports.CMD                  = CMD;
 exports.KILL_ALL_CONNECTION  = KILL_ALL_CONNECTION;
 exports.getPenaltyUser       = _serverCMD.getPenaltyUser;
 exports.updateUserInfo       = function (sID, data) {
     if (currentConnect){
         currentConnect.updateUserInfo(data);
     }
 };

 // =======================================================

 exports.firstLogin           = _serverCMD.firstLogin;
 exports.checkUploadRights    = checkUploadRights;
 exports.checkRightsByIds     = checkRightsByIds;
 exports.checkRightsByIdsStrict = checkRightsByIdsStrict;
 exports.getFromErrorBuffer   = getFromErrorBuffer;
 exports.GetFirstSID          = GetFirstSID;
 exports.setHardwareID        = function (hwid) { hardwareID = hwid; };
 exports.httpStarted          = function () { wcc = _serverCMD.httpStarted(); };
 exports.tryUpdateClient      = function (_tryUpdateClient){ tryUpdateClient = _tryUpdateClient; };
 exports.clientInformation    = function () { return clientInfo; };
 exports.getClientType        = getClientType;
 exports.resolveTokenLogin    = resolveTokenLogin;   // експонується для тестів Ф7
 exports.dropTokenAndRestart  = dropTokenAndRestart;
 // Засувка одноразова на процес — тестам потрібен спосіб пройти кілька сценаріїв підряд
 exports.__resetDeadTokenLatch = function (){ deadTokenLatch = false; };
 exports.setRestartApp        = function (fn){ _restartApp = fn; };
 exports.directSendElectronClient = function (ff) { directSendElectronClient = ff; };
 exports.getServerHWID        = function () {
     return serverHWID;
 };

