
/******************************************************************************
**
**  This program is free software; you can redistribute it and/or
**  modify it, however, you cannot sell it.
**
**  This program is distributed in the hope that it will be useful,
**  but WITHOUT ANY WARRANTY; without even the implied warranty of
**  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
**
**  You should have received a copy of the license attached to the
**  use of this software.  If not, visit www.shmoo.com/osiris for
**  details.
**
******************************************************************************/

/*****************************************************************************
**
**  File:    md_scan.c
**  Date:    June 3, 2002
**
**  Author:  Brian Wotring
**  Purpose: handle scan contexts, and streams for the management daemon.
**
******************************************************************************/

#include "libosiris.h"
#include "libosirism.h"
#include "libosirisdb.h"
#include "libosirisctl.h"
#include "libfileapi.h"

#include "common.h"

#include "md_hosts.h"
#include "md_log.h"
#include "md_config.h"
#include "md_control.h"
#include "md_compare.h"
#include "md_database.h"
#include "md_scan.h"
#include "md_utilities.h"

#include "logging.h"

extern char root_path[MAX_PATH_LENGTH];
extern SSL_CTX *ssl_context;

extern OSI_MANAGEMENT_CONFIG *config;


OSI_SCAN_CONTEXT * scan_context_new()
{
    OSI_SCAN_CONTEXT *context = osi_malloc( sizeof( OSI_SCAN_CONTEXT ) );

    if( context != NULL )
    {
        memset( context, 0, sizeof( OSI_SCAN_CONTEXT ) );
    }

    return context;
}

void scan_context_destroy( OSI_SCAN_CONTEXT *context )
{
    if( context != NULL )
    {
        if( context->ssl != NULL )
        {
            osi_ssl_destroy( &( context->ssl ) );
        }

        if( context->host != NULL )
        {
            osi_host_destroy( context->host );
        }

        if( context->cfg != NULL )
        {
            osi_config_destroy( context->cfg );
        }

        osi_free( context );
    }
}


/******************************************************************************
**
**    Function: start_scan_handler
**
**    Purpose:  start a new thread or process that is capable of handling
**              a started scan.
**
******************************************************************************/

void start_scan_handler( OSI_SCAN_CONTEXT *scan_context )
{
#ifndef WIN32
    pid_t scanner_pid;
#endif

#ifdef HAVE_THREADS
    HANDLE scanner_handle;
    unsigned int scanner_thread_id;
#endif

    char *name = NULL;

    if( scan_context == NULL )
    {
        return;
    }

    if( scan_context->host != NULL )
    {
        name = scan_context->host->name;
    }

#ifdef HAVE_THREADS

    scanner_handle = (HANDLE)_beginthreadex( NULL, 0, &handle_scan_stream,
                                             scan_context, 0,
                                             &scanner_thread_id );

    if( scanner_handle != 0 )
    {
        log_info( LOG_HIGH, name, "scan handling thread spawned." );
    }

    else
    {
        log_error( LOG_MEDIUM, name, "unable to spawn scan thread." );
    }

#else

    scanner_pid = osi_create_process();

    if( scanner_pid < 0 )
    {
        log_error( LOG_LOW, name,
                   "unable to spawn scan handling process, shutting down." );

        exit( EXIT_CODE_ERROR );
    }

    /* child process will kick off the scan function and die. */

    if( scanner_pid == 0 )
    {
        log_info(  LOG_HIGH, name,
                   "scan handling process spawned." );

        handle_scan_stream( scan_context );

        log_info(  LOG_HIGH, name,
                   "scan handling process going away." );

        exit( EXIT_CODE_NORMAL );
    }

    /* parent frees scan context and leaves here. */

    scan_context_destroy( scan_context );

#endif
}


