/*
 * conf.c - syslogd implementation for windows, configuration reader
 *
 * Created by Alexander Yaworsky
 *
 * THIS SOFTWARE IS NOT COPYRIGHTED
 *
 * This source code is offered for use in the public domain. You may
 * use, modify or distribute it freely.
 *
 * This code is distributed in the hope that it will be useful but
 * WITHOUT ANY WARRANTY. ALL WARRANTIES, EXPRESS OR IMPLIED ARE HEREBY
 * DISCLAIMED. This includes but is not limited to warranties of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 */

/* define SYSLOG_CONF_DIR where syslog.host should be
 */

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <winsock2.h>

#include <glib.h>

#include <syslog.h>
#include <syslogd.h>

#ifndef SYSLOG_CONF_DIR
static char syslog_conf_dir[] = ".";
#else
static char syslog_conf_dir[] = SYSLOG_CONF_DIR;
#endif

/* options and their default values */
gboolean use_dns = TRUE;
gchar *source_encoding = NULL;
gchar *destination_encoding = NULL;
int mark_interval = 0;
gchar *mark_message = "-- MARK --";
int hold = 3;
gchar *logdir = NULL;

/* sources, destinations, filters and logpaths */
struct logpath_names
{
    gchar *source;
    gchar *filter;
    gchar *destination;
};

GList *sources = NULL;
GList *destinations = NULL;
GList *filters = NULL;
GList *logpaths = NULL;

GList *purger_dirs = NULL;

/* Glib markup wrapper data */
static gchar *encoding = NULL;
static gboolean prolog_expected = TRUE;
/* parser data */
static struct filter *current_filter = NULL;

/******************************************************************************
 * xml_start_element
 *
 * parse configuration elements
 */
