
/*---------------------------------------------------------------------------*/
/* appwebsocket_client.cpp                                                   */
/* copyright (c) innovaphone 2020                                            */
/*                                                                           */
/*---------------------------------------------------------------------------*/

#include <platform.h>
#include "sdk/common/os/iomux.h"
#include "sdk/common/interface/appwebsocket_authenticator.h"
#include "sdk/common/interface/appwebsocket_client.h"
#include "sdk/common/interface/dns.h"
#include "sdk/common/interface/websocket_client.h"
#include "sdk/common/interface/webserver_plugin.h"
#include "common/ilib/json.h"
#include "common/ilib/hash.h"
#include "common/ilib/cipher.h"
#include "common/ilib/str.h"

#include "appwebsocket_client.h"

#define MT_APP_CHALLENGE            "AppChallenge"
#define MT_APP_CHALLENGE_RESULT     "AppChallengeResult"
#define MT_APP_LOGIN                "AppLogin"
#define MT_APP_LOGIN_RESULT         "AppLoginResult"

#define TIMEOUT_KEEPALIVE           60
#define RECONNECT_TIME_MIN          1000            // 1 second minimum reconnect timeout
#define RECONNECT_TIME_MAX          60000           // 60s maximum reconnect timeout

IAppWebsocketClientProvider * CreateAppWebsocketClientProvider(class IIoMux * const iomux,
                                                               class ISocketProvider * const tcpSocketProvider,
                                                               class ISocketProvider * const tlsSocketProvider,
                                                               class IDns * const dns)
{
    return new AppWebsocketClientProvider(iomux, tcpSocketProvider, tlsSocketProvider, dns);
}

AppWebsocketClientProvider::AppWebsocketClientProvider(class IIoMux * const iomux,
                                                       class ISocketProvider * const tcpSocketProvider,
                                                       class ISocketProvider * const tlsSocketProvider,
                                                       class IDns * const dns)
{
    this->iomux = iomux;
    this->tcpSocketProvider = tcpSocketProvider;
    this->tlsSocketProvider = tlsSocketProvider;
    this->dns = dns;
}

IAppWebsocketClient * AppWebsocketClientProvider::Create(class UAppWebsocketClient * const user, class IInstanceLog * const log)
{
    return IAppWebsocketClient::Create(iomux, user, tcpSocketProvider, tlsSocketProvider, dns, log);
}

IAppWebsocketClient * IAppWebsocketClient::Create(class IIoMux * const iomux,
                                    class UAppWebsocketClient * const user,
                                    class ISocketProvider * const tcpSocketProvider,
                                    class ISocketProvider * const tlsSocketProvider,
                                    class IDns * const dns,
                                    class IInstanceLog * const log)
{
    return new class AppWebsocketClient(iomux, user, tcpSocketProvider, tlsSocketProvider, dns, log);
}

/*---------------------------------------------------------------------------*/
/*                              AppWebsocketClient                           */
/*---------------------------------------------------------------------------*/

AppWebsocketClient::AppWebsocketClient(class IIoMux * const iomux, class UAppWebsocketClient * user, class ISocketProvider * const tcpSocketProvider, class ISocketProvider * const tlsSocketProvider, class IDns * const dns, class IInstanceLog * log) :
    iomux(iomux),
    tcpSocketProvider(tcpSocketProvider),
    tlsSocketProvider(tlsSocketProvider),
    dns(dns),
    reconnectTimer(iomux, this)
{
    this->log = log;
    this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::AppWebsocketClient", this);
    this->user = user;
    this->authenticator = NULL;
    this->connected = false;
    this->autoReconnect = false;
    this->waitLoginResult = false;
    this->fragmented = NULL;
    this->fragmentedLen = 0;
    this->app = NULL;
    this->domain = NULL;
    this->sip = NULL;
    this->guid = NULL;
    this->dn = NULL;
    this->info = 0;
    this->password = NULL;
    this->websocketUri = NULL;
    this->usr = NULL;
    this->appObj = NULL;
    this->reconnectTimeout = RECONNECT_TIME_MIN;
    memset(this->key, 0, sizeof(this->key));
    this->CreateClient();
}

