/*
  ulistd - FBB unproto lists recieving daemon
  (c) 1998 - 2002 by Radek Burget OK2JBG <radkovo@centrum.cz>

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version
  2 of the license, or (at your option) any later version.

  HISTORY - started at version 0.08

  30 Nov 99 - version 0.09 - the index file is not read again and again
                             to get the actual number of messages

  14 Jan 00 - version 0.10 - SSID is ignored in the path to list files

  23 Feb 00 - version 0.11
    Some heavy modifications done by Tom Mazouch (mazouch@ica.cz), especially
    for new AX25 lib/tools/apps. Added select() support, protocol machine
    rewritten, things from old ax25 library removed and used new ones from
    libax25.
    Modified for new AX25 from Matthias Welwarsky/Jens David.

    OK2JBG: re-modified back to support both new and old axlib. Minor change
    in protocol machine - do not test the end of list after each message.
    Added SIGUSR1 handler that forces list resync.

  27 Mar 01 - version 0.12 - Config file parsing fixed (times were ignored)
  07 Apr 01 - version 0.13 - Line collector added
  20 Jan 02 - version 0.14
    Some bugfixes (thanks to Mike G0WZY), pasive mode added.
*/

#ifdef HAVE_CONFIG_H
#include <config.h>
#else //default settings
#define NEW_AXLIB   //new axlib (from libax25) (old are ancient ax25-utils)
//#define NEW_AX25    //new ax25 implementation (patched 2.2.x kernel)
#endif

#include <ctype.h>
#include <fcntl.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>
#include <syslog.h>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/if_ether.h>
#include <sys/types.h>
#include <sys/socket.h>

#ifdef NEW_AXLIB
#include <netax25/kernel_ax25.h>	// new for libax25
#include <netax25/kernel_rose.h>	// required by axlib.h
#include <netax25/axlib.h>
#include <netax25/axconfig.h>
#include <netax25/daemon.h>
#else
#include <linux/ax25.h>
#include <linux/rose.h>
#include <ax25/axutils.h>
#include <ax25/axconfig.h>
#include <ax25/daemon.h>
#endif

#ifndef NEW_AXLIB
#define ax25_aton(x, y) convert_call(x, y)
#define ax25_ntoa(x) ax2asc(x)
#endif

#define AX25_AEB	1
#define AX25_UI		0x03
#define	AX25_PID_TEXT	0xF0

#define CALL_OFS 7
#define DATA_OFS 16
#define MIN_SIZE 15

#define COLLECTOR_LIMIT 100 /* line collector limit (lines) */

#define AXPORTS "/etc/ax25/axports"
#define CONFIG "/etc/ax25/ulistd.conf"
#define OUTDIR "/var/ax25/ulistd"
#define ENDOFLISTCMD "/var/ax25/list_agent"
#define COMPLETECMD "/var/ax25/complete_agent"

/* default time to wait for response (seconds) */
#define RESP_TIME 15

/* default time to wait for next frame (seconds) */
#define WAIT_TIME 10

/* number of unsuccesful tries before bailing out */
#define MAX_TRIES 32

#define DAEMON
#define COLLECTOR

#define PRGNAME "ulistd"
#ifndef VERSION
#define VERSION "0.14"
#endif

#define	ST_RQSENT		1
#define	ST_WAIT			2
#define	ST_COMPLETE		0

/* cline - collected line */
struct cline
{
  int num;                      /* message number */
  char *data;                   /* complete line */
  struct cline *next;
};

/* bbs info  - for each bbs */
struct bbs{
  char *call;			/* bbs callsign */
  char *path;			/* bbs path */
  char *port;			/* port callsign */
  int enabled;			/* sending is enabled */
  time_t next_event;		/* time of next event */
  int resptime;			/* how long to wait for response (sec) */
  int waittime;			/* how long to wait for next frame (sec) */
  int last;			/* last msg number on our list */
  int highest;                  /* highest msg number heard */
  int status;			/* communication state */
  int tries;			/* number of tries */
  time_t last_complete;         /* last call of exec_complete_agent */
  struct cline *collect;        /* line collector begining */
  struct bbs *next;
};

