/*
 * Copyright (c) 1990, 1999 Erick Engelke
 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <ctype.h>
#include <io.h>
#include <mem.h>
#include <rtos.h>
#include <net.h>
#include <dir.h>
#include <dos.h>
#include <share.h>

#ifdef __DJGPP__
#include <unistd.h>
#endif

#define S_INIT  0
#define S_WAIT  1
#define S_CONN  2   /* connected and talking */
#define S_DATA  4
#define S_CLOS  5
#define S_HALT  6   /* halt service */

#ifndef O_DENYNONE
#define O_DENYNONE 0
#endif

#ifndef min
#define min(a,b)        (((a) < (b)) ? (a) : (b))
#endif

#ifndef max
#define max(a,b)        (((a) > (b)) ? (a) : (b))
#endif

#ifdef __DJGPP__
#define movmem(s,d,n)   memmove(d,s,n)
#define ltoa(v,b,r)     itoa(v,b,r)
#define mkdir(p)        mkdir(p,0777)
#endif

char *cmds[] = {
    "ABOR", "QUIT", "PASS", "HELP", "NOOP", "USER", 
    "MKD",  "XMKD", "RMD",  "XRMD", "CWD",  "XCWD", "PWD",  "XPWD",
    "SITE", "DEL" , "TYPE", "SIZE", "PORT", "LIST", "NLST", "STOR", "RETR"
    };
enum {
     ABOR,   QUIT,   PASS,   HELP,   NOOP,   USER,
     MKD,    XMKD,   RMD,    XRMD,   CWD,    XCWD,   PWD,    XPWD,
     SITE,   DEL,    TYPE,   SIZE,   PORT,   LIST,   NLST,   STOR,   RETR,
     ENDLIST };




#define FTPDWELCOME "FTPD.WELCOME"
#define FTPDPASS   "FTPD.PASSWORD"
#define FTPDBUFSIZ "FTPD.BUFSIZ"
#define FTPDPORT    21

#define FTP_OP_DIR  0
#define FTP_OP_GET  1
#define FTP_OP_PUT  2
#define FTP_OP_DON  3
#define FTP_OP_NOP  4

#define FTPDIRLEN  65
#define FTPPATHLEN 81

typedef struct {
    int         status;
    tcp_Socket	socket;
    int         dstatus;
    int         doperation;
    int         dcmd;
    tcp_Socket  dsocket;
    struct ffblk ffblk;
    int         dhandle;        /* for file transfer */
    int         gotpass;

    int         ascii;         /* 1 is ascii */
    longword    biglen;         /* for file transfer completion stats */
    word        bufhead;
    word        buftail;

    longword    hisip;          /* used for data connections */
    word        hisport;
    char       *errmsg;
    char        dirpath[ FTPDIRLEN ];
    char        filepath[ FTPPATHLEN ];
} ftpdses;

static char *ftpdbuffer;
int   ftpdbufferlen = 0;

static ftpdses *ftpd;
char ftpdpassword[ 128 ];
char ftpdwelcome[ 128 ];

extern void *getvalue( char * p );
extern int cmdcompare( char *p, char *command );


typedef struct dostimedate {
    unsigned xxx  : 5;
    unsigned min  : 6;
    unsigned hour : 5;
    unsigned date : 5;
    unsigned mon  : 4;
    unsigned year : 7;
} dostimedate;

/*
 * handlerrs - install an error handler which simply returns as though fail
 *             was sellected by the user
 */

#pragma warn -par
/*
 * handlerrs - it prevents abort,retry,ignore,...
 *	     - coded Borland specific
 *	     - start = nonzero installs handler, zero resets it
 */
static void handlerrs( int start )
{
#if !defined(__DJGPP__)
    static void interrupt (*olderrhandler)();

    if ( start ) {
	olderrhandler = getvect( 0x24 );
	harderr( (int (*)()) hardretn );
    } else
	setvect( 0x24, olderrhandler );
#endif
}

/*
 * strins - copy little_string into big_string
 *
 * eg. strins( "0123456", "abcd", 2 ) creates "ab23456"
 */
