/*
 * FreeTime: Acorn Time Client
 * Network communication and time handling code
 * Based on RFC868 - Time protocol
 *
 *  Joseph Heenan, 1996-9
 * All rights reserved.
 *
 * $Id: rdate,v 1.10 2001/10/23 20:55:28 joseph Exp $
 *
 */

/*
  From RFC868:

When used via UDP the time service works as follows:

   S: Listen on port 37 (45 octal).

   U: Send an empty datagram to port 37.

   S: Receive the empty datagram.

   S: Send a datagram containing the time as a 32 bit binary number.

   U: Receive the time datagram.

   The server listens for a datagram on port 37.  When a datagram
   arrives, the server returns a datagram containing the 32-bit time
   value.  If the server is unable to determine the time at its site, it
   should discard the arriving datagram and make no reply.

The Time

The time is the number of seconds since 00:00 (midnight) 1 January 1900
GMT, such that the time 1 is 12:00:01 am on 1 January 1900 GMT; this
base will serve until the year 2036.

*/

#define TIME_PORT 37


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

int errno;

#include "unixlib.h"
#include "inetlib.h"
#include "socklib.h"
#include "netinet/in.h"
#include "sys/socket.h"
#include "sys/ioctl.h"
/* #include "sys/byteorder.h" */
#include "sys/errno.h"
#include "sys/time.h"
#include "netdb.h"

#include "swis.h"

#include "defines.h"
#include "dst.h"
#include "config.h"
#include "rdate.h"

#include "dnslib.h"

/* NetLib does not define close() */
extern int close(int /*s*/);


/* states for the main function */
typedef enum
{
  STARTUP,
  START,
  RESOLVE,
  REPLY,
  END
} state_t;


/* prototypes */
static int rdate_startup(char *error,int *socke,dns_t **query);
static int rdate_resolve(char *error,int *sock, dns_t **query,time_t t);
static int rdate_reply(char *error, int *sock,int maxdiff,int mindiff,time_t t);
static int days_in_year(int year);
static int days_in_month(int month,int year);
static int *time_to_ordinals(time_t t);
static int set_time(time_t t);
static char *socket_err_string(void);


/* information preserved across calls to rdate_main */

/* used to store difference between server and us so we can change
   clock later */
static time_t	    rdate_diff;
/* handle on dns query */
static dns_t        *dns = NULL;
/* socket in use */
static int	    sock = -1;

/* abort a query to the time server - called by the frontend if it is quit */
void rdate_abort( void )
{
  if (dns)
    dns_dispose(dns);
  dns = NULL;

  if ( sock != -1 )
    close( sock );
  sock = -1;
}

/* Main time fetch/set procedure
 * returns :
 *     0 -  In progress
 *   >=1 -  finished (value indicates outcome)
 *    <0 -  failed, don't retry
 * <-100 -  failed, could retry
 * error should be a 512 byte buffer, which will be used if the return is non-zero
 */
int rdate_main(char *error, int argc, char *argv[])
{
  /* state variables, need to be preserved between calls */
  static state_t    state = STARTUP;

  /* variables for this routine */
  static int  	    code;
  static time_t	    t;

  switch (state)
  {
    case STARTUP:
      code = config_load();
      if (code<0) strcpy(error,"Startup error occured");
      if (code>=0) state=START;
      config_parsecmdline( argc, argv );
      break;

    case START:
      code = rdate_startup(error,&sock,&dns);
      if (code>0) {state=RESOLVE; t=time(NULL);}
      break;

    case RESOLVE:
      code = rdate_resolve(error,&sock,&dns,t);
      if (code>0) { state=REPLY; t=time(NULL);}
      break;

    case REPLY:
      code = rdate_reply( error, &sock, config_maxdiff, config_mindiff, t );
      if (code>0) state=END;
      break;

    default: /* internal error */
      strcpy(error,"Internal error in rdate_main");
      return -1;
  }
  /* code < 0 is an error, code == 0 means in progress, code > 0 means finished */
  if (code<0) { state=START; return code; }

  if (state==END)
    return code;

  return 0;
}