static void xml_start_element (GMarkupParseContext *context,
                               const gchar         *element_name,
                               const gchar        **attribute_names,
                               const gchar        **attribute_values,
                               gpointer             user_data,
                               GError             **error)
{
    const gchar *aname, *aval;
    int line_number;

    prolog_expected = FALSE;

    g_markup_parse_context_get_position( context, &line_number, NULL );

    /* top-level elements */
    if( strcmp( element_name, "source" ) == 0 )
    {
        struct source *source = g_malloc( sizeof(struct source) );

        source->name = NULL;
        source->type = ST_UNDEFINED;
        memset( &source->udp, 0, sizeof(source->udp) );
        source->udp.sin_family = AF_INET;
        source->udp.sin_port = htons( SYSLOG_PORT );

        for( ; (aname = *attribute_names) != NULL; attribute_names++, attribute_values++ )
        {
            aval = *attribute_values;
            if( strcmp( aname, "name" ) == 0 )
                source->name = g_locale_from_utf8( aval, -1, NULL, NULL, NULL );
            else if( strcmp( aname, "type" ) == 0 )
            {
                if( strcmp( aval, "internal" ) == 0 )
                    source->type = ST_INTERNAL;
                else if( strcmp( aval, "udp" ) == 0 )
                    source->type = ST_UDP;
            }
            else if( strcmp( aname, "interface" ) == 0 )
            {
                struct hostent *he = gethostbyname( aval );
                if( !he )
                {
                    ERR( "Cannot resolve hostname %s; error %lu\n", aval, WSAGetLastError() );
                    g_free( source );
                    return;
                }
                memcpy( &source->udp.sin_addr.s_addr, he->h_addr, he->h_length );
            }
            else if( strcmp( aname, "port" ) == 0 )
                source->udp.sin_port = htons( strtoul( aval, NULL, 0 ) );
        }

        if( !source->name )
            ERR( "Undefined source name at line %d\n", line_number );
        if( ST_UNDEFINED == source->type )
            ERR( "Undefined source type at line %d\n", line_number );
        if( (!source->name) || ST_UNDEFINED == source->type )
        {
            g_free( source );
            return;
        }
        sources = g_list_append( sources, source );
    }
    else if( strcmp( element_name, "destination" ) == 0 )
    {
        struct destination *dest = g_malloc0( sizeof(struct destination) );

        for( ; (aname = *attribute_names) != NULL; attribute_names++, attribute_values++ )
        {
            aval = *attribute_values;
            if( strcmp( aname, "name" ) == 0 )
                dest->name = g_locale_from_utf8( aval, -1, NULL, NULL, NULL );
            else if( strcmp( aname, "file" ) == 0 )
                dest->file = normalize_pathname( aval );
            else if( strcmp( aname, "rotate" ) == 0 )
            {
                if( strcmp( aval, "daily" ) == 0 )
                    dest->rotate = RP_DAILY;
                else if( strcmp( aval, "weekly" ) == 0 )
                    dest->rotate = RP_WEEKLY;
                else if( strcmp( aval, "monthly" ) == 0 )
                    dest->rotate = RP_MONTHLY;
                else
                {
                    ERR( "Invalid rotation period at line %d\n", line_number );
                    dest->rotate = RP_INVALID;
                }
            }
            else if( strcmp( aname, "size" ) == 0 )
            {
                char *endptr;
                dest->size = strtoul( aval, &endptr, 0 );
                if( 'k' == *endptr )
                    dest->size *= 1024;
                else if( 'M' == *endptr )
                    dest->size *= 1024 * 1024;
            }
            else if( strcmp( aname, "backlogs" ) == 0 )
                dest->backlogs = strtoul( aval, NULL, 0 );
            else if( strcmp( aname, "ifempty" ) == 0 )
            {
                if( strcmp( aval, "yes" ) == 0 )
                    dest->ifempty = TRUE;
                else if( strcmp( aval, "no" ) == 0 )
                    dest->ifempty = FALSE;
                else
                {
                    dest->ifempty = TRUE;
                    ERR( "Invalid value \"%s\" of attribute \"%s\" at line %d; assumed \"yes\"\n",
                         aval, aname, line_number );
                }
            }
            else if( strcmp( aname, "olddir" ) == 0 )
                dest->olddir = normalize_pathname( aval );
            else if( strcmp( aname, "compresscmd" ) == 0 )
                dest->compresscmd = g_locale_from_utf8( aval, -1, NULL, NULL, NULL );
            else if( strcmp( aname, "compressoptions" ) == 0 )
                dest->compressoptions = g_locale_from_utf8( aval, -1, NULL, NULL, NULL );
        }
        if( !dest->name )
            ERR( "Undefined destination name at line %d\n", line_number );
        if( !dest->file )
            ERR( "Undefined destination file at line %d\n", line_number );
        if( (!dest->name) || (!dest->file) || RP_INVALID == dest->rotate )
        {
            if( dest->name ) g_free( dest->name );
            if( dest->file ) g_free( dest->file );
            if( dest->olddir ) g_free( dest->olddir );
            if( dest->compresscmd ) g_free( dest->compresscmd );
            if( dest->compressoptions ) g_free( dest->compressoptions );
            g_free( dest );
            return;
        }
        if( dest->compresscmd && !dest->compressoptions )
            dest->compressoptions = g_strdup( "$PATHNAME" );

        dest->file_writers = NULL;
        InitializeCriticalSection( &dest->cs_file_writers );

        destinations = g_list_append( destinations, dest );
    }
    else if( strcmp( element_name, "filter" ) == 0 )
    {
        current_filter = g_malloc0( sizeof(struct filter) );

        for( ; (aname = *attribute_names) != NULL; attribute_names++, attribute_values++ )
        {
            if( strcmp( aname, "name" ) == 0 )
                current_filter->name = g_locale_from_utf8( *attribute_values, -1, NULL, NULL, NULL );
        }
        if( !current_filter->name )
        {
            ERR( "Undefined filter name at line %d\n", line_number );
            g_free( current_filter );
            current_filter = NULL;
            return;
        }
    }
    else if( strcmp( element_name, "logpath" ) == 0 )
    {
        /* at first, fill logpaths list with logpath_names structures
           and replace them later with logpath structures after configuration has been read
         */
        struct logpath_names *logpath = g_malloc0( sizeof(struct logpath_names) );

        for( ; (aname = *attribute_names) != NULL; attribute_names++, attribute_values++ )
        {
            aval = *attribute_values;
            if( strcmp( aname, "source" ) == 0 )
                logpath->source = g_locale_from_utf8( aval, -1, NULL, NULL, NULL );
            else if( strcmp( aname, "filter" ) == 0 )
                logpath->filter = g_locale_from_utf8( aval, -1, NULL, NULL, NULL );
            else if( strcmp( aname, "destination" ) == 0 )
                logpath->destination = g_locale_from_utf8( aval, -1, NULL, NULL, NULL );
        }
        if( !logpath->source )
            ERR( "Undefined log path source at line %d\n", line_number );
        if( !logpath->destination )
            ERR( "Undefined log path destination at line %d\n", line_number );
        if( (!logpath->source) || (!logpath->destination) )
        {
            if( logpath->source ) g_free( logpath->source );
            if( logpath->filter ) g_free( logpath->filter );
            if( logpath->destination ) g_free( logpath->destination );
            g_free( logpath );
            return;
        }
        logpaths = g_list_append( logpaths, logpath );
    }
    else if( strcmp( element_name, "options" ) == 0 )
    {
        for( ; (aname = *attribute_names) != NULL; attribute_names++, attribute_values++ )
        {
            aval = *attribute_values;
            if( strcmp( aname, "dns" ) == 0 )
            {
                if( strcmp( aval, "yes" ) == 0 )
                    use_dns = TRUE;
                else if( strcmp( aval, "no" ) == 0 )
                    use_dns = FALSE;
                else
                    ERR( "Invalid value \"%s\" of attribute \"%s\" at line %d\n", aval, aname, line_number );
            }
            else if( strcmp( aname, "source_encoding" ) == 0 )
                source_encoding = g_strdup( aval );
            else if( strcmp( aname, "destination_encoding" ) == 0 )
                destination_encoding = g_strdup( aval );
            else if( strcmp( aname, "mark_interval" ) == 0 )
                mark_interval = strtoul( aval, NULL, 0 );
            else if( strcmp( aname, "mark_message" ) == 0 )
                mark_message = g_locale_from_utf8( aval, -1, NULL, NULL, NULL );
            else if( strcmp( aname, "hold" ) == 0 )
            {
                hold = strtoul( aval, NULL, 0 );
                if( hold < 1 )
                    hold = 1;
            }
            else if( strcmp( aname, "logdir" ) == 0 )
                logdir = normalize_pathname( aval );
        }
    }
    else if( strcmp( element_name, "purge" ) == 0 )
    {
        struct purger_dir *pdir = g_malloc0( sizeof(struct purger_dir) );

        for( ; (aname = *attribute_names) != NULL; attribute_names++, attribute_values++ )
        {
            aval = *attribute_values;
            if( strcmp( aname, "directory" ) == 0 )
                pdir->directory = normalize_pathname( aval );
            else if( strcmp( aname, "keep_days" ) == 0 )
                pdir->keep_days = strtoul( aval, NULL, 0 );
        }
        if( !pdir->directory )
            ERR( "Undefined purge directory at line %d\n", line_number );
        if( !pdir->keep_days )
            ERR( "Undefined keep_days parameter at line %d\n", line_number );
        if( (!pdir->directory) || (!pdir->keep_days) )
        {
            if( pdir->directory ) g_free( pdir->directory );
            g_free( pdir );
            return;
        }
        purger_dirs = g_list_append( purger_dirs, pdir );
    }
    else if( current_filter )
    {
        /* sub-elements of filter */
        int val = -2;

        if( strcmp( element_name, "facility" ) == 0 )
        {
            for( ; (aname = *attribute_names) != NULL; attribute_names++, attribute_values++ )
            {
                aval = *attribute_values;
                if( strcmp( aname, "value" ) == 0 )
                {
                    val = strtol( aval, NULL, 0 );
                    if( val < 0 || val >= LOG_NFACILITIES )
                    {
                        val = -1;
                        break;
                    }
                }
                else if( strcmp( aname, "name" ) == 0 )
                {
                    CODE *c;
                    for( c = facilitynames; c->c_name; c++ )
                        if( strcmp( aval, c->c_name ) == 0 )
                        {
                            val = LOG_FAC( c->c_val );
                            break;
                        }
                    if( !c->c_name )
                    {
                        val = -1;
                        break;
                    }
                }
            }
            if( -2 == val )
                ERR( "Undefined facility at line %d\n", line_number );
            else if( val != -1 )
                current_filter->facilities[ val ] = TRUE;
        }
        else if( strcmp( element_name, "priority" ) == 0 )
        {
            for( ; (aname = *attribute_names) != NULL; attribute_names++, attribute_values++ )
            {
                aval = *attribute_values;
                if( strcmp( aname, "value" ) == 0 )
                {
                    val = strtol( aval, NULL, 0 );
                    if( val < 0 || val >= 8 )
                    {
                        val = -1;
                        break;
                    }
                }
                else if( strcmp( aname, "name" ) == 0 )
                {
                    CODE *c;
                    for( c = prioritynames; c->c_name; c++ )
                        if( strcmp( aval, c->c_name ) == 0 )
                        {
                            val = LOG_PRI( c->c_val );
                            break;
                        }
                    if( !c->c_name )
                    {
                        val = -1;
                        break;
                    }
                }
            }
            if( -2 == val )
                ERR( "Undefined priority at line %d\n", line_number );
            else if( val != -1 )
                current_filter->priorities[ val ] = TRUE;
        }

        if( -1 == val )
            ERR( "Invalid value \"%s\" of attribute \"%s\" at line %d\n", aval, aname, line_number );
    }
}

