/*
 * Multiplexing File Server Copyright (c) 2018-2025 James Bailie.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * 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.
 *     * The name of James Bailie may not be used to endorse or promote
 * products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER 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 COPYRIGHT OWNER OR CONTRIBUTORS 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 <syslog.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>

#include <syslog.h>
#include <stdarg.h>

#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>

#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/param.h>

#include <openssl/ssl.h>

#include "frameserver.h"
#include "stack.h"

#define TIMEOUT 60

struct file
{
   int fd, ord, state;
   char *filename;
   DIR *dir;
   struct stat st;
   time_t mtime;
   size_t offset, count;
};

struct ccb
{
   int idx, closing;
   struct stack *queue;
   struct ccb **ccbp;
   void *conn;
   time_t last;
};

struct ccb **conns = NULL, **conn_ptr = NULL;

void timeout_conns()
{
   struct ccb **cptr;
   int i;
   time_t now;

   now = time( NULL );

   for( i = 0, cptr = conns; i < fs_max_conn; ++i, ++cptr )
   {
      if ( *cptr == NULL )
         continue;

      if (( now - ( *cptr )->last ) > TIMEOUT )
         fs_free_conn(( *cptr )->conn );
   }
}

void fs_init_func()
{
   if (( conns = malloc( fs_max_conn * sizeof( struct ccb * ))) == NULL )
   {
      syslog( LOG_ERR, "malloc(): %m" );
      exit( 1 );
   }

   bzero( conns, fs_max_conn * sizeof( struct ccb * ));
   conn_ptr = conns;
}

void fs_exit_func()
{
   free( conns );
}

int fs_accept_callback( void *conn )
{
   struct stack *queue;
   struct ccb *ccb;

   if (( ccb = malloc( sizeof( struct ccb ))) == NULL )
      return 1;

   if (( queue = stack_make()) == NULL )
   {
      free( ccb );
      return 1;
   }

   ccb->idx   = ccb->closing = 0;
   ccb->queue = queue;
   ccb->conn  = conn;
   ccb->last  = time( NULL );

   fs_set_data( conn, ccb );

   /*
    * Because the frameserver does not accumulate more than fs_max_conn
    * connections, we know that there is at least one slot open.  The goto
    * will not create an infinite loop.
    */

AGAIN:
   while( conn_ptr <= &conns[ fs_max_conn - 1 ] && *conn_ptr != NULL )
      ++conn_ptr;

   if ( conn_ptr > &conns[ fs_max_conn - 1 ] )
   {
      conn_ptr = conns;
      goto AGAIN;
   }

   *conn_ptr = ccb;
   ccb->ccbp = conn_ptr;

   if ( fs_conn_count == 1 )
      fs_set_periodic( timeout_conns, TIMEOUT );

   return 0;
}

int fs_open_callback( void *conn )
{
   int len;
   unsigned char buffer[ 32 ];
   struct ccb *ccb;

   if (( ccb = fs_get_data( conn )) == NULL )
      return 1;

   len = snprintf(( char *)buffer, sizeof( buffer ), "Multifile-%s:%d", VERSION, fs_max_requests );

   if ( len >= sizeof( buffer ))
   {
      syslog( LOG_ERR, "libmessage generated an oversized advertisement. "
                       "This should not happen!" );
      return 1;
   }

   if ( fs_queue_frame( conn, ++len, buffer ))
      return 1;

   return 0;
}

void clear_queue( struct stack *queue )
{
   union stack_val *val;
   struct file *fptr;

   while( queue->used )
   {
      val = stack_pop( queue );
      fptr = val->ptr;

      if ( fptr->fd > 0 )
         close( fptr->fd );

      if ( fptr->filename != NULL )
         free( fptr->filename );

      if ( fptr->dir != NULL )
         closedir( fptr->dir );

      free( fptr );
   }
}

void fs_close_callback( void *conn )
{
   struct ccb *ccb;

   if ( ! fs_conn_count )
      fs_set_periodic( NULL, 0 );

   if (( ccb = fs_get_data( conn )) == NULL )
      return;

   *ccb->ccbp = NULL;

   clear_queue( ccb->queue );
   stack_free( ccb->queue );
   free( ccb );
}

void close_file( struct file *fptr )
{
   fptr->state = -1;

   if ( fptr->dir != NULL )
   {
      closedir( fptr->dir );
      fptr->dir = NULL;
   }

   if ( fptr->fd < 0 )
      return;

   close( fptr->fd );
   fptr->fd = -1;
}

void send_terminator_frame( void *conn, struct ccb *ccb )
{
   clear_queue( ccb->queue );
   ccb->idx = 0;

   if ( fs_queue_frame( conn, 1, ( unsigned char *)"\0" ) < 0 )
      fs_free_conn( conn );
}