static void strins( char *big, char *little, int len )
{
    int movlen;
    movlen = min( strlen( little ), ((len > 0) ? len : -len));
    if ( len > 0 ) movmem( little, big, movlen );
    else movmem( little, big - (len + movlen), movlen );
}

/*
 * dirformblock - create a unix "ls -l" style of output for this dir entry
 */

static
void
dirformblock( struct ffblk* ff, char* formatted)
{
  struct dostimedate* pdos_time = (struct dostimedate*) &ff->ff_ftime;
  struct tm unix_time;
  time_t timer = time( NULL );
  int current_year = localtime( &timer )->tm_year + 1900; /* UNIX year bias */
  char buf[16];
  int i;

  memset(&unix_time, 0, sizeof(unix_time));
  unix_time.tm_min  = pdos_time->min;
  unix_time.tm_hour = pdos_time->hour;
  unix_time.tm_mday = pdos_time->date;
  unix_time.tm_mon  = pdos_time->mon - 1; /* DOS months 1-12, UNIX months 0-11 */
  unix_time.tm_year = pdos_time->year + 1980 /* DOS year bias */ - 1900 /* UNIX year bias */;
  
  sprintf( formatted, "%c", ff->ff_attrib & FA_DIREC ? 'd' : '-' );
  sprintf( buf, "r%cx", !(ff->ff_attrib & FA_RDONLY) ? 'w' : '-' );
  for(i = 0; i<3; i++)
    strcat( formatted, buf );
  sprintf( strchr(formatted, 0), " 1 user users %10s ", ltoa( ff->ff_fsize, buf, 10 ) );
  strftime( strchr(formatted, 0), -1,
    pdos_time->year + 1980 == current_year ? "%b %d %H:%M " : "%b %d  %Y ", &unix_time
  );
  strcat(formatted, ff->ff_name);
  strcat(formatted, "\r\n");
}

/*
 * functions to handle paths
 */
static char *pathfile( ftpdses *f, char *fname )
{
    /* better return bad name than overwrite buffers */
    if ( (strlen( f->dirpath ) + strlen( fname ) - 2) >= FTPPATHLEN )
        return( fname );

    // allow hard coding drive letter
    if ( f->dirpath[0] && f->dirpath[1] == ':') {
        f->filepath[0] = 0;
    } else {
        strcpy( f->filepath, ".\\");
    }
    if ( *f->dirpath ) {
        strcat( f->filepath, f->dirpath );
        strcat( f->filepath, "\\");
    }
    strcat( f->filepath, fname );
    return( f->filepath );
}
static int changedir( ftpdses *f, char *path)
{
    struct stat st;
    char *p;

    /* fix unix styled slashes */
    while ( (p = strchr( path, '/' )) != NULL ) *p = '\\';

    /* handle up one dir */
    if ( (path[0] == '.') && (path[1] == '.' )) {
        /* go up a subdir */
        if ( (p = strrchr( f->dirpath, '\\'))!= NULL) {
            *p = 0;
        } else
            f->dirpath[0] = 0;
        return( 0 );    /* say it was successful */
    }

    /* \ root is .\ */
    if ( (path[0] == '\\' ) && ( path[1] == 0 )) {
        *f->dirpath = 0;
        return( 0 );
    }


    if (( path[0] ) && ( path[1] == ':')) {
        // path = "c:\xxx"
    }
    /* if they didn't specify \ then assume we are adding to path */
    if ( *path == '\\' )
        path++;     /* skip leading \ for relative directory */
    else {
        strcpy( f->filepath, f->dirpath );
        if ( *f->dirpath )
            strcat( f->filepath, "\\");
        strcat( f->filepath, path );
        path = f->filepath;
    }

    if ( stat( path, &st ) != 0 )      /* no such file or directory */
        return( 1 );

    if ((st.st_mode & S_IFDIR ) == 0 )
        return( 1 ); /* not a directory */

    /* use new directory path */
    strncpy( f->dirpath, path, FTPDIRLEN - 1 );
    f->dirpath[ FTPDIRLEN - 1] = 0;
    return( 0 );
}


/*
 * data_op_... operations on data socket
 */

/*
 * data_op_get - does a "GET"
 *             - could be optimized for speed by allocing a second
 *               buffer and performing the DOS read while waiting for
 *               the remote host to ack the data
 */