/******************************************************************************
 * xml_end_element
 */
static void xml_end_element (GMarkupParseContext *context,
                             const gchar         *element_name,
                             gpointer             user_data,
                             GError             **error)
{
    if( strcmp( element_name, "filter" ) == 0 )
    {
        if( current_filter )
        {
            /* append filter to the list */
            filters = g_list_append( filters, current_filter );
            current_filter = NULL;
        }
    }
}

/******************************************************************************
 * xml_passthrough
 *
 * look for encoding name
 */
static void xml_passthrough (GMarkupParseContext *context,
                             const gchar         *passthrough_text,
                             gsize                text_len,  
                             gpointer             user_data,
                             GError             **error)
{
    const gchar *startptr, *endptr;

    if( !prolog_expected )
        return;

    startptr = g_strstr_len( passthrough_text, text_len, "<?xml " );
    if( !startptr )
        return;

    startptr += 6;
    text_len -= startptr - passthrough_text;

    endptr = g_strstr_len( startptr, text_len, "?>" );
    if( !endptr )
        goto parsed;

    text_len = endptr - startptr;
    startptr = g_strstr_len( startptr, text_len, "encoding=\"" );
    if( !startptr )
        goto parsed;

    startptr += 10;

    endptr = strchr( startptr, '"' );
    if( !endptr )
        goto parsed;

    if( strncmp( startptr, "windows-", 8 ) == 0 )
    {
        gchar *p;

        startptr += 8;
        p = g_strndup( startptr, endptr - startptr );
        if( !p )
            goto parsed;
        encoding = g_strdup_printf( "CP%s", p );
        g_free( p );
    }
    else
        encoding = g_strndup( startptr, endptr - startptr );

parsed:
    prolog_expected = FALSE;
}