static struct bbs *BBSBEG=NULL;

/* setup */
char mycall[20];             /* my callsign */
int logging = 0;             /* write to syslog ? */
int startreq = 1;            /* send requests at starup ? */
int minmsg = 1;              /* start asking this number */
int getmail = 0;             /* start "user agent" on end of list */

int last_message(struct bbs *);

/*======================== Callsign transformations =======================*/
/* Convert callsign into uppercase, strip zero SSID. */
char *normalize_call(char *call)
{
  char *p;

  strupr(call);
  p=strchr(call,'-');
  if(p){
    if(!atoi(p+1)) *p='\0';
  }
  return(call);
}

/* returns the callsign w/o SSID */
char *call_call(const char *src)
{
  static char s[7];
  sscanf(src, "%6[A-Za-z0-9]", s);
  return s;
}

/* Returns 1 if the callsigns (without SSID) match. */
int compare_call(char *c1, char *c2)
{
  for(;*c1&*c2;c1++,c2++){
    if((*c1=='-')&&(*c2=='-')) return(1);
    if(toupper(*c1)!=toupper(*c2)) return(0);
  }
  return(1);
}

/* return the first callsign in the path */
char *first_call(char *dest,const char *src)
{
  sscanf(src,"%10[A-Za-z0-9-]",dest);
  return(dest);
}

/* read & parse config file */
int read_config()
{
  FILE *config;
  struct bbs *bbs;
  char s[256],*port=NULL;
  int i,j;

  if(!(config=fopen(CONFIG,"r"))) return(0);;
  j=0;
  while ((fgets(s,255,config))){
    if((*s!='#')&&(strlen(s))){
      bbs=(struct bbs *)malloc(sizeof(struct bbs));
      memset(bbs,0,sizeof(struct bbs));
      // otestuj pamet
      i=sscanf(s,"%d %d %as %a[^\r\n]",&(bbs->resptime),&(bbs->waittime),
               &port,&(bbs->path));
      if(i!=4){
        free(bbs->port);
	free(bbs->path);
	free(bbs);
      }else{
        bbs->port=ax25_config_get_addr(port);
	free(port);
	first_call(s,bbs->path);
	bbs->call=strdup(normalize_call(s));
	bbs->enabled=1;
	bbs->status=ST_COMPLETE;
	bbs->last=last_message(bbs);
        bbs->highest=0;
        bbs->collect=NULL;
        bbs->next=BBSBEG;
	BBSBEG=bbs;
	j++;
      }
    }
  }
  fclose(config);
  return(j);
}

/* find struct bbs by BBS callsign */
struct bbs *find_bbs(char *call)
{
  struct bbs *bbs;
  
  for(bbs=BBSBEG;bbs;bbs=bbs->next)
    if(!strcasecmp(bbs->call,call)) 
      return(bbs);
  return(NULL);
}

/*=========================== Mail list operations ========================*/

/* returns the number of last message on the list */
int last_message(struct bbs *bbs)
{
  FILE *f;
  char s[256];
  int msgnum=minmsg;
  int i;

  snprintf(s,255,"%s/%s",OUTDIR,call_call(bbs->call));
  if((f=fopen(s,"r"))){
    while(fgets(s,255,f))
      if(sscanf(s,"%i",&i)==1) msgnum=i;
    fclose(f);
  }else{
    if(!(f=fopen(s,"w"))){
      if(logging)
        syslog(LOG_ERR,"Cannot create file %s",s);
    }else fclose(f);
  }
  return msgnum;
}

/* add next line to list */
void add_line(struct bbs *bbs, char *line)
{
  FILE *f;
  char s[256];

  snprintf(s,255,"%s/%s",OUTDIR,call_call(bbs->call));
  f = fopen(s, "a");
  if (!f && logging)
    syslog(LOG_ERR, "Cannot write to file %s\n", s);
  fprintf(f, "%s\n", line);
  fclose(f);
}

/*============================== Line collector ===========================*/