void open_file( struct file *fptr, time_t *ctime, size_t *count )
{
   size_t diff;

   if ( fptr->st.st_ctime <= fptr->mtime || fptr->offset >= fptr->st.st_size )
   {
      *ctime = fptr->st.st_ctime;
      return;
   }

   if (( fptr->fd = open( fptr->filename, O_RDONLY )) < 0 )
      return;

   if ( fptr->offset && lseek( fptr->fd, fptr->offset, SEEK_SET ) < 0 )
   {
      close_file( fptr );
      return;
   }

   diff = fptr->st.st_size - fptr->offset;

   if ( ! fptr->count || fptr->count > diff )
      fptr->count = diff;

   *ctime = fptr->st.st_ctime;
   *count = fptr->count;
}

void open_dir( struct file *fptr, time_t *ctime, size_t *count )
{
   if ( fptr->st.st_ctime <= fptr->mtime )
   {
      *ctime = fptr->st.st_ctime;
      close_file( fptr );
      return;
   }

   if (( fptr->dir = opendir( fptr->filename )) == NULL )
      return;

   readdir( fptr->dir );
   readdir( fptr->dir );

   *count = 1;
}

int read_dir( struct file *fptr, unsigned char *buffer, unsigned int size )
{
   struct dirent *dp;
   struct stat st;
   char *type, directory[ MAXPATHLEN + 1 ];
   int len;

   if ( ++fptr->state == 2 )
   {
      len = snprintf(( char *)buffer , size, "%ld:%s/", fptr->st.st_ctime, ( *fptr->filename == '.' ? "" : fptr->filename ));

      if ( len >= size )
         return 0;

      return len + 1;
   }

   if (( dp = readdir( fptr->dir )) == NULL )
      return 0;

   len = snprintf( directory, sizeof( directory ), "%s/%s", ( *fptr->filename == '.' ? "" : fptr->filename ), dp->d_name );

   if ( len >= sizeof( directory ) || stat( directory, &st ) < 0 )
      return 0;

   type = "";

   if ( S_ISDIR( st.st_mode ))
      type = "/";

   len = snprintf(( char *)buffer, size, "%ld:%ld:%s/%s%s", st.st_ctime, st.st_size, ( *fptr->filename == '.' ? "" : fptr->filename ), dp->d_name, type );

   if ( len >= size )
      return 0;

   return len + 1;
}

#define minimum(a, b) ((a) > (b) ? (b) : (a))

void fs_write_callback( void *conn )
{
   int i, n, active = 0, sendfile = 0;
   unsigned char buffer[ fs_max_frame ];
   struct ccb *ccb;
   struct file *fptr;
   time_t ctime;
   size_t count;

   if (( ccb = fs_get_data( conn )) == NULL || ! ccb->queue->used )
      return;

   for( i = 0; i < ccb->queue->used; ++i )
   {
      fptr         = ccb->queue->values[ i ].ptr;
      buffer[ 0 ]  = fptr->ord;
      n = sendfile = 0;

      switch( fptr->state )
      {
         case -1:
            continue;

         case 0:
            if (( fptr->state = stat( fptr->filename, &fptr->st )) == -1 )
            {
               buffer[ 1 ] = '0';
               buffer[ 2 ] = ':';
               buffer[ 3 ] = '0';
               buffer[ 4 ] = '\0';
               n = 4;
               break;
            }

            count = 0;
            ctime = 0;

            if ( S_ISREG( fptr->st.st_mode ))
               open_file( fptr, &ctime, &count );
            else if ( S_ISDIR( fptr->st.st_mode ))
               open_dir( fptr, &ctime, &count );

            fptr->state = ( ! count ? -1 : 1 );
            n = snprintf(( char *)&buffer[ 1 ], sizeof( buffer ) - 1, "%lu:%ld", count, ctime );

            if ( n >= sizeof( buffer ))
            {
               fs_free_conn( conn );
               return;
            }

            ++n;
            break;

         default:
            if ( fptr->dir != NULL )
            {
               if ( !( n = read_dir( fptr, &buffer[ 1 ], sizeof( buffer ) - 1 )))
               {
                  close_file( fptr );
                  continue;
               }
            }
            else if ( ! ( sendfile = fs_has_ktls( fptr )))
            {
               if (( n = read( fptr->fd, &buffer[ 1 ], minimum( fptr->count, sizeof( buffer ) - 1 ))) <= 0 )
               {
                  close_file( fptr );
                  continue;
               }

               if ( !( fptr->count -= n ))
                  close_file( fptr );
            }
      }

      if ( ! sendfile )
      {
         if ( fs_queue_frame( conn, n + 1, buffer ) < 0 )
         {
            fs_free_conn( conn );
            return;
         }
      }
      else
      {
         ssize_t len = minimum( fptr->count, sizeof( buffer ) - 1 );

         if ( ! len )
         {
            close_file( fptr );
            continue;
         }
         else
         {
            buffer[ 0 ] = ( unsigned char )fptr->ord;

            if ( fs_queue_sendfile_frame( conn, 1, buffer, fptr->fd, fptr->offset, len ))
            {
               fs_free_conn( conn );
               return;
            }

            fptr->count  -= len;
            fptr->offset += len;
         }
      }

      ++active;

      if ( fs_serial )
         break;
   }

   ccb->last = time( NULL );

   if ( ! active )
      send_terminator_frame( conn, ccb );
}

