/* qi.c - UIUC CCSO nameserver query interpreter */
/* Bruce Tanner - Cerritos College */

/* Version history: */
/* 1.0  1993/08/15 Initial version */
/* 1.1  1993/08/25 Add field instance attribute, conditionalize options */
/* 1.2  1993/09/08 Soundex is now an indexed explicit field; exact match mode */
/* 1.3  1993/09/15 Added ID to index key to remove duplicate index records */
/* 2.0  1993/09/16 Add login mode (login, answer, clear, logout) */
/* 2.1  1993/09/20 Interactive mode */

#include stdio
#include string
#include ctype
#include ssdef
#include descrip
#include time
#include stdarg
#include rms
#include dvidef
#include dcdef
#include "qi.h"


Fields fields[MAX_FIELD];  /* field attributes, global */
int mode = DEFAULT_MODE;   /* global mode flags */
extern int db_status;      /* status of database */
char login_alias[KEYWORD_SIZE + 1];  /* current login */
char login_challenge[CHALLENGE_SIZE + 1];
int login_mode = MODE_ANONYMOUS;

void db_open();
void db_close();
int read_fields(char *);
int fields_cmd(char *, int);
char *new_string(char *);
char *getlogical(char *);
void qilog(int, char *, ...);
extern int query(char *, int);
extern char *get_value(int, char*, char*, char *);
int quit(char *, int);
Arg *parse_cmd(char *, int);
Arg *make_arg(char *, int, char *, int);
void free_args(Arg *);
void swrite(int, char *, ...);
void writestring(int, char *);
void qiabort(int, char *);
int id_cmd(char *, int);
int stat_cmd(char *, int);
int set_cmd(char *, int);
int site_cmd(char *, int);
int login_cmd(char *, int);
int answer_cmd(char *, int);
int clear_cmd(char *, int);
int logout_cmd(char *, int);
char *challenge(int size);

struct verb_struct {
   char *name;
   int mode; /* requires login, etc */
   int (*proc)(char *, int);
} verbs[] = {{"quit", MODE_ANONYMOUS | MODE_LOGIN, quit},
            {"stop", MODE_ANONYMOUS | MODE_LOGIN, quit},
            {"exit", MODE_ANONYMOUS | MODE_LOGIN, quit},
            {"fields", MODE_ANONYMOUS | MODE_LOGIN, fields_cmd},
            {"query", MODE_ANONYMOUS | MODE_LOGIN, query},
            {"ph", MODE_ANONYMOUS | MODE_LOGIN, query},
            {"status", MODE_ANONYMOUS | MODE_LOGIN, stat_cmd},
            {"id", MODE_ANONYMOUS | MODE_LOGIN, id_cmd},
            {"set", MODE_ANONYMOUS | MODE_LOGIN, set_cmd},
            {"siteinfo", MODE_ANONYMOUS | MODE_LOGIN, site_cmd},
            {"login", MODE_ANONYMOUS, login_cmd},
            {"answer", MODE_PASSWORD, answer_cmd},
            {"clear", MODE_PASSWORD, clear_cmd},
            {"logout", MODE_LOGIN, logout_cmd}
           };

#define MAX_VERBS (sizeof(verbs) / sizeof(struct verb_struct))



/* This program is designed to run as an 'inetd' detached process */

main()
{
   int status, sock, class;
   unsigned short chan;
   $DESCRIPTOR(sysin_dsc, "SYS$INPUT");

   /* open a channel to an INET device */
   status = sys$assign(&sysin_dsc, &chan, 0, 0, 0);
   if ((status & 1) != SS$_NORMAL) {
       qilog(-1, "Open fail status = %d", status);
       exit(status);
   }
   sock = (int) chan;

   status = lib$getdvi(&DVI$_DEVCLASS, &chan, 0, &class, 0, 0);

   /* load fields */
   if (read_fields(getlogical(CONFIG_NAME)) == False)
       exit(4);

   /* initialize randomness */
   srand((int) time((time_t *) NULL));

   db_open();
   while (process(sock, class == DC$_TERM));
   closenet(sock);
   db_close();
}


/* process a command stream */