/* add the line to the collector (sorted by message numbers) */
void add_collector_line(struct bbs* bbs, int linenum, const char *linedata)
{
#ifdef COLLECTOR
   struct cline *line;
   struct cline *newline;
   int cnt = 0;

   newline = (struct cline *)malloc(sizeof(struct cline));
   newline->num = linenum;
   newline->data = strdup(linedata);
   newline->next = NULL;

   if (bbs->collect == NULL) bbs->collect = newline;
   else if (bbs->collect->num > linenum)
   {
      newline->next = bbs->collect;
      bbs->collect = newline;
   }
   else
      for (line = bbs->collect; line; line=line->next)
      {
         if (line->next == NULL || line->next->num > linenum)
         {
            newline->next = line->next;
            line->next = newline;
            break;
         }
         cnt++;
         if (cnt > COLLECTOR_LIMIT) break;
      }
#endif
}

/* scan the collector, remove obsolete lines, use and remove actual lines */
void scan_collector(struct bbs *bbs)
{
#ifdef COLLECTOR
  struct cline *line, *nextline;
  struct cline *last = NULL;

  for (line = bbs->collect; line; line=nextline)
  {
     if (line->num == bbs->last+1)
     {
        add_line(bbs, line->data);
        bbs->last = line->num;
     }

     nextline = line->next;
     if (line->num <= bbs->last) //remove old and used lines
     {
        if (last) last->next = line->next; else bbs->collect = line->next;
        free(line->data);
        free(line);
     }
     else last = line;
  }
#endif
}

/*============================ External agents ==========================*/

void exec_download_agent(struct bbs *bbs)
{
  char cmd[1024];
  snprintf(cmd, 1023, "%s %s >/dev/null", ENDOFLISTCMD,
           call_call(bbs->call));
#ifndef DAEMON
  printf("Executing: %s", cmd);
#endif
  system(cmd);
}

void exec_complete_agent(struct bbs *bbs)
{
  char cmd[1024];

  if (time(NULL) < bbs->last_complete+bbs->waittime) return; /* too early */
  bbs->last_complete = time(NULL);
  snprintf(cmd, 1023, "%s %s %i >/dev/null", COMPLETECMD,
           call_call(bbs->call), bbs->last+1);
#ifndef DAEMON
  printf("Executing: %s", cmd);
#endif
  system(cmd);
  bbs->last = last_message(bbs); /* recheck modified list */
  if (bbs->highest > bbs->last) bbs->last = bbs->highest;
}

/*============================ BBS Communication ==========================*/

/* send request to bbs->call with number bbs->last */
void send_request(struct bbs *bbs)
{
  int s;
  int sum,len;
  char path[30];
  char msg[30];
  struct full_sockaddr_ax25 addr;

  /* passive mode */
  if (bbs->resptime == 0)
  {
     exec_complete_agent(bbs);
     return;
  }
  /* already sent */
  if((bbs->next_event>time(NULL))&&(bbs->status==ST_RQSENT)) return;

#ifndef DAEMON
  printf("Request to %s\n", bbs->call);
#endif

  /* find BBS by callsign */
  if(!bbs->enabled) return; //requests not enabled

  /* calculate request */
  sum=(bbs->last>>24)+(bbs->last>>16)+(bbs->last>>8)+bbs->last;
  sprintf(msg,"? %08X%02X",bbs->last,sum&0xFF);

  /* send unproto frame */
  if ((s = socket(AF_AX25, SOCK_DGRAM, 0)) == -1)
    if (logging)
      syslog(LOG_ERR, "Cannot create socket for unproto");
  sprintf(path, "%s %s", mycall,bbs->port);
  len = ax25_aton(path, &addr);
  if (len == -1)
    if (logging)
      syslog(LOG_ERR, "Invalid path %s %s", mycall,bbs->port);
  if (bind(s, (struct sockaddr *)&addr, len) == -1)
     if (logging)
       syslog(LOG_ERR, "Cannot bind() socket for unproto");
  len = ax25_aton(bbs->path, &addr);
  if (len == -1)
    if (logging)
      syslog(LOG_ERR, "Invalid path %s",bbs->path);
  if (sendto(s, msg, strlen(msg), 0, (struct sockaddr *)&addr, len) == -1)
     if (logging)
       syslog(LOG_ERR, "Cannot sendto() unproto message");
  close(s);

  bbs->next_event=time(NULL)+bbs->resptime;
  bbs->status=ST_RQSENT;

  bbs->tries++;
  if(bbs->tries>MAX_TRIES){
    bbs->tries=0;
    bbs->status=ST_COMPLETE; 		//stop sending requests
    if(logging)
      syslog(LOG_ERR,"Specified BBS `%s' is not responding",bbs->call);
  }
}