/******************************************************************************
 * resolve_logpaths
 *
 * replace logpath_names structures
 */
static void resolve_logpaths()
{
    GList *paths = NULL;
    GList *path_item;
    struct logpath *logpath = NULL;

    for( path_item = logpaths; path_item; path_item = path_item->next )
    {
        struct logpath_names *names = path_item->data;
        GList *item;

        g_free( logpath );
        logpath = g_malloc( sizeof(struct logpath) );

        /* find source */
        for( item = sources; item; item = item->next )
        {
            struct source *s = item->data;
            if( strcmp( s->name, names->source ) == 0 )
                break;
        }
        if( !item )
        {
            ERR( "Undefined source \"%s\" in log path\n", names->source );
            continue;
        }
        logpath->source = item->data;

        /* find destination */
        for( item = destinations; item; item = item->next )
        {
            struct destination *d = item->data;
            if( strcmp( d->name, names->destination ) == 0 )
                break;
        }
        if( !item )
        {
            ERR( "Undefined destination \"%s\" in log path\n", names->destination );
            continue;
        }
        logpath->destination = item->data;

        /* find filter */
        if( !names->filter )
            logpath->filter = NULL;
        else
        {
            for( item = filters; item; item = item->next )
            {
                struct filter *f = item->data;
                if( strcmp( f->name, names->filter ) == 0 )
                    break;
            }
            if( item )
                logpath->filter = item->data;
            else
                logpath->filter = NULL;
        }
        /* add item to paths */
        paths = g_list_append( paths, logpath );
        logpath = NULL;
    }

    /* free list */
    for( path_item = logpaths; path_item; path_item = path_item->next )
    {
        struct logpath_names *names = path_item->data;
        g_free( names->source );
        g_free( names->destination );
        if( names->filter ) g_free( names->filter );
        g_free( names );
    }
    g_list_free( logpaths );

    /* set new list */
    logpaths = paths;
}