/* claim socket and start asynchronous dns query */
static int rdate_startup(char *error,int *socke,dns_t **query)
{
  char *hostname = config_server;
  struct sockaddr_in server;
  int 	 	     sock,tmp;

  /* create socket */
  if ((sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1)
  {
    sprintf(error,"Could not create socket - %s",socket_err_string());
    return -3;
  }

  /* mark as non-blocking */
  tmp=1;
  if (ioctl(sock, FIONBIO, &tmp) == -1)
  {
    sprintf(error,"Could not make socket non-blocking - %s",socket_err_string());
    close(sock);
    return -4;
  }

  /* bind socket to local address */
  memset(&server,0,sizeof(server));
  server.sin_family      = AF_INET;
  server.sin_port        = 0;        /* stack will allocate port */
  server.sin_addr.s_addr = INADDR_ANY;
  if (bind(sock, (void *)&server, sizeof(server))==-1)
  {
    sprintf(error,"Could not listen on socket - %s",socket_err_string());
    close(sock);
    return -5;
  }

  if (hostname[0] == '<' && hostname[strlen(hostname) - 1] == '>')
  {
    char *end = hostname + strlen(hostname) - 1;
    const char *newhost;

    *end = '\0';
    newhost = getenv(hostname + 1);
    *end = '>';

    if (newhost == NULL)
    {
      sprintf(error,"Server configuration system variable (%.32s) doesn't exist ", hostname);
      close(sock);
      return -6;
    }

    *query=dns_gethostbyname(newhost);
  }
  else
  {
    *query=dns_gethostbyname(hostname);
  }

  if (*query==NULL)
  {
    strcpy(error,"Error resolving hostname");
    close(sock);
    return -100;
  }

  /* name resolving now in progress. return and wait for answer */
  *socke=sock;
  return 1;
}


/* wait for dns query to complete, then send the request datagram to the server */
static int rdate_resolve(char *error,int *sock, dns_t **query,time_t t)
{
  static dns_status_t status;
  static struct hostent *host;
  struct sockaddr_in server;
  char data;

  status = dns_check(*query);

  if (status==dns_complete_failure)
  {
    strcpy(error,"Could not resolve server address");
    dns_dispose(*query);
    *query = NULL;
    close( *sock );
    *sock = -1;
    return -101;
  }

  if (status!=dns_complete_success)
  {
    if (difftime(time(NULL),t)>config_timeout)
    {
      strcpy(error,"Timeout trying to resolve server address");
      dns_dispose(*query);
      *query = NULL;
      close(*sock);
      *sock = -1;
      return -102; /* failed */
    }
    return 0; /* in progress */
  }

  host = dns_getanswer(*query);

  if ((host==NULL) || (host->h_addr_list==NULL))
  {
    strcpy(error,"Could not resolve server address");
    dns_dispose(*query);
    *query = NULL;
    close(*sock);
    *sock = -1;
    return -103;
  }

  /* tell stack remote address for this socket */
  memset(&server,0,sizeof(server));
  server.sin_family = AF_INET;
  server.sin_port=htons(37);
  memcpy((char *)&server.sin_addr,host->h_addr,host->h_length);

  dns_dispose(*query);
  *query = NULL;

  /* NB. this is udp, not tcp - we do not actually 'connect' to the server */
  if (connect(*sock,(void *)&server, sizeof(server))==-1)
  {
    sprintf(error,"Could not set remote address - %s",socket_err_string());
    close(*sock);
    *sock = -1;
    return -104;
  }

  /* send an empty datagram */
  if (send(*sock,&data,0,0)!=0)
  {
    sprintf(error,"Data send to server failed - %s",socket_err_string());
    close(*sock);
    *sock = -1;
    return -105;
  }

  return 1; /* send in progress, wait for reply */
}


/* wait for the reply from the time server, and process it according to the configuration */
int rdate_reply(char *error, int *sock,int maxdiff,int mindiff,time_t t)
{
  int result;
  time_t answer;
  time_t local;
  char *ptr;
  char x;

  local=time(NULL);

  /* receive time datagram */
  result = recv(*sock,(char *)&answer,sizeof(answer),0);

  if ((result == -1) && (errno==EWOULDBLOCK))
  {
    if (difftime(local,t)>config_timeout)
    {
      strcpy(error,"Timeout waiting for server to reply");
      close(*sock);
      *sock = -1;
      return -106; /* failed */
    }
    return 0; /* waiting for reply still */
  }

  close(*sock);
  *sock = -1;

  if (result==-1)
  {
    sprintf(error,"Error waiting for reply from time server - %s",socket_err_string());
    return -106;
  }

  if (result!=sizeof(answer))
  {
    sprintf(error,"Wrong size data packet received from server");
    return -107;
  }

  /* reverse byte order in buffer */
  ptr=(char *)&answer;
  x=ptr[2]; ptr[2]=ptr[1]; ptr[1]=x;
  x=ptr[3]; ptr[3]=ptr[0]; ptr[0]=x;

  answer -= ( local - t ) / 2; /* deduct half the RTT, to compensate for slow connections */

  local = mktime( gmtime(&local) ); /* takes value from time(NULL) and converts it to GMT */

  answer -= 2208988800U; /* adjust to secs from 1970, rather than secs from 1900 */


  if (((int)local-(int)answer) >= 0)
    sprintf(error,"Time from server is %s GMT. Clock is %d seconds fast - ",
            ctime(&answer),local-answer);
  else
    sprintf(error,"Time from server is %s GMT. Clock is %d seconds slow - ",
            ctime(&answer),answer-local);

  /* remove the \n ctime added for us */
  while ((ptr = strchr(error,'\n')) != NULL)
    *ptr=' ';

  rdate_diff = (int)local - (int)answer;

  if (abs(rdate_diff) < mindiff)
  {
    strcat(error,"not adjusted");
    if (config_setdst)
    {
      int code;
      char *dstmsg;
      code = dst_check(&dstmsg);
      /* any error or response isn't reported to the user.... */
      /*if (code == -1)
        strcpy(error, dstmsg);*/
    }
    return 2;
  }

  if (abs(rdate_diff) > maxdiff && maxdiff != 0)
  {
    strcat(error,"quite far out, still set time?");
    return 3;
  }

  if (!set_time(time(NULL)-rdate_diff))
  {
    strcpy(error,"Error setting time");
    return -108;
  }

  strcat(error,"clock adjusted");
  if (config_setdst)
  {
    int code;
    char *dstmsg;
    code = dst_check(&dstmsg);
    /* any error or response isn't reported to the user.... */
    /*if (code == -1)
      strcpy(error, dstmsg);*/
  }

  return 1; /* success */
}

/* set the time - called by frontend when the user confirms they want to use
 * a clock setting wildly different to their current one */
int rdate_settime(void)
{
  int code;
  int ret;
  char *dstmsg;

  ret = set_time(time(NULL) - rdate_diff);

  if (config_setdst)
  {
    code = dst_check(&dstmsg);
    /* any error or response isn't reported to the user.... */
    /*if (code == -1)
      strcpy(error, dstmsg);*/
  }

  return ret;
}


/* Takes a time_t representing local time, and sets the internal clock by it */
static int set_time(time_t t)
{
  int *ords;
  char nettime[5];
  _kernel_oserror *e;
  _kernel_swi_regs r,out;

  /* convert time_t value to 7 words for ConvertOrdinalsToTime */
  ords=time_to_ordinals(t);

  /* convert our local time to a 5 byte UTC riscos style time */
  r.r[0]=-1;
  r.r[1]=(int) nettime;
  r.r[2]=(int) ords;
  e = _kernel_swi(Territory_ConvertOrdinalsToTime,&r,&out);

  if (e!=NULL)
  {
     return 0;
  }

  r.r[0]=(int) nettime;
  e = _kernel_swi(Territory_SetTime,&r,&out);
  if (e!=NULL) {
	return 0;
  }

  return 1;
}

/* convert time t into an array of the ordinals (usec,secs,mins,hours,date,month,year) */
int *time_to_ordinals(time_t t)
{
  static int ords[7];

  ords[0] = 0;       /* usec */

  ords[1] = t % 60;  /* secs */
  t /= 60;

  ords[2] = t % 60;  /* mins */
  t /= 60;

  ords[3] = t % 24;  /* hours */
  t /= 24;

  /* answer now contains days since 1970 */

  ords[6]=1970; /* base year for time */
  while (t>=days_in_year(ords[6]))
  {
    t-=days_in_year(ords[6]);
    ords[6]++;
  }

  ords[5]=1; /* first month */
  while (t>=(days_in_month(ords[5],ords[6])))
  {
    t-=days_in_month(ords[5],ords[6]);
    ords[5]++;
  }
  ords[4]=t+1; /* answers contains number of days into month */

  return ords;
}

/* returns 1 if the year is a leap year. */
static int leap_year(int year)
{
  int yr;

  yr = year%100; /* contains year within century */
  year -= yr; /* contains century */

  if (yr == 0) /* if xx00 */
    return ((year%4) == 0);

  return ((yr%4) == 0);
}


/* returns the number of days in the given year */
static int days_in_year(int year)
{
  if (leap_year(year)) return 366;
  return 365;
}


/* returns the number of days in the given month in the given year */
static int days_in_month(int month,int year)
{
  static int days[]={31,28,31,30,31,30,31,31,30,31,30,31};
  if ((month<1) || (month>12)) return -1;
  if ((month==2) && (leap_year(year)))
    return 29;
  return days[month-1];
}


/* returns a string giving a textual description of the value of errno
 * (strerror()/perror() are provided by the shared c library, and do not know
 * about the socket error codes) */
static char *socket_err_string(void)
{
  static char ueb[64];

  switch (errno) {
    case 0: return "No error";
    default:
            sprintf(ueb,"Unknown error %d", errno);
            return ueb;

case ENOENT               : return "Not found"; /* 2 */
case EBADF                : return "Invalid descriptor"; /* 9 */
/* case EAGAIN               : return "No more ports"; */ /* 11 */
case EFAULT               : return "Bad address"; /* 14 */
case EINVAL               : return "Invalid argument"; /* 22 */
case ENOSPC               : return "No space on device/DNS buffer overflow"; /* 28 */
case EPIPE		  : return "Broken pipe"; /* 32 */
case EWOULDBLOCK          : return "Operation would block"; /* 35 */
case EINPROGRESS          : return "Operation now in progress"; /* 36 */
case EALREADY             : return "Operation already in progress"; /* 37 */
case ENOTSOCK             : return "Socket operation on non-socket"; /* 38 */
case EDESTADDRREQ         : return "Destination address required"; /* 39 */
case EMSGSIZE             : return "Message too long"; /* 40 */
case EPROTOTYPE           : return "Protocol wrong type for socket";
case ENOPROTOOPT          : return "Protocol not available"; /* 42 */
case EPROTONOSUPPORT      : return "Protocol not supported"; /* 43 */
case ESOCKTNOSUPPORT      : return "Socket type not supported"; /* 44 */
case EOPNOTSUPP           : return "Operation not supported on socket"; /* 45 */
case EPFNOSUPPORT         : return "Protocol family not supported";  /* 46 */
case EAFNOSUPPORT         : return "Address family not supported by protocol family"; /* 47 */
case EADDRINUSE           : return "Address already in use"; /* 48 */
case EADDRNOTAVAIL        : return "Can't assign requested address"; /* 49 */
case ENETDOWN             : return "Network is down"; /*/ 50 */
case ENETUNREACH          : return "Network is unreachable"; /*/ 51 */
case ENETRESET            : return "Network dropped connection on reset"; /*/ 52 */
case ECONNABORTED         : return "Software caused connection abort"; /*/ 53 */
case ECONNRESET           : return "Connection reset by peer"; /*/ 54 */
case ENOBUFS              : return "No buffer space available"; /*/ 55 */
case EISCONN              : return "Socket is already connected"; /*/ 56 */
case ENOTCONN             : return "Socket is not connected"; /*/ 57 */
case ESHUTDOWN            : return "Can't send after socket shutdown"; /*/ 58  */
case ETOOMANYREFS         : return "Too many references: can't splice"; /*/ 59  */
case ETIMEDOUT            : return "Connection timed out"; /*/ 60 */
case ECONNREFUSED         : return "Connection refused"; /*/ 61  */
case EHOSTDOWN            : return "Host is down"; /*/ 64 */
case EHOSTUNREACH         : return "No route to host"; /*/ 65 */
case 486		  : return "No internet stack available";
  }
}