AppWebsocketClient::~AppWebsocketClient()
{
    ASSERT(this->client == NULL, "AppWebsocketClient::~AppWebsocketClient client not NULL");
    this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::~AppWebsocketClient", this);
    if (authenticator) delete authenticator;
    free(fragmented);
    free(app);
    free(domain);
    free(sip);
    free(guid);
    free(dn);
    free(info);
    free(password);
    free(websocketUri);
    free(usr);
    free(appObj);
}

void AppWebsocketClient::CreateClient()
{
    this->client = IWebsocketClient::Create(this->iomux, this, this->tcpSocketProvider, this->tlsSocketProvider, this->dns, this->log, TIMEOUT_KEEPALIVE);
}

void AppWebsocketClient::StartReconnectTimer()
{
    this->reconnectTimer.Start(this->reconnectTimeout);
    this->reconnectTimeout *= 2;
    if (this->reconnectTimeout > RECONNECT_TIME_MAX) {
        this->reconnectTimeout = RECONNECT_TIME_MAX;
    }
}

void AppWebsocketClient::TimerOnTimeout(ITimer * timer)
{
    this->CreateClient();
    this->client->Connect(this->websocketUri);
}

void AppWebsocketClient::IoExec(void * execContext)
{
    // IoExec is just called to enforce an async callback to AppWebsocketClientClosed
    this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::AppWebsocketClientClosed", this);
    this->user->AppWebsocketClientClosed(this);
}

void AppWebsocketClient::Connect(const char * websocketUri, const char * password, const char * app, const char * domain, const char * sip, const char * guid, const char * dn, bool autoReconnect, const char * usr, const char * appObj, const char * info)
{
    this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::Connect uri:%s app:%s domain:%s sip:%s guid:%s dn:%s autoReconnect:%u", this, websocketUri, app, domain, sip, guid, dn, autoReconnect);
    this->app = app ? _strdup(app) : NULL;
    this->domain = domain ? _strdup(domain) : NULL;
    this->sip = sip ? _strdup(sip) : NULL;
    this->guid = guid ? _strdup(guid) : NULL;
    this->dn = dn ? _strdup(dn) : NULL;
    this->password = password ? _strdup(password) : NULL;
    this->autoReconnect = autoReconnect;
    this->websocketUri = _strdup(websocketUri);
    this->usr = _strdup(usr);
    this->appObj = _strdup(appObj);
    this->info = _strdup(info);
    client->Connect(websocketUri);
}

void AppWebsocketClient::Connect(const char * websocketUri, const char * app, class IAppWebsocketAuthenticator * authenticator, bool autoReconnect)
{
    this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::Connect uri:%s app:%s authenticator:%x autoReconnect:%u", this, websocketUri, app, authenticator, autoReconnect);
    this->app = app ? _strdup(app) : NULL;
    this->authenticator = authenticator;
    authenticator->SetUser(this);
    this->autoReconnect = autoReconnect;
    this->websocketUri = _strdup(websocketUri);
    client->Connect(websocketUri);
}

void AppWebsocketClient::WebsocketConnectComplete(IWebsocketClient * const websocket)
{
    char buffer[1024];
    class json_io send(buffer);
    word sendBase = send.add_object(JSON_ID_ROOT, NULL);
    send.add_string(sendBase, "mt", MT_APP_CHALLENGE);
    send.add_string(sendBase, "user", usr);
    send.add_string(sendBase, "appObj", appObj);
    MessageSend(send, send.get_buffer());
    MessageComplete();
}