void stop_transfer( struct ccb *ccb, int ord )
{
   int i;

   for( i = 0; i < ccb->queue->used; ++i )
   {
      struct file *fptr;

      fptr = ccb->queue->values[ i ].ptr;

      if ( ord == fptr->ord )
      {
         close_file( fptr );
         break;
      }
   }
}

void process_stops( void *conn, struct ccb *ccb, unsigned int len, unsigned char *buffer )
{
   char *ptr, *input;
   int ord;

   if ( ! ccb->queue->used || buffer[ len - 1 ] != '\0' )
      return;

   input = ( char *)buffer;

   while(( ptr = strsep( &input, "." )) != NULL )
   {
      if ( ! *ptr )
         continue;

      if (( ord = strtol( ptr, NULL, 10 )) < 0 || ( ! ord && errno == EINVAL ))
         continue;

      if ( ! ord )
      {
         send_terminator_frame( conn, ccb );
         return;
      }

      stop_transfer( ccb, ord );
   }
}

void extract_conditions( struct file *fptr, char **path )
{
   char *ptr, *offset, *count;

   fptr->mtime = 0;
   fptr->offset = 0;
   fptr->count = 0;

   if ( **path < '0' || **path > '9' )
      return;

   for( ptr = *path; *ptr; ++ptr )
      if ( *ptr == ':' || *ptr == '/' )
         break;

   if ( ! *ptr )
      return;

   fptr->mtime = ( time_t )strtol( *path, NULL, 10 );

   if ( *ptr == '/' )
   {
      *path = ptr;
      return;
   }

   for( offset = ++ptr; *ptr; ++ptr )
      if ( *ptr == ':' || *ptr == '/' )
         break;

   fptr->offset = ( size_t )strtol( offset, NULL, 10 );

   if ( ! *ptr || *ptr == '/' )
   {
      *path = ptr;
      return;
   }

   for( count = ++ptr; *ptr; ++ptr )
      if ( *ptr == '/' )
         break;

   *path = ptr;
   fptr->count = ( size_t )strtol( count, NULL, 10 );
}

int extract_name( struct file *fptr, char *path )
{
   char *ptr;

   if ( ! *path || *path == '.' )
      return 1;

   for( ptr = path; *ptr; ++ptr )
      if ( *ptr == '/' && ptr[ 1 ] == '.' && ptr[ 2 ] == '.' && ( ! ptr[ 3 ] || ptr[ 3 ] == '/' ))
         return 1;

   while( --ptr >= path && *ptr == '/' )
      *ptr = '\0';

   if ( ! *path )
      *path = '.';

   if (( fptr->filename = strdup( path )) == NULL )
      return 1;

   return 0;
}

unsigned char *get_line( unsigned char **input, unsigned int *len )
{
   unsigned char *ptr, *start;
   unsigned int line_len = 0;

   ptr = start = *input;

   while( *len && *ptr != '\0' )
   {
      --*len;
      ++ptr;
      ++line_len;
   }

   if ( ! *len && ! line_len )
      return NULL;

   if ( *len )
   {
      --*len;
      *input = ptr + 1;
   }

   return start;
}

void fs_read_callback( void *conn, unsigned int len, unsigned char *buffer )
{
   unsigned char *ptr, *input;
   struct ccb *ccb;
   union stack_val val;
   struct file *fptr;

   if ( buffer[ len - 1 ] != '\0' || ( ccb = fs_get_data( conn )) == NULL )
   {
      fs_free_conn( conn );
      return;
   }

   if ( *buffer == '.' )
   {
      process_stops( conn, ccb, len, buffer );
      return;
   }

   input = buffer;

   while(( ptr = get_line( &input, &len )) != NULL )
   {
      if ( ccb->idx == fs_max_requests )
         break;

      if (( fptr = malloc( sizeof( struct file ))) == NULL )
      {
         fs_free_conn( conn );
         return;
      }

      fptr->filename = NULL;
      fptr->state    = fptr->fd = -1;
      fptr->ord      = ++ccb->idx;
      fptr->dir      = NULL;

      extract_conditions( fptr, ( char **)&ptr );

      if ( extract_name( fptr, ( char *)ptr ))
      {
         free( fptr );
         fs_free_conn( conn );
         return;
      }

      fptr->state = 0;
      val.ptr = fptr;

      if ( stack_push( ccb->queue, val ))
      {
         free( fptr->filename );
         free( fptr );
         fs_free_conn( conn );
         return;
      }
   }

   fs_write_callback( conn );
}