int process(int sock, int interactive)
{
   int status, ind, length;
   static int bad_cmd = 0;
   char *cp, inputline[MAX_INPUT], *vp, verb[MAX_INPUT];

   strncpy(inputline, "", MAX_INPUT);
   if (interactive) {
       printf("qi> ");
       fgets(inputline, MAX_INPUT, stdin);
       length = strlen(inputline);
   }
   else
       length = readline(sock, inputline, MAX_INPUT); /** Get the line **/
   ZapCRLF(inputline);
   qilog(sock, "Cmd: %s", inputline);

   if (length <= 0) {
       qilog(sock, "Remote end shutdown");
       return False;
   }

   if (strlen(inputline) == 0)  /* ignore blank lines */
       return (++bad_cmd < MAX_BAD);  /* return False if too many null cmds */

   /* put first word of input in verb as lowercase */
   strncpy(verb, "", sizeof(verb));
   vp = verb;
   for (cp = inputline; *cp; cp++)
       if (*cp == ' ')
           break;                  /* break on space */
       else
           *vp++ = _tolower(*cp);  /* copy lower case char to verb */

   for (ind = 0; ind < MAX_VERBS; ind++)
       if (strcmp(verb, verbs[ind].name) == 0)
           break;

   if (ind == MAX_VERBS) {
       qilog(sock, "Unknown command: /%s/%s/", verb, inputline);
       writestring(sock, "514:Unknown command.\r\n");
       return (++bad_cmd < MAX_BAD);  /* return False if too many bad cmds */
   }

   if (((verbs[ind].mode & login_mode) == 0) && (login_mode == MODE_ANONYMOUS)) {
       qilog(sock, "Not logged in: %s", inputline);
       writestring(sock, "506:Request refused; must be logged in to execute.\r\n");
       return (++bad_cmd < MAX_BAD);  /* return False if too many bad cmds */
   }

   if (((verbs[ind].mode & login_mode) == 0) && (login_mode == MODE_PASSWORD)) {
       qilog(sock, "Not answer or clear: %s", inputline);
       writestring(sock, "523:Expecting 'answer' or 'clear'\r\n");
       return (++bad_cmd < MAX_BAD);  /* return False if too many bad cmds */
   }

   if ((ind < MAX_VERBS) && (verbs[ind].mode & login_mode))
           status = (*verbs[ind].proc)(inputline, sock);

   bad_cmd = 0;                  /* reset bad command count */
   return status;
}


int quit(char *cmd, int sock)
{
   writestring(sock, "200:Bye!\r\n");
   return False;
}


char * get_field(char *ptr, char *field, int lower)
{
   int ind;

   for (ind= 0; *ptr != '\0' && *ptr != ':'; ptr++, ind++)
       field[ind] = lower ? _tolower(*ptr) : *ptr;
   field[ind] = '\0';
   if (*ptr == ':') ptr++;  /* skip over terminating ":" */
   return ptr;
}


int read_fields(char *file)
{
   FILE *cnf;
   char *ptr, line[256], field[128];
   int ind, field_idx, count = 0;

   for (ind = 0; ind < MAX_FIELD; ind++) {
       fields[ind].number = NULL;
       fields[ind].name = NULL;
       fields[ind].desc = NULL;
       fields[ind].attrib = 0;
   }

   cnf = fopen(file, "r", "shr=get");
   if (cnf == NULL)
       return (False);

   while (fgets(line, sizeof(line), cnf)) {
       ZapCRLF(line);
       ptr = line;
       if ((*ptr == '#') || (*ptr == '\0'))    /* comment or blank? */
           continue;                           /* yes, skip line */
       count++;
       ptr = get_field(ptr, field, False);     /* field number */
       field_idx = atoi(field);
       fields[field_idx].number = new_string(field);

       ptr = get_field(ptr, field, True);      /* field name */
       fields[field_idx].name = new_string(field);

       ptr = get_field(ptr, field, False);     /* field size (ignore) */

       ptr = get_field(ptr, field, False);     /* field description */
       fields[field_idx].desc = new_string(field);

       ptr = get_field(ptr, field, False);     /* field option (ignore) */

       for (;;) {
           ptr = get_field(ptr, field, True);  /* get attribute */
           if (strlen(field) == 0)
               break;                          /* no more attributes */
           fields[field_idx].attrib |= field_attrib(field);
       }
       if (fields[field_idx].number < 1)
           qilog(-1, "Field \"%s\" has illegal field number",
                 fields[field_idx].name);
   }

   fclose(cnf);
   return True;
}