/* handle one unproto data line */
void handle_fbb_line(char *call, char *line)
{
  int msgnum;
  int n;
  char s1[256], s2[256];
  struct bbs *bbs;

  bbs=find_bbs(call);
  if(!bbs) return; 				//some other BBS
  
  n=sscanf(line,"%i %255s %255s",&msgnum,s1,s2);
  if (bbs->highest < msgnum) bbs->highest = msgnum;
  if (n>0)
  {
    bbs->tries=0;
    if(!strcmp(s1,"/")){ 			//request rejected
      if(compare_call(s2, mycall)){
        bbs->enabled=0;
        if(logging) syslog(LOG_ERR,"Request rejected by %s",bbs->call);
      }
      return;
    }
    
    if(!strcmp(s1,"!")){			//change list start
      if (compare_call(s2, mycall)){
        char s[20];
        sprintf(s, "%i  !", msgnum);
        add_line(bbs, s);
	bbs->last=msgnum;
        bbs->next_event=time(NULL)+bbs->waittime;
        bbs->status=ST_WAIT;
      }
      return;
    }
    
    if(!strcmp(s1,"!!")){		//end of list
      if(msgnum>bbs->last+1){		//incomplete list
        send_request(bbs);
      }else{				//list OK
        bbs->status=ST_COMPLETE;
        if (getmail) exec_download_agent(bbs); //run agent
      }
      return;
    }
    // message line
    if(msgnum==bbs->last+1){ 		//fits to the list
      add_line(bbs,line);
      bbs->last=msgnum;
      bbs->next_event=time(NULL)+bbs->waittime;
      if (bbs->status != ST_COMPLETE)   //don't ask for more unless you
        bbs->status=ST_WAIT;            //  have requested this line
      scan_collector(bbs);
    }else if(msgnum>bbs->last+1){ 	//a hole in the list
      add_collector_line(bbs, msgnum, line);
      scan_collector(bbs);
      send_request(bbs);		//request from our last number
    }
  }
}

/* send initial requests to all BBSs */
void send_init_requests()
{
  struct bbs *bbs;

  for(bbs=BBSBEG;bbs;bbs=bbs->next)
    send_request(bbs);
}

/*=========================================================================*/

/* check if all messages have been received */
void check()
{
  struct bbs *bbs;

  for(bbs=BBSBEG;bbs;bbs=bbs->next){
    if(bbs->status!=ST_COMPLETE){
      if(bbs->next_event<=time(NULL))
        send_request(bbs);		//send the request
    }
  }
  return;
}

/* USR1 signal handler */
void resync(int sig)
{
  struct bbs *bbs;
  
  signal(SIGUSR1, SIG_IGN);
  for(bbs=BBSBEG;bbs;bbs=bbs->next)
    send_request(bbs);
  signal(SIGUSR1, resync);
}

/* TERM signal handler */
void terminate(int sig)
{
  exit(0);
}