static void data_op_get( ftpdses *f, tcp_Socket *t )
{
    int len;
    unsigned diff, wrote, ofs;
    if ( sock_tbused( t ) == 0 ){
        diff = 0;
        if( _dos_read( f->dhandle, ftpdbuffer, ftpdbufferlen, &diff ) != 0 )
	    f->errmsg = "550 Error while trying to read file\r\n";        
        else if ( diff == 0 ) /* eof or possibly error condition */
	    f->doperation = FTP_OP_DON;
        else {
            // was good, sit in loop writing it out
            wrote = ofs = 0;
            do {
                sock_mode( t, TCP_MODE_NONAGLE );
                wrote = sock_fastwrite( t, ftpdbuffer + ofs, diff );
                ofs += wrote;
                diff -= wrote;
                if ( wrote == 0 ) rt_sleep( 0 );
            } while ( diff > 0 );
        }
    }
}

static void data_op_init( ftpdses *f, word operation )
{
    f->doperation = operation;
    f->dstatus = S_INIT;
    f->status = S_DATA;
    f->biglen = 0;
    f->bufhead = f->buftail = 0;
    f->errmsg = "250 Done\r\n";

    sock_puts( &f->socket, "150 Openning data connection\r\n");
}


static void c_op_dir( ftpdses *f, char *path )
{
    char buffer[ 81 ], p;
    int status;

    f->dhandle = 0;

    /* support pseudo-standard -L */
    if ( !strnicmp( path, "-L", 2 )) {
        path += 2;
        if ( !*path ) path = NULL;
    }

    /* new for directories */
    if ( path != NULL ) strcpy( buffer, pathfile( f, path ));
    else strcpy( buffer, pathfile( f, "*.*" ));
//    if (path != NULL ) movmem( path, buffer, sizeof(buffer) - 1 );
//    else strcpy( buffer, "*.*" );

    buffer[ sizeof( buffer ) - 1 ] = 0;
    status = findfirst( buffer, &f->ffblk, FA_DIREC);

    if ( !strchr( buffer, '*' ) &&
         !strchr( buffer, '?' ) &&
         !status                &&
         (ftpd->ffblk.ff_attrib & FA_DIREC ) &&
	 *ftpd->ffblk.ff_name != '.') {

	strcat( buffer, "\\*.*");
	status = findfirst( buffer, &f->ffblk, FA_DIREC);
    }
    if (!status) data_op_init( f, FTP_OP_DIR );
    else sock_puts(&f->socket, "550 File or directory not found\r\n");
}

static void (*oldinit)(char *, char *);
static void newinit( char *directive, char *value )
{
    char *p;
    if (!strcmp( directive, FTPDWELCOME )) {
        strcpy( ftpdwelcome, "220-");
        p = strchr( ftpdwelcome, 0 );
        strncpy( p, value, sizeof( ftpdwelcome) -1 - strlen( ftpdwelcome) );
        ftpdwelcome[ sizeof( ftpdwelcome ) - 1] = 0;
    }
    if (!strcmp( directive, FTPDPASS )) {
        strncpy( ftpdpassword, value, sizeof(ftpdpassword)-1);
        ftpdpassword[ sizeof(ftpdpassword) - 1] = 0;
    } else if (!strcmp( directive, FTPDBUFSIZ ))
        ftpdbufferlen = atoi( value );
    else
        if (oldinit) (*oldinit)( directive, value );
}


/*
 * getport - reads the FTP port negotiation stuff
 *         - return nonzero on success
 *
 */
static int getport( ftpdses *f, char *text )
{
    byte a[6], *p;
    int i;

    for ( i = 0 ; (i < 6) && text ; ++i ) {
        if (( p = strchr( text , ',' )) != NULL) *p++ = 0;
        a[i] = atoi( text );
        text = p;
    }
    if ( i == 6 ) {
        f->hisip = intel( *(longword*)a );
        f->hisport = intel16( *(word*)&a[4] );
    } else {
        f->hisip = 0L;          /* should be gained from his structure */
        f->hisport = 0;
    }
    return( i == 6 );
}


/*
 * parseftpline - called with a particular session and some data
 *              - note, this baby supports multiple FTP sessions
 *                by calling with different ftpdses values
 */