int write_afield(int field_num, int sock)
{
   char line[128];
   int aidx;

   if (fields[field_num].name == NULL)
       return False;
   sprintf(line, "-200:%d:%s:max %d",
           field_num, fields[field_num].name, DATA_SIZE);
   for (aidx = 0; aidx < MAX_ATTRIBUTES; aidx++)
       if (fields[field_num].attrib & attributes[aidx].value) {
           strcat(line, " ");
           strcat(line, attributes[aidx].name);
       }
   strcat(line, "\r\n");
   writestring(sock, line);
   swrite(sock, "-200:%d:%s:%s\r\n",
           field_num, fields[field_num].name, fields[field_num].desc);
   return True;
}


int fields_cmd(char *cmd, int sock)
{
   int fidx, aidx, count = 0;
   char line[256];
   Arg *list, *listp;

   list = listp = parse_cmd(cmd, sock);

   if (list == NULL)   /* null arg list means all fields */
       for (fidx = 0; fidx < MAX_FIELD; fidx++)
           count += write_afield(fidx, sock);
   else
       for (; listp; listp = listp->next)
           if (listp->field > -1)
               count += write_afield(listp->field, sock);
           else
               writestring(sock, "507:Field does not exist.\r\n");

   free_args(list);

   writestring(sock, "200:Ok.\r\n");
   if (DEBUG) qilog(sock, "Sent %d field definitions", count);
   return True;
}


id_cmd(char *cmd, int sock)
{
   writestring(sock, "200:Thanks, but we don't use ids here.\r\n");
   return True;
}


stat_cmd(char *cmd, int sock)
{
   if ((db_status & 1) == SS$_NORMAL)
       writestring(sock, "200:Database ready.\r\n");
   else
       writestring(sock, "475:Database unavailable; try later.\r\n");
   return True;
}


/* set global mode flags on/off */
int set_cmd(char *cmd, int sock)
{
   Arg *list, *listp;
   int index;

   list = listp = parse_cmd(cmd, sock);
   for (; listp; listp = listp->next) {
       for (index = 0; index < MAX_MODES; index++)
           if (listp->name && strcmp(listp->name, modes[index].name) == 0) {
               switch (listp->type & TYPE_MASK) {
               case TYPE_ON:
                   mode |= modes[index].value;
                   writestring(sock, "200:Done.\r\n");
                   break;
               case TYPE_OFF:
                   mode &= ~modes[index].value;
                   writestring(sock, "200:Done.\r\n");
                   break;
               default:
                   writestring(sock, "513:Option must be ON or OFF.\r\n");
                   break;
               }
               break;
           }
       if (index == MAX_MODES)
           swrite(sock, "513:Unknown mode %s\r\n",
                  listp && listp->name ? listp->name : "");
   }
   free_args(list);
   return True;
}


/* return some arbitrary site info */
int site_cmd(char *cmd, int sock)
{
   FILE *fd;
   char line[128];

   swrite(sock, "-200:0:version:%s\r\n", VERSION);
   if ((fd = fopen(getlogical(SITEINFO_NAME), "r", "shr=get")) == NULL)
       swrite(sock, "525:No siteinfo available.\r\n");
   else {
       while (fgets(line, sizeof(line), fd)) {
           ZapCRLF(line);
           swrite(sock, "-200:%s\r\n", line);
       }
       swrite(sock, "200:Ok.\r\n");
       fclose(fd);
   }
   return True;
}


/* set current alias */
int login_cmd(char *cmd, int sock)
{
   Arg *list;
   char *ap;

   list = parse_cmd(cmd, sock);
   if ((list == NULL) || (list->next) || (list->type != TYPE_VALUE)) {
       free_args(list);
       writestring(sock, "599:Syntax error\r\n");
       return True;
   }
   if ((ap = get_value(sock, list->value, ALIAS_FIELD, ALIAS_FIELD)) == NULL) {
       free_args(list);
       swrite(sock, "500:Alias does not exist\r\n");
       return True;
   }
   free_args(list);
   strcpy(login_alias, ap);
   if (get_value(sock, login_alias, ALIAS_FIELD, PASSWORD_FIELD) == NULL) {
       swrite(sock, "500:Password does not exist\r\n");
       return True;
   }
   strcpy(login_challenge, challenge(CHALLENGE_SIZE));
   swrite(sock, "301:%s\r\n", login_challenge);
   login_mode = MODE_PASSWORD;
   return True;
}