THREAD_FUNCTION_TYPE handle_scan_stream( void *scan_context )
{
    int result;
    int ssl_socket;
    int message_read_errors = 0;

    SSL *ssl;

    OSI_SCAN_CONTEXT *context = (OSI_SCAN_CONTEXT *)scan_context;
    OSI_ERROR *error;
    OSI_SCAN_RESULTS *results;
    OSI_HOST *host;
    OSI_DB db;

    osi_uint16 type;
    osi_uint64 record_type;

    fd_set read_set;
    MESSAGE message;

    if( context == NULL ) 
    {
        #ifdef WIN32
            _endthreadex(0);
        #endif

        return THREAD_RETURN_VALUE;
    }

    ssl = context->ssl;
    host = context->host;

    ssl_socket = SSL_get_fd( ssl );

    /* try to create a database. */

    memset( &db, 0, sizeof( OSI_DB ) );
    result = create_new_database_for_host( &db, host );

    if( result != OSI_OK )
    {
        log_error( LOG_LOW, host->name, "unable to create database: %s.",
                   osi_get_name_for_error( result ) );

        goto exit_gracefully;
    }

    /* if the config is not null, store it in the database. */

    if( context->cfg != NULL )
    {
        store_config_in_db( context->cfg, &db );
    }

    /* listen for scan stream data. */

    for(;;)
    {
        int record_size;
        int message_size;

        unsigned char buffer[MAX_SCAN_RECORD_LENGTH];

        FD_ZERO( &read_set );
        FD_SET( ssl_socket, &read_set );

        select( ( ssl_socket + 1 ), &read_set, NULL, NULL, NULL );

        if( !FD_ISSET( ssl_socket, &read_set ) )
        {
            continue;
        }

        result = osi_ssl_read_message( &message, ssl );

        if( result != MESSAGE_OK )
        {
            message_read_errors++;

            log_error( LOG_MEDIUM, host->name,
                       "scan-data message read error: %s",
                       osi_get_name_for_error( result ) );

            if( message_read_errors >= MAX_SCAN_DATA_ERRORS )
            {
                OSI_ERROR final_error;
                final_error.type = OSI_ERROR_DB_RETRIEVING;

                osi_strlcpy( final_error.message,
                   "too many errors receiving scan data, aborting receive.",                        sizeof( final_error.message ) );

                log_error( LOG_MEDIUM, host->name,
 "received maximum scan-data read errors (%d), aborting recieve of scan-data.",
                             MAX_SCAN_DATA_ERRORS );

                result = osi_scan_db_store_error( &db, &final_error );
                break;
            }
        }

        /* no error, process this message. */

        type = GET_MESSAGE_TYPE( ( (MESSAGE *)&message ) );

        switch( (int)type )
        {
            case MESSAGE_TYPE_SCAN_DATA:

                log_info( LOG_HIGH, host->name,
                          "received scan data message." );

                record_size = (int)GET_MESSAGE_LENGTH( ((MESSAGE *)&message) );

                if( ( record_size > 0 ) &&
                    ( record_size < sizeof( buffer ) ) )
                {
                    unsigned char *raw;
                    SCAN_RECORD *record = (SCAN_RECORD *)buffer;

                    raw = MESSAGE_TO_TYPE( &message, unsigned char * );

                    unpack_scan_record( record, raw, record_size );
                    unwrap_scan_record( record );

                    result = osi_scan_db_store_record( &db, record );
                }

                break;

            case MESSAGE_TYPE_SCAN_DATA_FIRST:

                log_info( LOG_HIGH, host->name,
                          "received first scan-data message." );

                /* this message contains only the type of records. */

                message_size = (int)GET_MESSAGE_LENGTH( ((MESSAGE *)&message) );
                       
                /* make sure we're given the 64 bit record type identifier. */
                        
                if( message_size = sizeof( record_type ) )
                {
                    memcpy( &record_type, message.data, message_size );
                    record_type = NTOHLL( record_type );

                    
                    osi_scan_db_store_header_item( &db,
                                OSI_DB_HEADER_ITEM_RECORD_TYPE,
                                &record_type,
                                sizeof( record_type ) );
                    
                }
                     
                else
                {
                    log_error( LOG_MEDIUM, host->name,
                      "scan-data-first message doesn't contain record type!" );
                }

                break;

            case MESSAGE_TYPE_SCAN_DATA_LAST:

                log_info( LOG_HIGH, host->name,
                          "received last scan data message." );

                results = MESSAGE_TO_TYPE( &message, OSI_SCAN_RESULTS * );

                if( results != NULL )
                {
                    osi_uint64 complete = 1;
                    unwrap_scan_results( results );

                    osi_scan_db_store_header_item( &db,
                                          OSI_DB_HEADER_ITEM_SCAN_RESULTS,
                                          results,
                                          sizeof( OSI_SCAN_RESULTS ) );

                    osi_scan_db_store_header_item( &db,
                                          OSI_DB_HEADER_ITEM_COMPLETE,
                                          &complete,
                                          sizeof( complete ) );

                    log_info( LOG_MEDIUM, host->name, "scan ended." );
                }

                else
                {
                    log_error( LOG_MEDIUM, host->name,
                               "failed to receive scan-data results." );
                }

                goto exit_gracefully;
                break;

            case MESSAGE_TYPE_ERROR:

                error = MESSAGE_TO_TYPE( &message, OSI_ERROR * );
                unwrap_error( error );
                result = osi_scan_db_store_error( &db, error );

                if( result != OSI_DB_OK )
                {
                    log_error( LOG_MEDIUM, host->name,
                               "unable to store error in database." );
                }

                break;

            default:

                log_error( LOG_MEDIUM, host->name,
                           "unrecognized message type in scan-data sequence." );

                break;
        }

    }   /* end for */



exit_gracefully:

    /* close the socket, database, and we are responsible */
    /* for destroying  the context object.                */
   
    osi_close_socket( ssl_socket ); 
    ssl_socket = 0;

    /* if we received a complete scan stream. */

    if( results != NULL )
    {
        compare_db_with_default_for_host( &db, host );
    }

    osi_scan_db_close( &db );
    scan_context_destroy( context );

#ifdef WIN32
    _endthreadex(0);
#endif

    return THREAD_RETURN_VALUE;
}