static void parseftpline( ftpdses *f, tcp_Socket *s, char *line )
{
    char *p, *q;
    int cmdnum;
    FILE* file;

    /* remove blanks from line - p points to parameters */
    p = strchr( line, ' ');
    while ( *p == ' ') *p++ = 0;
    if ((q = strchr( p, ' '))!=NULL)
        while ( *q == ' ' ) *q++ = 0;

    if (!*line) return;     /* nothing there */
    for ( cmdnum = 0 ; cmdnum < ENDLIST ; ++cmdnum )
        if ( !strnicmp( line, cmds[ cmdnum ], strlen( cmds[ cmdnum ] )))
            break;
    if ( cmdnum == ENDLIST ) {
        sock_puts( s, "500 Command not understood '");
        sock_puts( s, line );
        sock_puts( s, "'\r\n");

        /* check for unprivileged access */
    } else if ( cmdnum > USER && f->gotpass == 0 )
        sock_puts( s, "530 You're not logged in yet\r\n");

        /* file commands need a port number */
    else if ( cmdnum > PORT && ( f->hisport == 0 || f->hisip ==0L ))
        sock_puts( s, "425 Sorry, need PORT command first\r\n");

        /* do the normal thing */
    else switch( f->dcmd = cmdnum ) {
        case ABOR   :
            if (f->status == S_DATA ) {
                sock_abort( &f->dsocket );
                sock_puts( s, "426 Aborting at user's request\r\n");
            } else
                sock_puts( s, "550 Abort what?\r\n");
            break;

        case QUIT   :
            sock_puts( s, "221 Goodbye\r\n");
            sock_close( s );
            sock_close( &f->dsocket );    /* if necessary */
            f->status = S_CLOS;
            break;

        case USER   :
            sock_puts( s, "331 Password required\r\n");
            break;

        case PASS   :
            if ( (*ftpdpassword && *line && !strcmp( p, ftpdpassword )) || !*ftpdpassword ) {
                f->gotpass = 1;
                sock_puts( s, "230 User logged in\r\n");
            } else {
                sock_puts( s, "530 Not quite\r\n");
                f->gotpass = 0;
            }
            break;

        case HELP   :
            sock_puts( s, "214-HELP - list of command accepted by WATTCP FTP daemon\r\n");
            sock_puts( s, "    ABOR  QUIT  USER  PASS  HELP  NOOP  DEL   TYPE  PORT\r\n");
            sock_puts( s, "    LIST  NLST  SIZE  STOR  RETR  SITE\r\n");
            sock_puts( s, "    RMD   XRMD  MKD   XMKD  CWD   XCWD  PWD   XPWD\r\n");
            sock_puts( s, "214 OK\r\n");
            break;

        case RMD    :
        case XRMD   :
            if ( rmdir( pathfile(f,p) ) == 0 )
              sock_puts( s, "250 RMD worked\r\n");
            else
              sock_puts( s, "550 RMD failed\r\n");
            break;
        case MKD    :
        case XMKD   :
            if ( mkdir( pathfile(f,p)) == 0 )
              sock_puts( s, "250 MKD worked\r\n");
            else
              sock_puts( s, "550 MKD failed\r\n");
            break;
        case PWD    :
        case XPWD   :
            sock_puts( s, "257 \"");
            sock_puts( s, f->dirpath );
            sock_puts( s, "\" is current directory.\r\n");
            break;
        case CWD    :
        case XCWD   :
            if ( !changedir( f, p )) {
                sock_puts( s, "250 changed directory to ");
                sock_puts( s, f->dirpath );
                sock_puts( s, "\r\n");
            } else
                sock_puts( s, "550 CWD failed\r\n");
            break;

        case DEL    :
            sock_puts( s, (!unlink(pathfile(f,p))) ?
                "250 File deleted - \"" : "550 Not Deleted - \"" );
            sock_puts( s, p );
            sock_puts( s, "\"\r\n");
            break;

        case PORT   :
            if ( getport( f, p ))
                sock_puts( s, "200 Port command successful\r\n");
            else
                sock_puts( s, "501 Bad port command\r\n");
            break;

        case NLST :
        case LIST   :
            c_op_dir( f, p );       /* start directory */
            break;

        case STOR   :               /* start a put */
            unlink( pathfile(f, p) );
            if ( (f->dhandle =  open( pathfile(f,p), O_RDWR | O_CREAT |
                ((f->ascii) ? O_TEXT : O_BINARY) | O_DENYNONE, S_IWRITE)) == -1) {

//                                creat( pathfile(f,p), S_IWRITE )) == -1 ) {
                sock_puts( &f->socket, "550 Could not create file '");
                sock_puts( &f->socket, p );
                sock_puts( &f->socket, "'.\r\n");
            } else
                data_op_init( f, FTP_OP_PUT );
            break;

        case RETR   :               /* start a get */
            if ( (f->dhandle = open( pathfile(f,p), O_RDONLY ||
                ( /* (f->ascii) ? O_TEXT : */ O_BINARY))) == -1) {
                sock_puts( &f->socket, "550 Could not open file '");
                sock_puts( &f->socket, p );
                sock_puts( &f->socket, "' for reading\r\n");
            } else
                data_op_init( f, FTP_OP_GET );
            break;

        case SIZE   :
            file = fopen( p, "rb" );
            if ( file == 0 || fseek( file, 0, SEEK_END ) != 0 ) {
                sock_puts( &f->socket, "550 Could not open file '");
                sock_puts( &f->socket, p );
                sock_puts( &f->socket, "' for reading\r\n");
            } else {
                long int fsize = ftell(file);
                sock_puts( &f->socket, "213 " );
                ltoa( fsize, ftpdbuffer, 10 );
                sock_puts( &f->socket, ftpdbuffer );
                sock_puts( &f->socket, "\n" );
                fclose( file );                
            }
            break;
            
        case TYPE   :
            *p = toupper( *p );
            if ( *p == 'B' || *p == 'I' ) f->ascii = 0;
            else if ( *p == 'A' ) f->ascii = 1;
            else if ( *p == 'L' && *q == '8' ) f->ascii = 0;
            else {
                sock_puts( s, "550 Type not understood\r\n");
                p = NULL;
            }
            if (p)
                sock_puts( s, (f->ascii)? "200 ASCII mode\r\n":"200 Binary mode\r\n");
            break;
        case NOOP  :
            sock_puts( s, "200 NOOP\r\n");
            break;
        case SITE :
            if ( !strnicmp( p, "HALT", 4 )) {
                sock_puts( s, "221 Goodbye! Halting service\r\n");
                sock_close( s );
                sock_close( &f->dsocket );  /* if necessary */
                f->status = S_HALT;
            } else {
                sock_puts( s, "500 command not understood 'SITE ");
                sock_puts( s, p );
                sock_puts( s, "'\r\n");
            }
            break;

    }
}