void AppWebsocketClient::WebsocketCloseComplete(IWebsocketClient * const websocket, closereason_t reason)
{
    delete websocket;
    this->client = NULL;
    if (this->autoReconnect) {
        if (this->reconnectTimeout == RECONNECT_TIME_MIN) { // just trigger AppWebsocketClientDisconnected on first disconnect 
            this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::AppWebsocketClientDisconnected reason:%u reconnect in %u seconds", this, reason, this->reconnectTimeout / 1000);
            this->user->AppWebsocketClientDisconnected(this);
        }
        else {
            this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::WebsocketCloseComplete reason:%u reconnect in %u seconds", this, reason, this->reconnectTimeout / 1000);
        }
        if (!this->waitLoginResult) {    // we're waiting for an async AppWebsocketAuthenticatorGetLoginResult, so do nothing until a result is received
            this->StartReconnectTimer();
        }
    }
    else {
        this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::AppWebsocketClientClosed reason:%u", this, reason);
        if (!this->waitLoginResult) {   // we're waiting for an async AppWebsocketAuthenticatorGetLoginResult, so do nothing until a result is received
            this->user->AppWebsocketClientClosed(this);
        }
    }
}

void AppWebsocketClient::WebsocketSendResult(IWebsocketClient * const websocket)
{
    if (connected) {
        this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::AppWebsocketClientSendResult", this);
        user->AppWebsocketClientSendResult(this);
    }
}

void AppWebsocketClient::WebsocketRecvResult(IWebsocketClient * const websocket, void * buf, size_t len, bool text, bool isFragment)
{
    if (fragmented) {
        fragmented = (char *)realloc(fragmented, fragmentedLen + len + 1);
        memcpy(&fragmented[fragmentedLen], buf, len);
        fragmentedLen += len;
        free(buf);
    }
    if (isFragment) {
        if (!fragmented) {
            fragmented = (char *)buf;
            fragmentedLen = len;
        }
        websocket->Recv();
        return;
    }
    if (fragmented) {
        buf = fragmented;
        len = fragmentedLen;
        fragmented = NULL;
        fragmentedLen = 0;
    }

    if (text) {
        char * buffer = (char *)buf;
        buffer[len] = '\0';
        class json_io json(buffer);
        bool success = false;
        if (json.decode()) {
            word base = json.get_object(JSON_ID_ROOT, NULL);
            const char * mt = json.get_string(base, "mt");
            const char * src = json.get_string(base, "src");
            if (mt) {
                if (!strcmp(mt, MT_APP_CHALLENGE_RESULT)) success = RecvAppChallengeResult(json, base);
                else if (!strcmp(mt, MT_APP_LOGIN_RESULT)) success = RecvAppLoginResult(json, base);
                else if (connected) {
                    this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::AppWebsocketClientMessage mt:%s src:%s", this, mt, src);
                    user->AppWebsocketClientMessage(this, json, base, mt, src);
                    success = true;
                }
            }
            else {
                log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::WebsocketRecvResult empty mt buffer:%s", this, buffer);
            }
        }
        else {
            log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::WebsocketRecvResult json decode failed buffer:%s", this, buffer);
        }
        if (!success) {
            client->Close();
        }
    }
    else {
        this->user->AppWebsocketClientBinaryMessage(this, buf, len);
    }
    // else ignore binary messages for now
    free(buf);
}

void AppWebsocketClient::CalculateKey(const char * challenge)
{
    byte k[HASH_SIZE_SHA256];
    hash h;
    h.init(HASH_SHA256);
    h.update("innovaphoneAppSessionKey:", 25);
    h.update(challenge, strlen(challenge));
    h.update(":", 1);
    if (password) h.update(password, strlen(password));
    h.final(k);
    str::from_hexmem(k, HASH_SIZE_SHA256, key);
}