/******************************************************************************
 * dump_configuration
 */
static void dump_configuration()
{
#   ifdef HAVE_DEBUG

    GList *item;

    TRACE( "Sources:\n" );
    for( item = sources; item; item = item->next )
    {
        struct source *s = item->data;
        TRACE( "\tname=%s\ttype=%s\tinterface=%d:%d:%d:%d\tport=%d\n",
               s->name,
               (s->type == ST_INTERNAL)? "internal" : ((s->type == ST_UDP)? "udp" : "undefined"),
               s->udp.sin_addr.S_un.S_un_b.s_b1, s->udp.sin_addr.S_un.S_un_b.s_b2,
               s->udp.sin_addr.S_un.S_un_b.s_b3, s->udp.sin_addr.S_un.S_un_b.s_b4,
               ntohs( s->udp.sin_port ) );
    }
    TRACE( "Destinations:\n" );
    for( item = destinations; item; item = item->next )
    {
        struct destination *d = item->data;
        TRACE( "\tname=%s\tfile=%s\n"
               "\t\trotate=%s size=%d backlogs=%d ifempty=%s\n"
               "\t\tolddir=%s compresscmd=%s\n",
               d->name, d->file,
               (d->rotate == RP_DAILY)? "daily"
                   : (d->rotate == RP_WEEKLY)? "weekly"
                   : (d->rotate == RP_MONTHLY)? "monthly"
                   : "undefined",
               d->size, d->backlogs, d->ifempty? "yes" : "no",
               d->olddir? d->olddir : "NULL",
               d->compresscmd? d->compresscmd : "NULL" );
    }
    TRACE( "Filters:\n" );
    for( item = filters; item; item = item->next )
    {
        struct filter *f = item->data;
        int i;
        TRACE( "\tname=%s\n", f->name );
        TRACE( "\tfacilities:\n" );
        for( i = 0; i < LOG_NFACILITIES; i++ )
            if( f->facilities[i] )
                TRACE( "\t\t%s\n", get_facility_name( i ) );
        TRACE( "\tpriorities:\n" );
        for( i = 0; i < 8; i++ )
            if( f->priorities[i] )
                TRACE( "\t\t%s\n", get_priority_name( i ) );
    }
    TRACE( "Log paths:\n" );
    for( item = logpaths; item; item = item->next )
    {
        struct logpath *p = item->data;
        TRACE( "\tsource=%s\tfilter=%s\tdestination=%s\n",
               p->source->name, p->filter? p->filter->name : "NULL", p->destination->name );
    }
    TRACE( "Purge directories:\n" );
    for( item = purger_dirs; item; item = item->next )
    {
        struct purger_dir *p = item->data;
        TRACE( "\tdirectory=%s\tkeep_days=%d\n", p->directory, p->keep_days );
    }
    TRACE( "Options:\n" );
    TRACE( "\tuse_dns=%d\n", (int) use_dns );
    TRACE( "\tsource_encoding=%s\n", source_encoding? source_encoding : "NULL" );
    TRACE( "\tdestination_encoding=%s\n", destination_encoding? destination_encoding : "NULL" );
    TRACE( "\tmark_interval=%d\n", mark_interval );
    TRACE( "\tmark_message=%s\n", mark_message );
    TRACE( "\thold=%d\n", hold );
    TRACE( "\tlogdir=%s\n", logdir? logdir : "NULL" );

#   endif /* HAVE_DEBUG */
}