int clear_cmd(char *cmd, int sock)
{
   char *cp, *pw;

   cp = strchr(cmd, ' ');             /* skip verb */
   for (;cp && *cp && (*cp == ' '); cp++);  /* skip spaces after verb */
   if (cp == NULL) {
       writestring(sock, "599:Syntax error\r\n");
       login_mode = MODE_ANONYMOUS;
       return True;
   }
   if ((pw = get_value(sock, login_alias, ALIAS_FIELD, PASSWORD_FIELD)) == NULL) {
       swrite(sock, "500:Password does not exist\r\n");
       login_mode = MODE_ANONYMOUS;
       return True;
   }
   if (strcmp(cp, pw) == 0) {
       swrite(sock, "200:%s:Password accepted\r\n", login_alias);
       login_mode = MODE_LOGIN;
   }
   else {
       swrite(sock, "500:Login failed\r\n", login_alias);
       login_mode = MODE_ANONYMOUS;
   }
   return True;
}


int answer_cmd(char *cmd, int sock)
{
   char *cp, decrypted[128];

   cp = strchr(cmd, ' ');             /* skip verb */
   for (;cp && *cp && (*cp == ' '); cp++);  /* skip spaces after verb */
   if (cp == NULL) {
       writestring(sock, "599:Syntax error\r\n");
       login_mode = MODE_ANONYMOUS;
       return True;
   }
   crypt_start(get_value(sock, login_alias, ALIAS_FIELD, PASSWORD_FIELD));
   decrypt(decrypted, cp);
   if ((cp = strchr(decrypted, '\r')) || (cp = strchr(decrypted, '\n')))
       *cp = '\0';  /* truncate at cr or lf */
   if (DEBUG)
       swrite(sock, "%s decrypted into\r\n%s compared with\r\n%s\r\n",
              cp, decrypted, login_challenge);
   if (strcmp(decrypted, login_challenge) == 0) {
       swrite(sock, "200:%s:Password accepted\r\n", login_alias);
       login_mode = MODE_LOGIN;
   }
   else {
       swrite(sock, "500:Login failed\r\n");
       login_mode = MODE_ANONYMOUS;
   }
   return True;
}


int logout_cmd(char *cmd, int sock)
{
   strncpy(login_alias, "", sizeof(login_alias));
   login_mode = MODE_ANONYMOUS;
   swrite(sock, "200:Done.\r\n");
   return True;
}


char *challenge(int size)
{
   char *ptr, *base;

   base = calloc(size + 1, sizeof(char));
   for (ptr = base; size; ptr++, size--)
       *ptr = (rand() & 0x3f) + 0x21;
   return (base);
}


/* return the attribute value for the given field name */
int field_attrib(char *str)
{
   int ind;

   for (ind = 0; ind < MAX_ATTRIBUTES; ind++)
       if (*str == _tolower(*attributes[ind].name)) /* check only first char */
           return (attributes[ind].value);
   return (0);  /* no match = no bits */
}


/* return the field_number for the given field name */
int field_number(char *str)
{
   int ind;

   for (ind = 0; ind < MAX_FIELD; ind++)
       if (fields[ind].name && (strcmp(str, fields[ind].name) == 0))
           return (atoi(fields[ind].number));
   return (-1);  /* no field number */
}


/* get a token as part of the 'field=value' clause */
/* return pointer to terminator */
char *get_token(char *cp, char *dp)
{
   int in_quote = False;

   if (*cp) {
       while (isspace(*cp)) cp++; /* skip space */
       while (*cp && (in_quote || ((*cp != ' ') && (*cp != '='))))
           if (*cp == '"') {
               in_quote = !in_quote;
               cp++;
           }
           else
               *dp++ = in_quote ? *cp++ : tolower(*cp++);
   }
   *dp = '\0';
   return cp;
}