/*
 * ftpd_alt_tick - processes data when we are doing something and not
 *                 awaiting user input
 */
static void ftpd_alt_tick( ftpdses *f)
{
    tcp_Socket *t;
    int len, wlen;

    t = &f->dsocket;

    switch ( ftpd->dstatus ) {
        case S_INIT :
                tcp_open( t, FTPDPORT-1, f->hisip, f->hisport, NULL );
                f->dstatus = S_WAIT;

        case S_WAIT :
                if ( sock_established( t )) ftpd->dstatus = S_CONN;
                break;

        case S_CONN :
                switch ( ftpd->doperation ) {

                    case FTP_OP_DIR :   /* directory operation */
                            while ( sock_tbleft( t ) > 80 ) {
                                if( f->dcmd == NLST ) {
                                    sock_puts( t, f->ffblk.ff_name );
                                    sock_puts( t, "\n" );
                                } else {
                                    dirformblock( &f->ffblk, ftpdbuffer );
                                    sock_puts( t, ftpdbuffer );
                                }
                                if ( findnext( &f->ffblk ))
                                    f->doperation = FTP_OP_DON;
                                break;
                            }
                            break;

                    case FTP_OP_PUT :   /* part of put */
                            if ((len = sock_fastread( t, ftpdbuffer, ftpdbufferlen)) > 0 ) {
                                wlen = write( f->dhandle, ftpdbuffer, len );
                                if (wlen < len || wlen == -1 ) {
                                    f->doperation = FTP_OP_DON;
                                    f->errmsg = "550 Error incurred while writing file\r\n";
                                }
                            }
                            break;

                    case FTP_OP_GET : data_op_get(f,t); break;

                    case FTP_OP_DON : /* close socket .... */
                            sock_close( t );
                            if (f->dhandle) close( f->dhandle );
                            f->dhandle = 0;
                            sock_puts( &f->socket, f->errmsg );
                            f->doperation = FTP_OP_NOP;
                            break;

                    case FTP_OP_NOP : break;
                }
                if (!tcp_tick( t )) {
                    if ( ftpd->doperation < FTP_OP_DON )
                        ftpd->doperation = FTP_OP_DON;
                    else {
                        sock_close( t );
                        ftpd->status = S_CONN;
                        ftpd->dstatus = S_INIT;
                    }
                }
                break;
    }
}