/******************************************************************************
 * read_configuration
 */
gboolean read_configuration()
{
    gboolean ret = FALSE;
    GMarkupParser parser = {
        xml_start_element,
        xml_end_element,
        NULL,
        xml_passthrough,
        NULL
    };
    gchar *pathname;
    FILE *fd = NULL;
    GMarkupParseContext *ctx = NULL;
    char buffer[256];
    GError *error = NULL;

    TRACE_ENTER( "\n" );

    if( '\\' == syslog_conf_dir[0] || '/' == syslog_conf_dir[0] || ':' == syslog_conf_dir[1] )
        /* absolute path */
        pathname = g_build_filename( syslog_conf_dir, "syslog.conf", NULL );
    else
        /* relative path */
        pathname = g_build_filename( g_path_get_dirname( __argv[0] ),
                                     syslog_conf_dir, "syslog.conf", NULL );
    fd = fopen( pathname, "r" );
    if( !fd )
    {
        ERR( "Cannot open configuration file %s: %s\n", pathname, strerror(errno) );
        goto done;
    }

    ctx = g_markup_parse_context_new( &parser, 0, NULL, NULL );
    if( !ctx )
    {
        ERR( "Failed g_markup_parse_context_new\n" );
        goto done;
    }

    while( fgets( buffer, sizeof(buffer), fd ) )
    {
        gchar *encoded = NULL;
        gchar *parser_input;
        gboolean r;

        /* wrapper for Glib's XML parser:
           determine encoding in xml_passthrough and convert data fed to the parser to UTF-8 */
        if( encoding )
        {
            encoded = g_convert( buffer, -1, "UTF-8", encoding, NULL, NULL, &error );
            if( !encoded )
                goto done;

            parser_input = encoded;
        }
        else
            parser_input = buffer;

        r = g_markup_parse_context_parse( ctx, parser_input, strlen( parser_input ), &error );
        if( encoded ) g_free( encoded );
        if( !r )
            goto done;
    }
    if( !feof( fd ) )
    {
        ERR( "Cannot read configuration file %s: %s\n", pathname, strerror(errno) );
        goto done;
    }

    resolve_logpaths();
    dump_configuration();
    if( !logdir )
    {
        ERR( "logdir is not defined\n" );
        goto done;
    }
    ret = TRUE;

done:
    if( error )
    {
        gchar *locale_msg = g_locale_from_utf8( error->message, -1, NULL, NULL, NULL );
        if( locale_msg )
        {
            ERR( "%s\n", locale_msg );
            g_free( locale_msg );
        }
        g_error_free( error );
    }
    if( ctx ) g_markup_parse_context_free( ctx );
    if( fd ) fclose( fd );
    g_free( pathname );

    TRACE_LEAVE( "done; ret=%d\n", (int) ret );
    return ret;
}