bool AppWebsocketClient::RecvAppChallengeResult(class json_io & json, word base)
{
    char ch[12];
    const char * challenge = json.get_string(base, "challenge");
    if (!challenge) {
        dword chn = json.get_unsigned(base, "challenge");
        sprintf(ch, "%u", chn);
        challenge = ch;
    }
    if (authenticator) {
        this->waitLoginResult = true;
        authenticator->GetLogin(this->app, challenge);
    }
    else {
        CalculateKey(challenge);
        class hash hash;
        byte digest_bin[HASH_SIZE_SHA256];
        char digest_check[HASH_SIZE_SHA256 * 2 + 1];
        hash.init(HASH_SHA256);
        if (this->app) hash.update(this->app, strlen(this->app));
        hash.update(":", 1);
        if (this->domain) hash.update(this->domain, strlen(this->domain));
        hash.update(":", 1);
        if (this->sip) hash.update(this->sip, strlen(this->sip));
        hash.update(":", 1);
        if (this->guid) hash.update(this->guid, strlen(this->guid));
        hash.update(":", 1);
        if (this->dn) hash.update(this->dn, strlen(this->dn));
        hash.update(":", 1);
        if (info) {
            hash.update(info, strlen(info));
            hash.update(":", 1);
        }
        hash.update(challenge, strlen(challenge));
        hash.update(":", 1);
        if (this->password) hash.update(this->password, strlen(this->password));
        hash.final(digest_bin);
        for (int i = 0; i < HASH_SIZE_SHA256; i++) sprintf(&digest_check[i * 2], "%02x", digest_bin[i]);

        char buffer[WS_MAX_DATA_SIZE];
        class json_io send(buffer);
        word sendBase = send.add_object(JSON_ID_ROOT, NULL);
        send.add_string(sendBase, "mt", MT_APP_LOGIN);
        send.add_string(sendBase, "app", this->app);
        send.add_string(sendBase, "domain", this->domain);
        send.add_string(sendBase, "sip", this->sip);
        send.add_string(sendBase, "guid", this->guid);
        send.add_string(sendBase, "dn", this->dn);
        send.add_string(sendBase, "digest", digest_check);
        if (info && info[0]) send.add_json(sendBase, "info", info);
        MessageSend(send, buffer);
    }
    MessageComplete();
    return true;
}

bool AppWebsocketClient::RecvAppLoginResult(class json_io & json, word base)
{
    bool ok = json.get_bool(base, "ok");
    if (ok) {
        connected = true;
        this->reconnectTimeout = RECONNECT_TIME_MIN;    // reset reconnect timeout to minimum value after a successfull login
        this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::AppWebsocketClientConnectComplete", this);
        user->AppWebsocketClientConnectComplete(this);
        return true;
    }
    return false;
}

void * AppWebsocketClient::WebsocketRecvBuffer(size_t len)
{
    return malloc(len + 1); // terminating zero
}

void AppWebsocketClient::WebsocketRecvCanceled(IWebsocketClient * const websocket, void * buf)
{
    free(buf);
}

void AppWebsocketClient::MessageComplete()
{
    this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::MessageComplete", this);
    client->Recv();
}

void AppWebsocketClient::MessageSend(class json_io & msg, char * buffer)
{
    this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::MessageSend", this);
    word len = msg.encode();
    client->Send(buffer, len, true);
}

void AppWebsocketClient::MessageSendText(const char * buffer)
{
    this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::MessageSendText", this);
    client->Send(buffer, strlen(buffer), true);
}

void AppWebsocketClient::MessageSendBinary(byte * buffer, size_t len)
{
    this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::MessageSendBinary", this);
    client->Send(buffer, len, false);
}

void AppWebsocketClient::Encrypt(const char * seed, const char * data, char * out, size_t outLen)
{
    ASSERT(outLen > strlen(data) * 2, "AppWebsocketClient::Encrypt - output buffer too small");
    size_t len = strlen(data);
    char * k = (char *)malloc(strlen(seed) + strlen(key) + 2);
    byte * o = (byte *)malloc(outLen);
    _sprintf(k, "%s:%s", seed, key);
    Cipher c;
    c.Init(CIPHER_RC4, (const byte *)k, (int)strlen(k), true);
    c.Crypt((const byte*)data, o, (int)len);
    str::from_hexmem(o, (const dword)len, out);
    free(k);
    free(o);
}