/* cmd = 'verb [field=]value ...' */
Arg *parse_cmd(char *cmd, int sock)
{
   int index;
   char *cp, token[128];
   Arg *start = NULL, *end = NULL;

   cp = strchr(cmd, ' ');  /* skip verb */
   while (cp) {
       cp = get_token(++cp, token);
       if (strlen(token) == 0)
           return start;
       if (start == NULL)
           start = end = make_arg(NULL, -1, NULL, 0);
       else {
           end->next = make_arg(NULL, -1, NULL, 0);
           end = end->next;
       }
       if (*cp == '=') {
           end->name = new_string(token);
           end->field = field_number(token);
           end->type |= TYPE_NAME | TYPE_EQUAL;
           cp = get_token(++cp, token);
       }
       if (strlen(token)) {
           end->value = new_string(token);
           end->type |= TYPE_VALUE;
       }
       if (strcmp(token, "return") == 0)  /* check for special names */
           end->type |= TYPE_RETURN;
       else if (strcmp(token, "on") == 0)
           end->type |= TYPE_ON;
       else if (strcmp(token, "off") == 0)
           end->type |= TYPE_OFF;
       if (end->field == -1)       /* if there were no field name given */
           end->field = field_number(token);  /* try the field value as a field name */
       if (DEBUG)
           swrite(sock, "-100: Parse >> %s (field %d) = %s\r\n",
                   end->name ? end->name : "", end->field,
                   end->value ? end->value : "");
   }
   return start;  /* should only get here on null list */
}


Arg *make_arg(char * name, int field, char *value, int type)
{
   Arg *ptr;

   ptr = malloc(sizeof (Arg));
   ptr->type = type;
   ptr->name = name;
   ptr->field = field;
   ptr->value = value;
   ptr->next = (Arg *) 0;
   return ptr;
}



void free_args(Arg *ptr)
{
   Arg *next;

   while (ptr) {
       next = ptr->next;
       free(ptr);
       ptr = next;
   }
}


/* copy string into malloc'ed space */

char *new_string(char *str)
{
   char *ptr;

   ptr = (char *) malloc(strlen(str) + 1);
   strcpy(ptr, str);
   return (ptr);
}


void swrite(int sock, char *fmt, ...)
{
   char    buf[512];
   va_list arg_ptr;

   va_start(arg_ptr, fmt);
   vsprintf(buf, fmt, arg_ptr);
   va_end(arg_ptr);

   writestring(sock, buf);
}


void qilog(int sock, char *fmt, ...)
{
   FILE    *logfd;
   char    host_name[256];
   time_t  Now;
   char    NowBuf[26];
   char    *cp;
   char    buf[512];
   va_list arg_ptr;

   va_start(arg_ptr, fmt);
   vsprintf(buf, fmt, arg_ptr);
   va_end(arg_ptr);
   host_name[0] = '\0';
   if (sock > -1)
       inet_netnames(sock, host_name);
   time(&Now);
   cp = (char *) ctime(&Now);
   ZapCRLF(cp);
   cp = strcpy(NowBuf, cp);
   if ((logfd = fopen(getlogical(LOG_NAME), "a",
                      "dna=qi.log", "shr=put")) != NULL) {
       if (strlen(fmt)) {
           fprintf(logfd, "%s %s : %s\n", cp, host_name, buf);
           fflush(logfd);
       }
       fclose(logfd);
   }
}


void qiabort(int sock, char *str)
{
   writestring(sock, str);
   ZapCRLF(str);
   qilog(sock, "Abort: %s", str);
   exit(2);
}


void *my_realloc(void *mem, int size)
{
   if ((mem == (void *) 0))
       return ((void *) malloc(size));
   else
       return ((void *) realloc(mem, size));
}


/* translate an exec mode logical name */
char *getlogical(char *name)
{
#include psldef
#include lnmdef

   typedef struct {
       short length;
       short item_code;
       char  *bufadr;
       short *ret_len_addr;
   } item_desc;

   struct {
       item_desc string;
       int terminator;
   } trnlst;

   static char result[128];  /* the object returned */
   short ret_len = 0;
   int acmode = PSL$C_EXEC;
   $DESCRIPTOR(table_dsc, "LNM$SYSTEM_TABLE");
   struct dsc$descriptor_s log_dsc = { strlen(name), DSC$K_DTYPE_T,
                                       DSC$K_CLASS_S, name };

   trnlst.string.bufadr = result;
   trnlst.string.length = sizeof(result);
   trnlst.string.item_code = LNM$_STRING;
   trnlst.string.ret_len_addr = &ret_len;
   trnlst.terminator = 0;

   sys$trnlnm(0, &table_dsc, &log_dsc, &acmode, &trnlst);
   result[ret_len] = '\0';
   return result;
}