void scan_host( const char *name, MESSAGE *res )
{
    int ssl_socket;
    int result;

    osi_uint16 type;

    OSI_ERROR error;
    OSI_HOST *host = NULL;

    MESSAGE req;
    SSL *ssl = NULL;

    if( ( name == NULL ) || ( res == NULL ) )
    {
        return;
    }

    initialize_message( res, MESSAGE_TYPE_ERROR );

    /* make sure we have a host name. */

    if( strlen( name ) == 0 )
    {
        error.type = OSI_ERROR_NO_HOST_SPECIFIED;
        osi_strlcpy( error.message, "no host specified.",
                     sizeof( error.message ) );

        message_set_payload( res, &error, sizeof( error ), 0 );
        return;
    }

    /* read host structure from disk. */

    if( ( host = osi_read_host( name ) ) == NULL )
    {
        error.type = OSI_ERROR_HOST_DOES_NOT_EXIST;
        osi_strlcpy( error.message, "specified host does not exist.",
                     sizeof( error.message ) );

        message_set_payload( res, &error, sizeof( error ), 0 );
        return;
    }

    /* now send start scan message to the remote daemon. */

    initialize_message( &req, MESSAGE_TYPE_START_SCAN );

    ssl = osi_ssl_connect_to_host_on_port( host->host, DAEMON_PORT,
                                           ssl_context, FALSE );

    if( ssl == NULL )
    {
        error.type = OSI_ERROR_UNABLE_TO_CONNECT;
        osi_strlcpy( error.message, "unable to contact remote daemon: ",
                     sizeof( error.message ) );
        osi_strlcat( error.message, host->host, sizeof( error.message ) );

        osi_host_destroy( host );
        message_set_payload( res, &error, sizeof( error ), 0 );

        return;
    }

    ssl_socket = SSL_get_fd( ssl );

    if( perform_session_key_negotiation( ssl, host ) == FALSE )
    {
        error.type = OSI_ERROR_SESSION_KEY_NEGOTIATION_FAILED;
        osi_strlcpy( error.message,
                     "session key negotiation with remote host failed.",
                     sizeof( error.message ) );

        message_set_payload( res, &error, sizeof( error ), 0 );
        goto exit_error;
    }

    result = osi_ssl_write_message( &req, ssl );

    if( result != MESSAGE_OK )
    {
        error.type = OSI_ERROR_UNABLE_TO_CONNECT;
        osi_strlcpy( error.message,
                     "unable to communicate with the remote daemon.",
                     sizeof( error.message ) );

        message_set_payload( res, &error, sizeof( error ), 0 );
        goto exit_error;     
    }

    /* now read the response from the daemon.  If the scan was started */
    /* then we spawn a new thread or process to handle the receving    */
    /* and storage of the scan data.                                   */

    if( osi_ssl_read_message( res, ssl ) != MESSAGE_OK )
    {
        error.type = OSI_ERROR_NO_RESPONSE;
        osi_strlcpy( error.message,
                     "no response from remote daemon.",
                     sizeof( error.message ) );

        message_set_payload( res, &error, sizeof( error ), 0 );
        goto exit_error;
    }

    type = GET_MESSAGE_TYPE( res );

    if( type == MESSAGE_TYPE_SUCCESS )
    {
        char *config_id;
        OSI_SCAN_CONTEXT *scan_context;

        scan_context = scan_context_new();
        log_info( LOG_MEDIUM, name, "scan started." );

        /* we started a scan, now we need to setup a new process or */
        /* thread that will handle the receiving of the scan stream */

        scan_context->host = host;
        scan_context->ssl = ssl;

        /* get the configuration id from the deamon's response. */

        config_id = MESSAGE_TO_TYPE( res , char * );

        if( config_id != NULL )
        {
            scan_context->cfg = osi_host_read_config_with_id( name, config_id );
        }

        start_scan_handler( scan_context );
        goto exit_gracefully;
    }

    else
    {
        log_error( LOG_MEDIUM, name, "start scan failed." );
        goto exit_error;
    }

exit_error:

    osi_close_socket( ssl_socket );
    osi_ssl_destroy( &ssl );
    osi_host_destroy( host );

    return;

exit_gracefully:

#ifndef HAVE_THREADS
    osi_close_socket( ssl_socket );
#endif

    return;
}


osi_bool store_config_in_db( configuration *cfg, OSI_DB *db )
{
    osi_bool result = FALSE;

    if( ( cfg == NULL ) || ( db == NULL ) )
    {
        return result;
    }

    if( cfg->id != NULL )
    {
        osi_scan_db_store_header_item( db, OSI_DB_HEADER_ITEM_CONFIG_ID,
                                       cfg->id, strlen( cfg->id ) );
    }

    if( cfg->name != NULL )
    {
        osi_scan_db_store_header_item( db, OSI_DB_HEADER_ITEM_CONFIG_NAME,
                                       cfg->name, strlen( cfg->name ) );
    }

    /* TO-DO: STORE CONFIG DATA */

    result = TRUE;
    return result;
}
