/**
 * @file PasswordListener.c
 * @author Ambroz Bizjak <ambrop7@gmail.com>
 * 
 * @section LICENSE
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the author nor the
 *    names of its contributors may be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdlib.h>

#include <prerror.h>

#include <ssl.h>

#include <misc/offset.h>
#include <misc/byteorder.h>
#include <misc/balloc.h>
#include <misc/compare.h>
#include <base/BLog.h>
#include <security/BRandom.h>
#include <nspr_support/DummyPRFileDesc.h>

#include <client/PasswordListener.h>

#include <generated/blog_channel_PasswordListener.h>

static int password_comparator (void *user, uint64_t *p1, uint64_t *p2);
static void remove_client (struct PasswordListenerClient *client);
static void listener_handler (PasswordListener *l);
static void client_connection_handler (struct PasswordListenerClient *client, int event);
static void client_sslcon_handler (struct PasswordListenerClient *client, int event);
static void client_receiver_handler (struct PasswordListenerClient *client);

int password_comparator (void *user, uint64_t *p1, uint64_t *p2)
{
    return B_COMPARE(*p1, *p2);
}

void remove_client (struct PasswordListenerClient *client)
{
    PasswordListener *l = client->l;
    
    // stop using any buffers before they get freed
    if (l->ssl) {
        BSSLConnection_ReleaseBuffers(&client->sslcon);
    }
    
    // free receiver
    SingleStreamReceiver_Free(&client->receiver);
    
    // free SSL
    if (l->ssl) {
        BSSLConnection_Free(&client->sslcon);
        ASSERT_FORCE(PR_Close(client->sock->ssl_prfd) == PR_SUCCESS)
    }
    
    // free connection interfaces
    BConnection_RecvAsync_Free(&client->sock->con);
    BConnection_SendAsync_Free(&client->sock->con);
    
    // free connection
    BConnection_Free(&client->sock->con);
    
    // free sslsocket structure
    free(client->sock);
    
    // move to free list
    LinkedList1_Remove(&l->clients_used, &client->list_node);
    LinkedList1_Append(&l->clients_free, &client->list_node);
}

void listener_handler (PasswordListener *l)
{
    DebugObject_Access(&l->d_obj);
    
    // obtain client entry
    if (LinkedList1_IsEmpty(&l->clients_free)) {
        struct PasswordListenerClient *client = UPPER_OBJECT(LinkedList1_GetFirst(&l->clients_used), struct PasswordListenerClient, list_node);
        remove_client(client);
    }
    struct PasswordListenerClient *client = UPPER_OBJECT(LinkedList1_GetLast(&l->clients_free), struct PasswordListenerClient, list_node);
    LinkedList1_Remove(&l->clients_free, &client->list_node);
    LinkedList1_Append(&l->clients_used, &client->list_node);
    
    // allocate sslsocket structure
    if (!(client->sock = (sslsocket *)malloc(sizeof(*client->sock)))) {
        BLog(BLOG_ERROR, "malloc failedt");
        goto fail0;
    }
    
    // accept connection
    if (!BConnection_Init(&client->sock->con, BConnection_source_listener(&l->listener, NULL), l->bsys, client, (BConnection_handler)client_connection_handler)) {
        BLog(BLOG_ERROR, "BConnection_Init failed");
        goto fail1;
    }
    
    BLog(BLOG_INFO, "Connection accepted");
    
    // init connection interfaces
    BConnection_SendAsync_Init(&client->sock->con);
    BConnection_RecvAsync_Init(&client->sock->con);
    
    StreamPassInterface *send_if = BConnection_SendAsync_GetIf(&client->sock->con);
    StreamRecvInterface *recv_if = BConnection_RecvAsync_GetIf(&client->sock->con);
    
    if (l->ssl) {
        // create bottom NSPR file descriptor
        if (!BSSLConnection_MakeBackend(&client->sock->bottom_prfd, send_if, recv_if, l->twd, l->ssl_flags)) {
            BLog(BLOG_ERROR, "BSSLConnection_MakeBackend failed");
            goto fail2;
        }
        
        // create SSL file descriptor from the bottom NSPR file descriptor
        if (!(client->sock->ssl_prfd = SSL_ImportFD(l->model_prfd, &client->sock->bottom_prfd))) {
            ASSERT_FORCE(PR_Close(&client->sock->bottom_prfd) == PR_SUCCESS)
            goto fail2;
        }
        
        // set server mode
        if (SSL_ResetHandshake(client->sock->ssl_prfd, PR_TRUE) != SECSuccess) {
            BLog(BLOG_ERROR, "SSL_ResetHandshake failed");
            goto fail3;
        }
        
        // set require client certificate
        if (SSL_OptionSet(client->sock->ssl_prfd, SSL_REQUEST_CERTIFICATE, PR_TRUE) != SECSuccess) {
            BLog(BLOG_ERROR, "SSL_OptionSet(SSL_REQUEST_CERTIFICATE) failed");
            goto fail3;
        }
        if (SSL_OptionSet(client->sock->ssl_prfd, SSL_REQUIRE_CERTIFICATE, PR_TRUE) != SECSuccess) {
            BLog(BLOG_ERROR, "SSL_OptionSet(SSL_REQUIRE_CERTIFICATE) failed");
            goto fail3;
        }
        
        // initialize SSLConnection
        BSSLConnection_Init(&client->sslcon, client->sock->ssl_prfd, 0, BReactor_PendingGroup(l->bsys), client, (BSSLConnection_handler)client_sslcon_handler);
        
        send_if = BSSLConnection_GetSendIf(&client->sslcon);
        recv_if = BSSLConnection_GetRecvIf(&client->sslcon);
    }
    
    // init receiver
    SingleStreamReceiver_Init(&client->receiver, (uint8_t *)&client->recv_buffer, sizeof(client->recv_buffer), recv_if, BReactor_PendingGroup(l->bsys), client, (SingleStreamReceiver_handler)client_receiver_handler);
    
    return;
    
    // cleanup on error
fail3:
    if (l->ssl) {
        ASSERT_FORCE(PR_Close(client->sock->ssl_prfd) == PR_SUCCESS)
    }
fail2:
    BConnection_RecvAsync_Free(&client->sock->con);
    BConnection_SendAsync_Free(&client->sock->con);
    BConnection_Free(&client->sock->con);
fail1:
    free(client->sock);
fail0:
    LinkedList1_Remove(&l->clients_used, &client->list_node);
    LinkedList1_Append(&l->clients_free, &client->list_node);
}

void client_connection_handler (struct PasswordListenerClient *client, int event)
{
    PasswordListener *l = client->l;
    DebugObject_Access(&l->d_obj);
    
    if (event == BCONNECTION_EVENT_RECVCLOSED) {
        BLog(BLOG_INFO, "connection closed");
    } else {
        BLog(BLOG_INFO, "connection error");
    }
    
    remove_client(client);
}

void client_sslcon_handler (struct PasswordListenerClient *client, int event)
{
    PasswordListener *l = client->l;
    DebugObject_Access(&l->d_obj);
    ASSERT(l->ssl)
    ASSERT(event == BSSLCONNECTION_EVENT_ERROR)
    
    BLog(BLOG_INFO, "SSL error");
    
    remove_client(client);
}

void client_receiver_handler (struct PasswordListenerClient *client)
{
    PasswordListener *l = client->l;
    DebugObject_Access(&l->d_obj);
    
    // check password
    uint64_t received_pass = ltoh64(client->recv_buffer);
    BAVLNode *pw_tree_node = BAVL_LookupExact(&l->passwords, &received_pass);
    if (!pw_tree_node) {
        BLog(BLOG_WARNING, "unknown password");
        remove_client(client);
        return;
    }
    PasswordListener_pwentry *pw_entry = UPPER_OBJECT(pw_tree_node, PasswordListener_pwentry, tree_node);
    
    BLog(BLOG_INFO, "Password recognized");
    
    // remove password entry
    BAVL_Remove(&l->passwords, &pw_entry->tree_node);
    
    // stop using any buffers before they get freed
    if (l->ssl) {
        BSSLConnection_ReleaseBuffers(&client->sslcon);
    }
    
    // free receiver
    SingleStreamReceiver_Free(&client->receiver);
    
    if (l->ssl) {
        // free SSL connection
        BSSLConnection_Free(&client->sslcon);
    } else {
        // free connection interfaces
        BConnection_RecvAsync_Free(&client->sock->con);
        BConnection_SendAsync_Free(&client->sock->con);
    }
    
    // remove connection handler
    BConnection_SetHandlers(&client->sock->con, NULL, NULL);
    
    // move client entry to free list
    LinkedList1_Remove(&l->clients_used, &client->list_node);
    LinkedList1_Append(&l->clients_free, &client->list_node);
    
    // give the socket to the handler
    pw_entry->handler_client(pw_entry->user, client->sock);
    return;
}

int PasswordListener_Init (PasswordListener *l, BReactor *bsys, BThreadWorkDispatcher *twd, BAddr listen_addr, int max_clients, int ssl, int ssl_flags, CERTCertificate *cert, SECKEYPrivateKey *key)
{
    ASSERT(BConnection_AddressSupported(listen_addr))
    ASSERT(max_clients > 0)
    ASSERT(ssl == 0 || ssl == 1)
    
    // init arguments
    l->bsys = bsys;
    l->twd = twd;
    l->ssl = ssl;
    l->ssl_flags = ssl_flags;
    
    // allocate client entries
    if (!(l->clients_data = (struct PasswordListenerClient *)BAllocArray(max_clients, sizeof(struct PasswordListenerClient)))) {
        BLog(BLOG_ERROR, "BAllocArray failed");
        goto fail0;
    }
    
    if (l->ssl) {
        // initialize model SSL fd
        DummyPRFileDesc_Create(&l->model_dprfd);
        if (!(l->model_prfd = SSL_ImportFD(NULL, &l->model_dprfd))) {
            BLog(BLOG_ERROR, "SSL_ImportFD failed");
            ASSERT_FORCE(PR_Close(&l->model_dprfd) == PR_SUCCESS)
            goto fail1;
        }
        
        // set server certificate
        if (SSL_ConfigSecureServer(l->model_prfd, cert, key, NSS_FindCertKEAType(cert)) != SECSuccess) {
            BLog(BLOG_ERROR, "SSL_ConfigSecureServer failed");
            goto fail2;
        }
    }
    
    // initialize client entries
    LinkedList1_Init(&l->clients_free);
    LinkedList1_Init(&l->clients_used);
    for (int i = 0; i < max_clients; i++) {
        struct PasswordListenerClient *conn = &l->clients_data[i];
        conn->l = l;
        LinkedList1_Append(&l->clients_free, &conn->list_node);
    }
    
    // initialize passwords tree
    BAVL_Init(&l->passwords, OFFSET_DIFF(PasswordListener_pwentry, password, tree_node), (BAVL_comparator)password_comparator, NULL);
    
    // initialize listener
    if (!BListener_Init(&l->listener, listen_addr,  l->bsys, l, (BListener_handler)listener_handler)) {
        BLog(BLOG_ERROR, "Listener_Init failed");
        goto fail2;
    }
    
    DebugObject_Init(&l->d_obj);
    return 1;
    
    // cleanup
fail2:
    if (l->ssl) {
        ASSERT_FORCE(PR_Close(l->model_prfd) == PR_SUCCESS)
    }
fail1:
    BFree(l->clients_data);
fail0:
    return 0;
}

void PasswordListener_Free (PasswordListener *l)
{
    DebugObject_Free(&l->d_obj);

    // free clients
    LinkedList1Node *node;
    while (node = LinkedList1_GetFirst(&l->clients_used)) {
        struct PasswordListenerClient *client = UPPER_OBJECT(node, struct PasswordListenerClient, list_node);
        remove_client(client);
    }
    
    // free listener
    BListener_Free(&l->listener);
    
    // free model SSL file descriptor
    if (l->ssl) {
        ASSERT_FORCE(PR_Close(l->model_prfd) == PR_SUCCESS)
    }
    
    // free client entries
    BFree(l->clients_data);
}

uint64_t PasswordListener_AddEntry (PasswordListener *l, PasswordListener_pwentry *entry, PasswordListener_handler_client handler_client, void *user)
{
    DebugObject_Access(&l->d_obj);
    
    while (1) {
        // generate password
        BRandom_randomize((uint8_t *)&entry->password, sizeof(entry->password));
        
        // try inserting
        if (BAVL_Insert(&l->passwords, &entry->tree_node, NULL)) {
            break;
        }
    }
    
    entry->handler_client = handler_client;
    entry->user = user;
    
    return entry->password;
}

void PasswordListener_RemoveEntry (PasswordListener *l, PasswordListener_pwentry *entry)
{
    DebugObject_Access(&l->d_obj);
    
    // remove
    BAVL_Remove(&l->passwords, &entry->tree_node);
}