int main(int argc, char **argv)
{
  int s; /* socket */
  unsigned char buffer[1024],*p,*l;
  struct sockaddr sa;
  fd_set readfds;
  time_t next_event;
  struct timeval tv,*tvp;
  struct bbs *bbs;
  int size;
  int i;

  char from_call[20];
  char to_call[20];

  *mycall='\0';
  while ((i = getopt(argc, argv, "c:nlgm:vh")) != -1){
     switch(i){
       case 'c': strncpy(mycall, optarg, 15); break;
       case 'n': startreq = 0; break;
       case 'l': logging = 1; break;
       case 'g': getmail = 1; break;
       case 'm': minmsg = atoi(optarg); break;
       #ifdef PACKAGE
       case 'v': printf("%s from %s %s\n", PRGNAME, PACKAGE, VERSION); return 0; break;
       #else
       case 'v': printf("%s version %s\n", PRGNAME, VERSION); return 0; break;
       #endif
       case 'h':
       case ':':
       case '?': printf("ulistd daemon by OK2JBG & CZ6TOM\n");
                 printf("usage: ulistd -c <mycall> [-n] [-l] [-g] [-m number] [-v] [-h]\n");
                 return 1;
                 break;
     }
  }

  normalize_call(mycall);

  if (strlen(mycall) == 0){
    fprintf(stderr, "Callsign is not set. Use -c option.\n");
    return 1;
  }

  if(ax25_config_load_ports() == -1){
    fprintf(stderr, "cannot read file %s\n", AXPORTS);
    return 1;
  }

  if(read_config()<=0){
    fprintf(stderr, "no BBS specified in configuration file %s\n", CONFIG);
    return(1);
  }

  signal(SIGTERM, terminate);
  signal(SIGUSR1, resync);
  if ((s=socket(AF_PACKET,SOCK_PACKET,htons(ETH_P_AX25)))==-1){
    perror("ulistd: socket");
  }
  fcntl(s,F_SETFL,O_NONBLOCK);

#ifdef DAEMON
  if(!daemon_start(0)){
    fprintf(stderr,"ulistd: cannot become a daemon\n");
    return 1;
  }
#endif

  if (logging)
    openlog("ulistd", LOG_PID, LOG_DAEMON);

  if (startreq)
    send_init_requests();

  while(1){
    int asize = sizeof(sa);
    tvp=NULL;
    next_event=0;
    for(bbs=BBSBEG;bbs;bbs=bbs->next){
      if(bbs->status==ST_COMPLETE) 
        continue;
      if((!next_event)||(bbs->next_event<next_event))
        next_event=bbs->next_event;
    }
    if(next_event){
      tv.tv_sec=next_event-time(NULL);
      tv.tv_usec=0;
      tvp=&tv;
    }
    FD_ZERO(&readfds);
    FD_SET(s,&readfds);
    while(select(s+1,&readfds,NULL,NULL,tvp) == -1) ;
    if(FD_ISSET(s,&readfds)){
      size = recvfrom(s, buffer, sizeof(buffer), 0, &sa, &asize);
      if((size!=-1)&&(sa.sa_family==AF_AX25)&&(size>=MIN_SIZE)){
        #ifdef NEW_AX25
        strcpy(to_call, ax25_ntoa((ax25_address *)buffer));
        strcpy(from_call, ax25_ntoa((ax25_address *)(buffer+CALL_OFS)));
        #else
        signed char *data = buffer+1;
        strcpy(to_call, ax25_ntoa((ax25_address *)data));
        strcpy(from_call, ax25_ntoa((ax25_address *)(data+CALL_OFS)));
        #endif
        normalize_call(to_call);
        normalize_call(from_call);
#ifndef DAEMON
        printf("From: %s  To: %s  Data: %i\n", from_call, to_call, size);
#endif

        if(strcmp(to_call,"FBB")) continue;
        for(p=buffer;(!(*p&AX25_AEB))&&size;p++,size--);
	if(size<3) continue;		// Useless frame
	p++;size--;
	if(*p!=AX25_UI) continue;	// Only UI frames wanted
	p++;size--;
	if(*p!=AX25_PID_TEXT) continue;	// PID must be 0xF0
	p++;size--;
	p[size]='\0';
  
#ifndef DAEMON
        printf("Text(%i): %s\n",size,p);
#endif

        /* handle FBB unproto frame */
	while(*p){
          i=strcspn(p,"\r\n");
	  l=p;
	  p+=i;
	  while(*p&&strchr("\r\n",*p)) p++;
	  l[i]='\0';
          handle_fbb_line(from_call,l);
	}    
      }
    } else check();	// nothing in readfds -> timeout
  }
}