/*
 * ftpd_tick - call this whenever you have a free moment
 *           - modify this routine to use several ftpdses
 *             structures if you wish
 */
void ftpd_tick( void )
{
    tcp_Socket *t;
    t = &ftpd->socket;


    switch ( ftpd->status ) {
        case S_INIT :   sock_abort( t );
                        memset( ftpd, 0, sizeof( ftpdses ));
                        ftpd->ascii = 0;
                        tcp_listen( t, FTPDPORT, 0L, 0, NULL, 0 );
                        ftpd->status = S_WAIT;
                        break;
        case S_WAIT :   if ( sock_established( t )) {
                            sock_puts(t,"220-FTP Server\r\n");
                            if (*ftpdwelcome) {
                                sock_puts(t,ftpdwelcome);
				sock_puts(t,"\r\n");
                            }
                            sock_puts(t,"220 Login\r\n");
                            ftpd->status = S_CONN;
                            sock_mode( t, TCP_MODE_ASCII );
                        }
                        if ( !tcp_tick( t ))
                            ftpd->status = S_CONN;

                        break;
        case S_DATA :   sock_mode( t, TCP_MODE_BINARY );
                        ftpd_alt_tick( ftpd );
                        sock_mode( t, TCP_MODE_ASCII );
                        /* fall through */
                        if ( !tcp_tick( t )) {
                            sock_abort( t );
                            sock_abort( &ftpd->dsocket );
                            ftpd->status = S_CLOS;
                        }

	case S_CONN :   if ( sock_dataready( t )) {
                            sock_gets( t, ftpdbuffer, ftpdbufferlen);
                            sock_mode( t, TCP_MODE_BINARY );
                            parseftpline( ftpd, t, ftpdbuffer );
                            sock_mode( t, TCP_MODE_ASCII );
                        }
			if (!tcp_tick( t )) {
                            sock_close( t );
                            ftpd->status = S_CLOS;
                        }
                        break;
        case S_CLOS :   if ( !tcp_tick( t ) ) {
                            ftpd->status = S_INIT;
                            sock_close( t );
                        }
                        break;
        case S_HALT :   break;
    }
}

#ifdef __DJGPP__
__attribute__((constructor))
#endif

static void ftpd_init()
{
    *ftpdpassword = 0;
    *ftpdwelcome = 0;
    oldinit = usr_init;
    usr_init = newinit;
    ftpd = calloc( sizeof( ftpdses ), 1 );  /* before kernel init */
    ftpd->status = S_INIT;
}

#pragma startup ftpd_init 100

void ftpdthread( DWORD x )
{
    if (!ftpdbufferlen) ftpdbufferlen = 2048;
    ftpdbuffer = kcalloc( 1, ftpdbufferlen);

    handlerrs( 1 );

    if ( *ftpdpassword == 0 )
        cprintf("WARNING: ftpd.password is not set in config file, any password will match\r\n");


    while (1) {
	ftpd_tick();

        /* improve response only while doing real stuff */
        if ( ftpd->status == S_HALT ) break;
        else if ( ftpd->status == S_DATA ) rt_sleep(0);
        else rt_sleep( 100 );
    }

    /* if SITE was used to halt us */
    while ( tcp_tick( &ftpd->socket )) rt_sleep( 100 );
    kfree( ftpdbuffer );
    handlerrs( 0 );
}