void AppWebsocketClient::Decrypt(const char * seed, const char * data, char * out, size_t outLen)
{
    ASSERT(outLen > strlen(data) / 2, "AppWebsocketClient::Decrypt - output buffer too small");
    size_t len = strlen(data);
    char * k = (char *)malloc(strlen(seed) + strlen(key) + 2);
    byte * i = (byte *)malloc(outLen);
    len = str::to_hexmem(data, i, (dword)outLen);
    _sprintf(k, "%s:%s", seed, key);
    Cipher c;
    c.Init(CIPHER_RC4, (const byte *)k, (int)strlen(k), false);
    c.Crypt(i, (byte*)out, (int)len);
    out[len] = '\0';
    free(k);
    free(i);
}

void AppWebsocketClient::Hash(const char * seed, const char * data, char * out, size_t outLen)
{
    ASSERT(outLen > HASH_SIZE_SHA256 * 2, "AppWebsocketClient::Decrypt - output buffer too small");
    byte buf[HASH_SIZE_SHA256];
    hash h;
    h.init(HASH_SHA256);
    if (seed) h.update(seed, strlen(seed));
    h.update(":", 1);
    h.update(key, strlen(key));
    h.update(":", 1);
    if (data) h.update(data, strlen(data));
    h.final(buf);
    str::from_hexmem(buf, HASH_SIZE_SHA256, out);
}

void AppWebsocketClient::Close()
{
    this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::Close", this);
    this->autoReconnect = false;
    this->reconnectTimer.Cancel();
    if (this->client == nullptr) {
        if (!this->waitLoginResult) {
            this->iomux->SetExec(this, nullptr);
        }
    }
    else {
        this->client->Close();
    }
}

void AppWebsocketClient::AppWebsocketAuthenticatorClosed(class IAppWebsocketAuthenticator * authenticator)
{
    this->waitLoginResult = false;

    this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::AppWebsocketAuthenticatorClosed authenticator object is invalid and cannot be reused anymore", this);
    delete authenticator;
    this->authenticator = 0;
    this->autoReconnect = false;    // doesn't make sense to reconnect if we lost the authenticator
    if (client) client->Close();
    else this->iomux->SetExec(this, nullptr);   // trigger an async AppWebsocketClientClosed
}

void AppWebsocketClient::AppWebsocketAuthenticatorGetLoginResult(class IAppWebsocketAuthenticator * authenticator, const char * domain, const char * sip, const char * guid, const char * dn, const char * pbxObj, const char * app, const char * info, const char * digest, const char * key)
{
    this->waitLoginResult = false;

    if (!this->autoReconnect) { // otherwise we need to keep the authenticator for further logins
        delete authenticator;
        this->authenticator = 0;
    }
    str::to_str(key, this->key, sizeof(this->key));
    char buffer[WS_MAX_DATA_SIZE];
    class json_io send(buffer);
    word sendBase = send.add_object(JSON_ID_ROOT, NULL);
    send.add_string(sendBase, "mt", MT_APP_LOGIN);
    send.add_string(sendBase, "app", app);
    send.add_string(sendBase, "domain", domain);
    send.add_string(sendBase, "sip", sip);
    send.add_string(sendBase, "guid", guid);
    send.add_string(sendBase, "dn", dn);
    send.add_string(sendBase, "digest", digest);
    if (info && info[0]) send.add_json(sendBase, "info", info);
    if (pbxObj) send.add_string(sendBase, "pbxObj", pbxObj);
    if (client) MessageSend(send, buffer);
    else if (this->autoReconnect) this->StartReconnectTimer();
    else this->iomux->SetExec(this, nullptr);   // trigger an async AppWebsocketClientClosed
}

void AppWebsocketClient::AppWebsocketAuthenticatorGetLoginError(class IAppWebsocketAuthenticator * authenticator)
{
    this->waitLoginResult = false;

    this->log->Log(LOG_APP_WEBSOCKET, "AppWebsocketClient(%08x)::AppWebsocketAuthenticatorGetLoginError authenticator object is invalid and cannot be reused anymore", this);
    if (!this->autoReconnect) {
        delete authenticator;
        this->authenticator = 0;
    }    
    if (client) client->Close();
    else if (this->autoReconnect) this->StartReconnectTimer();
    else this->iomux->SetExec(this, nullptr);   // trigger an async AppWebsocketClientClosed
}
