/*
** Copyright (c) 1998 D. Richard Hipp
**
** 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.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
** General Public License for more details.
** 
** You should have received a copy of the GNU General Public
** License along with this program; if not, write to the
** Free Software Foundation, Inc., 59 Temple Place - Suite 330,
** Boston, MA  02111-1307, USA.
**
** Author contact information:
**   drh@acm.org
**   http://www.hwaci.com/drh/
*/
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>

/*
** Names of files and executables and other hard-coded
** strings.
*/
#define DB_DIR     "/var/eznet"
#define DB_FILE    DB_DIR "/eznet.conf"
#define PPP_DIR    "/etc/ppp"
#define PPP_EXE    "/usr/sbin/pppd"
#define SELF       "/usr/bin/eznet"
#define DIALD_EXE  "/usr/sbin/diald"
#define SESSIONLOG DB_DIR "/session.html"

/*
** The following define determines what version of PPP
** this program expects by default.  You can override this
** value using the pppversion= parameter.
*/
#define DFLT_PPP_VERSION "2.3"

/*
** Each entry in the database is represented in memory by an instance
** of the following structure.
*/
typedef struct ConfEntry ConfEntry;
struct ConfEntry {
  char *zLabel;       /* A label for this entry */
  ConfEntry *next;    /* Next entry in the same record */
  char *zValue;       /* Value for this entry */
};

/*
** These variables record the whole database.  There are nRecord
** records, and each record consists of a linked list of ConfEntry
** structures where aRecord[N] points to the head of the list for
** record number N.
*/
static int nRecord = 0;           /* Number of records */
static ConfEntry **aRecord = 0;   /* List of ConfEntries for each record */

/*
** A transcript of the CHAT session is written to this file, if
** it is open.
*/
static FILE *transcript = 0;

/*
** If this is a set-uid or set-gid process, then release all
** privileges.
*/
static void Unprivileged(void){
  int uid = getuid();
  int gid = getgid();
  setregid(gid,gid);
  setreuid(uid,uid);
}

/*
** Read the entire contents of a file into memory.  Space to
** hold the file is obtained from malloc.  NULL is returned if
** we can't read the file for any reason.
*/
static char *ReadFile(const char *zFilename){
  int fd;
  FILE *in;
  int toread;
  int read;
  int n;
  char *zBuf;
  struct stat sbuf;
 
  in = fopen(zFilename,"r");
  if( in==0 ) return 0;
  fd = fileno(in);
  if( fstat(fd, &sbuf)!=0 ){
    fclose(in);
    return 0;
  }
  toread = sbuf.st_size;
  read = 0;
  zBuf = malloc( toread+1 );
  if( zBuf==0 ){
    fclose(in);
    return 0;
  }
  while( toread && (n=fread(&zBuf[read],1,toread,in))>0 ){
    toread -= n;
    read += n;
  }
  zBuf[read] = 0;
  fclose(in);
  return zBuf;
}

/* Change the status for a record.  The status is recorded in
** a file named
**
**       /var/eznet/status.NNN
**
** where NNN is the record number.
*/
static void WriteStatus(int iRec, char *zStatus, ...){
  FILE *out;
  va_list ap;
  char zFilename[sizeof(DB_DIR)+20];

  sprintf(zFilename,"%s/status.%d",DB_DIR,iRec);
  out = fopen(zFilename,"w");
  if( out ){
    time_t now;
    struct tm *p;
    char zBuf[200];
    fchown(fileno(out),0,0);
    fchmod(fileno(out),0644);
    va_start(ap, zStatus);
    vfprintf(out,zStatus,ap);
    va_end(ap);
    time(&now);
    p = localtime(&now);
    strftime(zBuf,sizeof(zBuf)-1," %I:%M%p %a %b %d, %Y\n", p);
    fprintf(out,zBuf);
    fclose(out);
  }
}

/* Write a status report from Chat.  This is written to a file
** named
**
**     /var/eznet/chatstat.NNN
**
** where NNN is the record number.
*/
static void WriteChatStat(int iRec, char *zStatus, ...){
  FILE *out;
  va_list ap;
  char zFilename[sizeof(DB_DIR)+20];

  sprintf(zFilename,"%s/chatstat.%d",DB_DIR,iRec);
  out = fopen(zFilename,"w");
  if( out ){
    va_start(ap, zStatus);
    vfprintf(out,zStatus,ap);
    va_end(ap);
    fclose(out);
  }
}

/*
** Write text into a file.  zFile is the name of the file into
** which the text is written.  zFormat is a printf-like format
** string that defines the text.  The owner of the file is converted
** to root and the permissions are set to "mode".
*/
static void WriteToFile(char *zFile, int mode, char *zFormat, ...){
  FILE *out;
  va_list ap;

  out = fopen(zFile,"w");
  if( out==0 ) return;
  fchmod(fileno(out),mode);
  fchown(fileno(out),0,0);
  va_start(ap,zFormat);
  vfprintf(out,zFormat,ap);
  va_end(ap);
  fclose(out);
}

/* Make sure /etc/ppp/ip-up and /etc/ppp/ip-down invoke eznet
** with either the "ipup" or "ipdown" argument (as approprite.)
** This is needed so that we can adjust the status when the link
** goes up or down.
*/
static void SetIpUpDown(char *zUpDown){
  char *zText;
  int i;
  int lineLen = 0;
  int patLen;
  char zPat[100];
  char zFile[sizeof(PPP_DIR)+20];

  sprintf(zFile,"%s/ip-%s",PPP_DIR,zUpDown);
  zText = ReadFile(zFile);
  if( zText==0 ){
    WriteToFile(zFile, 0744, 
      "#!/bin/sh\n" SELF " ip%s $* &\n", zUpDown);
    return;
  }
  for(i=0; zText[i] && zText[i]!='\n'; i++){}
  if( zText[i]=='\n' ){
    lineLen = i;
  }
  sprintf(zPat,SELF " ip%s $* &",zUpDown);
  patLen = strlen(zPat);
  while( zText[i] && (zText[i]!='/' || strncmp(&zText[i], zPat, patLen)) ){
    i++;
  }
  if( zText[i]==0 ){
    WriteToFile(zFile, 0744, "#!/bin/sh\n%s\n%s",
      zPat, &zText[lineLen]);
  }
}

/*
** Parse a single line of the pap-secrets or chap-secrets file whose
** text begins as *pzText.  The elements of the line are place in
** azArgs[0] through azArgs[3].
**
** After parsing, *pzText is left pointing to the first character
** after then end of the parsed text.  At end of file, *pzText points
** to 0.
**
** All of azArgs[0] through azArgs[3] are always filled in.  Empty
** strings are inserted if necessary.  A comment line is placed in
** azArgs[0] and the others are empty strings.
**
** Quotes and escapes are removed from azArgs[0] through azArgs[2],
** except for comment lines which are passed through unaltered.
*/
static void GetSecret(char **pzText, char **azArgs){
  int i;
  char *z = *pzText;
  while( isspace(*z) ) z++;
  azArgs[0] = azArgs[1] = azArgs[2] = azArgs[3] = "";
  if( *z=='#' ){
     azArgs[0] = z;
     while( *z && *z!='\n' ) z++;
     if( *z=='\n' ){
       *z = 0;
       z++;
     }
     *pzText = z;
     return;
  }
  for(i=0; i<=2; i++){
    int hasEscape = 0;
    azArgs[i] = z;
    if( *z=='"' ){
      azArgs[i]++;
      z++;
      while( *z && *z!='"' ){ 
        if( *z=='\\' && z[1] ){ z++; hasEscape = 1; }
        z++;
      }
      if( *z=='"' ){
        *z = 0;
        z++;
      }
    }else{
      while( *z && !isspace(*z) ){
        if( *z=='\\' && z[1] ){ z++; hasEscape = 1; }
        z++;
      }
      if( *z ){
        *z = 0;
        z++;
      }
    }
    while( isspace(*z) && *z!='\n' ){ z++; }
    if( hasEscape ){
      int j = 0, k = 0;
      char *zStr = azArgs[i];
      while( zStr[j] ){
        if( zStr[j]=='\\' && zStr[j+1] ){ j++; }
        zStr[k++] = zStr[j++];
      }
    }
  }
  azArgs[i] = z;
  while( *z && *z!='\n' ){
    if( *z=='\\' && z[1] ){ z++; }
    z++;
  }
  if( *z=='\n' ){
    *z = 0;
    z++;
  }
  *pzText = z;
}

/*
** The elements azArgs[0] through azArgs[3] contain the text of
** a secret.  This secret is written to *pzBuf.  *pzBuf is updated
** to point to the null terminator at the end of the written text.
**
** Quotes and escapes are inserted on azArgs[0] through azArgs[2]
** if needed.  Except if the first character of azArgs[0] is "#"
** then no quotes or escapes are inserted.
*/
static void AppendSecret(char **pzBuf, char **azArgs){
  char *z = *pzBuf;
  char *zSrc;
  int i;
  if( azArgs[0][0]=='#' ){
    zSrc = azArgs[0];
    while( *zSrc ){ *(z++) = *(zSrc++); }
    *(z++) = '\n';
    *z = 0;
    *pzBuf = z;
    return;
  }
  for(i=0; i<=2; i++){
    zSrc = azArgs[i];
    if( *zSrc==0 ) continue;
    while( *zSrc ){
      switch( *zSrc ){
        case ' ':
        case '\t':
        case '\n':
        case '"':
        case '#':
        case '\'':
        case '\\':
          *(z++) = '\\';
          /* Fall thru */
        default:
          *(z++) = *(zSrc++);
          break;
      }
    }
    *(z++) = ' ';
  }
  while( *zSrc ){ *(z++) = *(zSrc++); }
  *(z++) = '\n';
  *z = 0;
  *pzBuf = z;
}

/* Make sure the correct password for the service and user is found in the
** secrets file.  Add it if is not.  Preserve any commands or
** unrelated secrets that are previously found in the file.
**
** Needless to say, we have to have root privileges to run
** this function successfully.  The secrets files contain information
** which should not be publicly viewable.
*/
static void SetSecrets(
  char *zBase,         /* Either "pap-secrets" or "chap-secrets" */
  char *zService,      /* Name of the service provider we are calling */
  char *zUser,         /* Our login name */
  char *zPassword      /* Our password */
){
  char *zOld, *zOldOrig;
  char *z;
  char *zNew;
  int extra;
  int seenServiceUser = 0;
  int seenUserService = 0;
  char *azArgs[4];
  char zFile[sizeof(PPP_DIR)+20];

  if( zPassword==0 || zUser==0 || zService==0 || zFile==0 ) return;
  sprintf(zFile,"%s/%.15s",PPP_DIR,zBase);
  zOldOrig = zOld = ReadFile(zFile);
  extra = (strlen(zService) + strlen(zPassword) + strlen(zUser) + 10)*4;
  if( zOld==0 ){
    WriteToFile(zFile, 0600, "#\n%s %s %s\n%s %s %s\n",
       zService, zUser, zPassword,
       zUser, zService, zPassword
    );
    return;
  }
  z = zNew = malloc( strlen(zOld) + extra + 100 );
  if( z==0 ) return;
  while( zOld && *zOld ){
    GetSecret(&zOld, azArgs);
    if( azArgs[0][0]!='#' ){
      if( strcmp(azArgs[0],zService)==0 && strcmp(azArgs[1],zUser)==0 ){
        seenServiceUser = 1;
        azArgs[2] = zPassword;
        azArgs[3] = "";
      }else if( strcmp(azArgs[0],zUser)==0 && strcmp(azArgs[1],zService)==0 ){
        seenUserService = 1;
        azArgs[2] = zPassword;
        azArgs[3] = "";
      }
    }
    AppendSecret(&z, azArgs);
  }
  if( !seenServiceUser ){
    azArgs[0] = zService;
    azArgs[1] = zUser;
    azArgs[2] = zPassword;
    azArgs[3] = "";
    AppendSecret(&z, azArgs);
  }
  if( !seenUserService ){
    azArgs[0] = zUser;
    azArgs[1] = zService;
    azArgs[2] = zPassword;
    azArgs[3] = "";
    AppendSecret(&z, azArgs);
  }
  WriteToFile(zFile, 0600, "%s", zNew);
  free(zOldOrig);
  free(zNew);
}

/* zFile[*pI] is the first character in a line of text.  Return
** a pointer to the first non-whitespace character in this line.
** Also convert the \n into a \0 and make *pI be the index of
** the first character on the next line.
**
** If zFile[*pI] is 0, then we've reached the end of input.
** Return NULL.
*/
static char *GetLine(char *zFile, int *pI){
  int i = *pI;
  int iStart;
  if( zFile[i]==0 ) return 0;
  while( isspace(zFile[i]) ){ i++; }
  iStart = i;
  while( zFile[i] && zFile[i]!='\n' ){ i++; }
  if( zFile[i] ){
    zFile[i] = 0;
    *pI = i+1;
  }else{
    *pI = i;
  }
  return &zFile[iStart];
}

/* This array maps hexadecimal digit values into their numeric
** value.
*/
static int hexval[] = {
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0,
  0,10,11,12,13,14,15, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0,10,11,12,13,14,15, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};

/* Return a pointer to the first space-separated token past
** the character zLine[*pI].  Return NULL if there are no more
** tokens.  *pI is left pointing to the first character after
** the token.
**
** The HTTP-style encoding is removed from the token and the
** token is null-terminated.
*/
static char *GetToken(char *zLine, int *pI){
  int i = *pI;
  int iStart;
  int j;
  while( isspace(zLine[i]) ){ i++; }
  if( zLine[i]==0 ) return 0;
  iStart = i;
  while( zLine[i] && !isspace(zLine[i]) ){ i++; }
  if( zLine[i] ){
    *pI = i + 1;
    zLine[i] = 0;
  }else{
    *pI = i;
  }
  for(i=j=iStart; zLine[i]; i++){
    switch( zLine[i] ){
      case '+':
       zLine[j++] = ' ';
       break;
      case '%':
       if( zLine[i+1] && zLine[i+2] ){
         zLine[j++] = hexval[0x7f & zLine[i+1]]*16 + hexval[0x7f & zLine[i+2]];
         i += 2;
       }else{
         zLine[j++] = '%';
       }
       break;
      default:
       zLine[j++] = zLine[i];
       break;
    }
  }
  zLine[j] = 0;
  return &zLine[iStart];
}

/*
** This function inserts a new entry into the database (or replaces
** the value of an existing entry).  iRec is the record number.
** New records are created as necessary.  zField is the entry label,
** and zValue is its value.  Both are plain text.
**
** If zValue==0 or *zValue==0, then the entry is deleted.
**
** Memory to hold the new entry is obtained from malloc.
*/
static void InsertEntry(int iRec, char *zLabel, char *zValue){
  ConfEntry *p;
  if( zValue==0 || *zValue==0 ){
    if( iRec>=0 && iRec<nRecord ){
      ConfEntry **pp = &aRecord[iRec];
      while( (p=*pp)!=0 ){
        if( strcmp(p->zLabel,zLabel)==0 ) break;
        pp = &p->next;
      }
      if( p ){
        *pp = p->next;
        free( p->zValue );
        free( p );
      }
    }
    return;
  }
  if( iRec>=nRecord ){
    if( nRecord==0 ){
      aRecord = malloc( sizeof(ConfEntry*)*(iRec+1) );
    }else{
      aRecord = realloc( aRecord, sizeof(ConfEntry*)*(iRec+1) );
    }
    if( aRecord==0 ){ nRecord = 0; return; }
    while( nRecord<=iRec ){
      aRecord[nRecord++] = 0;
    }
  }
  for(p=aRecord[iRec]; p; p=p->next){
    if( strcmp(p->zLabel,zLabel)==0 ) break;
  }
  if( p==0 ){
    p = malloc( sizeof(ConfEntry) + strlen(zLabel) + 1 );
    if( p==0 ) return;
    p->zLabel = (char*)&p[1];
    strcpy(p->zLabel, zLabel);
    p->next = aRecord[iRec];
    p->zValue = 0;
    aRecord[iRec] = p;
  }
  if( p ){
    if( p->zValue ) free(p->zValue);
    p->zValue = malloc( strlen(zValue) + 1 );
    if( p->zValue ) strcpy(p->zValue, zValue);
  }
}

/*
** The set of characters that need to be excaped are coded as "1".
** Safe characters are 0.
*/
static unsigned char NeedEsc[] = {
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
};

/* Do an HTTP-style encoding of zToken and write it on the
** output file given.
*/
static void WriteToken(FILE *out, char *zToken){
  int i = 0;
  while( zToken[i] ){
    if( !NeedEsc[(unsigned)zToken[i]&0xff] ){
      i++;
    }else{
      if( i>0 ){
        fwrite(zToken,1,i,out);
      }
      if( zToken[i]==' ' ){
        fwrite("+",1,1,out);
      }else{
        char zBuf[4];
        zBuf[0] = '%';
        zBuf[1] = "0123456789ABCDEF"[(zToken[i]>>4)&0xf];
        zBuf[2] = "0123456789ABCDEF"[zToken[i]&0xf];
        zBuf[3] = 0;
        fwrite(zBuf,1,3,out);
      }
      zToken += i + 1;
      i = 0;
    }
  }
  if( i>0 ){
    fwrite(zToken,1,i,out);
  }
}

/*
** The set of characters that need to be excaped like "\012" are
** are coded as 1.  Safe characters are 0.  Characters that need
** a backslash in front are coded with 2.  Another code generates
** a symbolic escape by putting a backslash in front of the value.
*/
static unsigned char NeedTclEsc[] = {
  1,  1,  1,  1,  1,  1,  1, 'a',  'b','t','n', 1, 'f','r', 1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  2,  0,  2,  1,  2,  0,  0,  0,    0,  0,  0,  0,  0,  0,  0,  0,
  0,  0,  0,  0,  0,  0,  0,  0,    0,  0,  0,  0,  0,  0,  0,  0,    
  0,  0,  0,  0,  0,  0,  0,  0,    0,  0,  0,  0,  0,  0,  0,  0,    
  0,  0,  0,  0,  0,  0,  0,  0,    0,  0,  0,  2,  2,  2,  0,  0,    
  0,  0,  0,  0,  0,  0,  0,  0,    0,  0,  0,  0,  0,  0,  0,  0,    
  0,  0,  0,  0,  0,  0,  0,  0,    0,  0,  0,  1,  0,  1,  0,  1,    
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
};

/* Write a string as a single TCL token.
*/
static void WriteTcl(FILE *out, char *z){
  register c;
  while( (c=*(z++))!=0 ){
    if( NeedTclEsc[c] ){
      int e = NeedTclEsc[c];
      if( e==1 ){
        fprintf(out,"\\%03o",0xff&c);
      }else if( e==2 ){
        fprintf(out,"\\%c",c);
      }else{
        fprintf(out,"\\%c",e);
      }
    }else{
      putc(c,out);
    }
  }
}

/*
** The set of characters that need to be excaped like "\012" are
** are coded as 1.  Safe characters are 0.  Characters that need
** a backslash in front are coded with 2.  Another code generates
** a symbolic escape by putting a backslash in front of the value.
*/
static unsigned char NeedHumanEsc[] = {
  1,  1,  1,  1,  1,  1,  1, 'a',  'b','t','n', 1, 'f','r', 1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  0,  0,  2,  0,  2,  0,  0,  0,    0,  0,  0,  0,  0,  0,  0,  0,
  0,  0,  0,  0,  0,  0,  0,  0,    0,  0,  0,  0,  0,  0,  0,  0,    
  0,  0,  0,  0,  0,  0,  0,  0,    0,  0,  0,  0,  0,  0,  0,  0,    
  0,  0,  0,  0,  0,  0,  0,  0,    0,  0,  0,  0,  2,  0,  0,  0,    
  0,  0,  0,  0,  0,  0,  0,  0,    0,  0,  0,  0,  0,  0,  0,  0,    
  0,  0,  0,  0,  0,  0,  0,  0,    0,  0,  0,  0,  0,  0,  0,  1,    
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
  1,  1,  1,  1,  1,  1,  1,  1,    1,  1,  1,  1,  1,  1,  1,  1,
};

/* Write a string as a single TCL token.
*/
static void WriteHumanReadable(FILE *out, char *z){
  register c;
  while( (c=*(z++))!=0 ){
    if( NeedHumanEsc[c] ){
      int e = NeedHumanEsc[c];
      if( e==1 ){
        fprintf(out,"\\%03o",0xff&c);
      }else if( e==2 ){
        fprintf(out,"\\%c",c);
      }else{
        fprintf(out,"\\%c",e);
      }
    }else{
      putc(c,out);
    }
  }
}

/*
** Convert a non-negative number from its ASCII form into a binary
** form.  Return -1 if the input string is not a non-negative number.
*/
static int GetInt(char *z){
  int v = 0;
  while( *z ){
    if( *z<'0' || *z>'9' ) return -1;
    v = v*10 + *(z++) - '0';
  }
  return v;
}

/*
** Read the whole database into memory.
*/
static void ReadDb(void){
  char *zDb;
  int i,j;
  char *zLine;
  char *zToken;
  int iRec;
  char *zField;
  char *zValue;

  zDb = ReadFile(DB_FILE);
  if( zDb==0 ) return;
  i = 0;
  while( (zLine=GetLine(zDb,&i))!=0 ){
    if( *zLine=='#' ) continue;
    j = 0;
    zToken = GetToken(zLine,&j);
    if( zToken==0 ) continue;
    iRec = GetInt(zToken);
    if( iRec<0 ) continue;
    zField = GetToken(zLine,&j);
    zValue = GetToken(zLine,&j);
    if( zValue==0 ) continue;
    InsertEntry(iRec,zField,zValue);
  }
  free( zDb );
}

/* Given two lists of ConfEntry structures both of which are
** sorted and either or both of which can be empty, combine the
** two lists into a single sorted list.  Return a pointer to 
** the head of the new list.
**
** This is part of the merge-sort algorithm.
*/
static ConfEntry *merge(ConfEntry *left, ConfEntry *right){
  ConfEntry *head = 0, *tail = 0;
  while( left && right ){
    int c = strcmp(left->zLabel, right->zLabel);
    if( c<0 ){
      if( tail ){
        tail->next = left;
        tail = tail->next;
      }else{
        head = tail = left;
      }
      left = left->next;
      tail->next = 0;
    }else{
      if( tail ){
        tail->next = right;
        tail = tail->next;
      }else{
        head = tail = right;
      }
      right = right->next;
      tail->next = 0;
    }
  }
  if( left ){
    if( tail ){
      tail->next = left;
    }else{
      head = tail = left;
    }
  }else if( right ){
    if( tail ){
      tail->next = right;
    }else{
      head = tail = right;
    }
  }
  return head;
}

/* Sort a list of ConfEntry structures.  Return a pointer to
** the sorted list.
**
** The algorithm is merge-sort.  
*/
static ConfEntry *sort(ConfEntry *p){
  int i;
  ConfEntry *pNext;
  ConfEntry *a[32];

  for(i=0; i<sizeof(a)/sizeof(a[0]); i++){ a[i] = 0; }
  while( p ){
    pNext = p->next;
    p->next = 0;
    for(i=0; a[i] && i<sizeof(a)/sizeof(a[0])-1; i++){
      p = merge(a[i],p);
      a[i] = 0;
    }
    a[i] = p;
    p = pNext;
  }
  for(i=0; i<sizeof(a)/sizeof(a[0]); i++){
    p = merge(p,a[i]);
  }
  return p;
}

/*
** Write the entire database back to the disk.  Make sure the
** database file is owned by root and has 0600 permissions so
** that unprivileged users can't see the passwords.
*/
static void WriteDb(void){
  FILE *out;
  int i;
  ConfEntry *p;
  int fd;
  char zFilename[sizeof(DB_FILE)+100];

  sprintf(zFilename,"%s.%d",DB_FILE,getpid());
  out = fopen(zFilename,"w");
  if( out==0 ) return;
  fd = fileno(out);
  fchmod(fd,0600);
  fchown(fd,0,0);
  for(i=0; i<nRecord; i++){
    aRecord[i] = p = sort(aRecord[i]);
    for(p=aRecord[i]; p; p=p->next){
      fprintf(out,"%d %s ",i,p->zLabel);
      WriteToken(out,p->zValue);
      fprintf(out,"\n");
    }
  } 
  fclose(out);
  unlink(DB_FILE);
  link(zFilename,DB_FILE);
  unlink(zFilename);
}

/*
** Given a sequence of strings of the form
**
**              LABEL=VALUE
**
** parse the strings up into LABEL and VALUE and add entries
** for each string to the database as record iRec.
*/
static void AddEntries(int iRec, int argc, char **argv){
  int i, j;
  char *zLabel;
  char *zValue;
  for(i=0; i<argc; i++){
    zLabel = argv[i];
    for(j=1; zLabel[j] && zLabel[j]!='='; j++){}
    if( zLabel[j]!='=' ) continue;
    zLabel[j] = 0;
    zValue = &zLabel[j+1];
    InsertEntry(iRec, zLabel, zValue);
  }
}

/*
** Rearrange the order of records so that the Mth record
** becomes the Nth record.
*/
static void MoveRecord(int M, int N){
  int i;
  ConfEntry *pTemp;

  if( nRecord<0 ) return;
  if( M>=nRecord ) M = nRecord-1;
  if( M<0 ) M = 0;
  if( N>=nRecord ) N = nRecord-1;
  if( N<0 ) N = 0;
  if( M==N ) return;
  if( M<N ){
    pTemp = aRecord[M];
    for(i=M; i<N; i++){
      aRecord[i] = aRecord[i+1];
    }
    aRecord[N] = pTemp;
  }else{
    pTemp = aRecord[M];
    for(i=M; i>N; i--){
      aRecord[i] = aRecord[i-1];
    }
    aRecord[N] = pTemp;
  }
}

/*
** Return TRUE if the given string is an IP address composed of
** 4 numbers between 0 and 255 separated by dots.  If the string
** is an IP address, return the value in *pIp.
*/
static int IsIpAddr(char *z,  int *pIp){
  int a1, a2, a3, a4, rc;
  if( sscanf(z,"%d.%d.%d.%d",&a1,&a2,&a3,&a4)==4
    && a1>=0 && a1<=255
    && a2>=0 && a2<=255
    && a3>=0 && a3<=255
    && a4>=0 && a4<=255
  ){
    *pIp = (a1<<24) | (a2<<16) | (a3<<8) | a4;
    rc = 1;
  }else{
    rc = 0;
  }
  return rc;
}

/*
** Look up a specific field in a specific record and return its
** value.  Return the given default value if it is not found.
*/
static char *Lookup(int iRec, char *zLabel, char *zDflt){
  ConfEntry *p;
  if( iRec<0 || iRec>=nRecord ) return zDflt;
  for(p=aRecord[iRec]; p; p=p->next){
    if( strcmp(zLabel,p->zLabel)==0 ) return p->zValue;
  }
  return zDflt;
}

/*
** Given a command-line parameter that might be a record number,
** or an IP address, or a modem tty name, or the keyword "all",
** return a record number.  The special code FIND_ERROR is returned
** if we can't find a match.  FIND_ALL is returned if the keyword
** "all" appears or for a NULL string.
*/
#define FIND_ALL    -1
#define FIND_ERROR  -2
static int FindRecord(char *z){
  int i;
  int ip;

  if( z==0 ){
    if( nRecord==1 ) return 0;
    return FIND_ALL;
  }
  if( strcmp(z,"all")==0 ){
    return FIND_ALL;
  }
  i = GetInt(z);
  if( i>=0 ){
    if( i>=nRecord ) i = nRecord-1;
    if( i<0 ) i = 0;
    return i;
  }
  if( IsIpAddr(z,&ip) ){
    int t, mask, iRec;
    char *zVal;
    for(iRec=0; iRec<nRecord; iRec++){
      zVal = Lookup(iRec,"ip","0.0.0.0");
      if( !IsIpAddr(zVal,&t) ) continue;
      zVal = Lookup(iRec,"netmask","0.0.0.0");
      if( !IsIpAddr(zVal,&mask) ) continue;
      if( (t&mask)==(ip&mask) ) return iRec;
    }
  }
  if( z[0]=='/' ){
    int iRec;
    char *zVal;
    for(iRec=0; iRec<nRecord; iRec++){
      zVal = Lookup(iRec,"tty","/dev/modem");
      if( strcmp(zVal,z)==0 ) return iRec;
    }
  }
  for(i=0; i<nRecord; i++){
    char *zVal = Lookup(i,"service",0);
    if( zVal && strcmp(zVal,z)==0 ) return i;
  }
  return FIND_ERROR;
}

/*
** Write a timestamp on the transcript.
*/
static void WriteTimestamp(void){
  struct timeval stv;
  struct timezone stz;
  if( transcript==0 ) return;
  gettimeofday(&stv,&stz);
  fprintf(transcript,"%02ld:%02ld.%03ld", (long)(stv.tv_sec/60)%60,
    (long)stv.tv_sec % 60, (long)stv.tv_usec/1000);
}

/*
** Write to a file descriptor, and also to the transcript.
*/
static void ChatWrite(char *z, int shroud){
  int n, w;
  if( transcript ){
    if( shroud>0 ){
      int i = strlen(z);
      while( i>0 ){
        fprintf(transcript,"%.*s",i,"**********" "**********" "***********");
        i -= 30;
      }
    }else{
      WriteHumanReadable(transcript,z);
    }
  }
  n = strlen(z);
  while( n ){
    w = write(1, z, n);
    if( w>0 ){
      n -= w;
      z += w;
    }
  }
}

/*
** Send output to the modem. 
**
** Delay for "delay" microseconds before actually doing the write.
** This is for modems that get fouled up by input that follows to
** closely after their own output.
**
** The transmitted text is normally written to the transcript file.
** But if "shroud" is one, then "*" characters are subsitituted.
** This is used when writting the password, to keep it from view.
*/
static void ChatSend(int delay, int shroud, char *z, ...){
  va_list ap;
  if( z==0 ) return;
  if( delay>0 ){
    usleep(delay);
  }
  if( transcript ){
    WriteTimestamp();
    fprintf(transcript," Send:  \"");
  }
  va_start(ap, z);
  ChatWrite(z, shroud);
  while( (z = va_arg(ap,char*))!=0 ){
    ChatWrite(z, shroud);
    shroud--;
  }
  if( transcript ){
    fprintf(transcript,"\"\n");
    fflush(transcript);
  }
}

/*
** Read text from the input and put it into zBuf.  Reading continues
** until:
**
**   *  No characters are received after waiting for "timeout" 
**      seconds, or
**
**   *  Some characters have been received but no characters have
**      appeared for 500 milliseconds or more.
**
** Only characters since the last new-line are returned in zBuf.
** But all received characters are written to the transcript. 
**
** Characters in zBuf[] when this routine is called are not erased
** or overwritten.  Newly read characters are appended.  Unless
** a new-line or carriage routine is seen, when all characters are
** deleted.
**
** The function returns the number of characters that were received
** during the current call to this function.  This number will be
** zero if a timeout occurs.
*/
static int ChatRecv(char *zBuf, int nBuf, int timeout){
  int n;            /* Number of bytes returned from a single read() */
  int i;            /* Loop counter */
  int idle = 0;     /* Number of 1/10ths of second since last read() */
  int got;          /* How many characters previously read */
  int nChar = 0;    /* Total number of characters read */
  
  time_t start;
  time_t now;
  
  /* Remember when we began looking... */
  time(&start);

  /* Figure out how much text is already in zBuf[].  New
  ** text will be appended.
  */
  for(got=0; zBuf[got]; got++){}
  if( got>0 && (zBuf[got-1]=='\n' || zBuf[got-1]=='\r') ){ got = 0; }
  zBuf[got] = 0;

  /* Start reading input...
  */
  while( got<nBuf-1 && (nChar==0 || idle<5) ){
    time(&now);
    if( start+timeout < now ) break;
    n = read(0, &zBuf[got], nBuf-1-got);
    if( n<0 ) n = 0;
    nChar += n;
    if( n==0 ){
      usleep(100000);
      idle++;
      continue;
    }
    zBuf[got+n] = 0;
    idle = 0;
    got += n;

    /* Remove any characters that are followed by a '\n'.  Except,
    ** don't remove characters if the '\n' is followed by nothing
    ** unless '\n' is on a line by itself or preceded by a single
    ** '\r'.
    */
    for(i=0; i<got; i++){
      if( zBuf[i]=='\n' && (zBuf[i+1]!=0 || i==0 || (i==1 && zBuf[0]=='\r')) ){
        if( transcript ){
          int c;
          WriteTimestamp();
          fprintf(transcript," Skip:  \"");
          c = zBuf[i+1];
          zBuf[i+1] = 0;
          WriteHumanReadable(transcript,zBuf);
          zBuf[i+1] = c;
          fprintf(transcript,"\"\n");
          fflush(transcript);
        }
        strcpy(zBuf, &zBuf[i+1]);
        got -= i+1;
        i = -1;
      }
    }
  }
  if( transcript && zBuf[0] ){
    WriteTimestamp();
    fprintf(transcript," Recv:  \"");
    WriteHumanReadable(transcript,zBuf);
    fprintf(transcript,"\"\n");
    fflush(transcript);
  }
  return nChar;
}

/*
** Return true if zPattern occurs anywhere in zBuf
*/
static int Match(char *zBuf, char *zPattern){
  int len = strlen(zPattern);
  while( *zBuf ){
    if( *zBuf==*zPattern && strncmp(zBuf,zPattern,len)==0 ){
      if( transcript ){
        WriteTimestamp();
        fprintf(transcript," Match: \"");
        WriteHumanReadable(transcript,zPattern);
        fprintf(transcript,"\"\n");
        fflush(transcript);
      }
      return 1;
    }
    zBuf++;
  }
  return 0;
}

/*
** The following array maps 8-bit characters into 7-bit characters
** and upper-case into lower-case.
*/
static unsigned char SevenBit[] = {
   0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15,
  16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
  32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
  48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
  64, 97, 98, 99,100,101,102,103,104,105,106,107,108,109,110,111,
 112,113,114,115,116,117,118,119,120,121,122, 91, 92, 93, 94, 95,
  96, 97, 98, 99,100,101,102,103,104,105,106,107,108,109,110,111,
 112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,
 128,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15,
  16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
  32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
  48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
  64, 97, 98, 99,100,101,102,103,104,105,106,107,108,109,110,111,
 112,113,114,115,116,117,118,119,120,121,122, 91, 92, 93, 94, 95,
  96, 97, 98, 99,100,101,102,103,104,105,106,107,108,109,110,111,
 112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,
};

/*
** Compare two strings.  In the first string, use only the lower
** 7 bits and convert all upper case to lower case letters.
*/
static int StrNCmp7(char *zBuf, char *zPattern, int n){
  while( n-- ){
    int a = SevenBit[(int)*((unsigned char*)zBuf++)];
    int b = SevenBit[(int)*((unsigned char*)zPattern++)];
    if( a==0 || b==0 || a!=b ){ return a - b; }
  }
  return 0;
}

/*
** Like Match(), but only uses the lower seven 7 bits of zBuf
** and treats all characters in zBuf as lower-case.
*/
static int Match7(char *zBuf, char *zPattern){
  int len = strlen(zPattern);
  while( *zBuf ){
    if( SevenBit[*(unsigned char*)zBuf]==SevenBit[*(unsigned char*)zPattern]
    && StrNCmp7(zBuf,zPattern,len)==0 ){
      if( transcript ){
        WriteTimestamp();
        fprintf(transcript," Match: \"");
        WriteHumanReadable(transcript,zPattern);
        fprintf(transcript,"\"\n");
        fflush(transcript);
      }
      return 1;
    }
    zBuf++;
  }
  return 0;
}

/*
** Return TRUE if the input line looks like a prompt of some kind.
** A "prompt" is a seqence of words followed by a colon, or perhaps
** something like "->".
*/
static int IsPrompt(char *zLine){
  int i;
  for(i=0; zLine[i]!=0 && zLine[i]!='\n' && zLine[i]!='\r'; i++){}
  while( i>0 && isspace(zLine[i-1]) ){ i--; }
  i--;
  if( i<=0 ) return 0;
  if( zLine[i]==':' ) return 1;
  if( zLine[i]=='?' ) return 1;
  if( zLine[i]=='>' ) return 1;
  if( zLine[i]=='$' ) return 1;
  return 0;
}

#define MAX_TIME      70   /* Maximum time to connect */
#define DIAL_TIMEOUT "60"  /* How long to wait for dialing to complete */
#define CHAT_TIMEOUT  "3"  /* How long to wait for responses after dialing */

/* This routine is pretty much the whole point.  Assuming a
** modem is connected on file descriptors 0 and 1, initialize
** the modem, dial the phone number, supply user ID and password
** (if requested).  Return successfully (0) if we see the start of
** PPP communication and return an error (1) if anything goes
** wrong.
**
** This routine runs as root so that it will have permission
** to open the output file associated with WriteChatStat().
*/
static int Chat(int iRec){
  struct termios tio_0_saved;
  struct termios tio_1_saved;
  struct termios tio_new;
  int flags;
  int i;
  char *zVal;
  int timeout;
  int dial_timeout;
  int nTimeout = 0;
  time_t start, now;
  int rc = 4;
  int chatDelay = 0;
  char *zChat;
  char *zPhone;
  char *zExpect[10];
  char *zReply[10];
  char zLine[1000];

  tcgetattr(0,&tio_0_saved);
  tcgetattr(1,&tio_1_saved);
  tio_new = tio_0_saved;
  cfmakeraw(&tio_new);
  tcsetattr(0,TCSANOW,&tio_new);
  tio_new = tio_1_saved;
  cfmakeraw(&tio_new);
  tcsetattr(1,TCSANOW,&tio_new);
  flags = fcntl(0, F_GETFL);
  fcntl(0, F_SETFL, O_NONBLOCK);

  sprintf(zLine,"%s/transcript.%d",DB_DIR,iRec);
  transcript = fopen(zLine,"a");
  if( transcript ){
    fchmod(fileno(transcript),0644);
    fchown(fileno(transcript),0,0);
  }

  zChat = Lookup(iRec,"chat","yes");
  chatDelay = atoi(Lookup(iRec,"delay","0"))*1000;
  for(i=0; i<=9; i++){
    static char *dfltInit[10] = { "atz", "at&d3", };
    sprintf(zLine,"init%d",i);
    zVal = Lookup(iRec,zLine,dfltInit[i]);
    if( zVal ){
      ChatSend(chatDelay,0,zVal,"\r",0);
      zLine[0] = 0;
      ChatRecv(zLine,sizeof(zLine),1000);
      if( !Match(zLine,"OK") ){
        ChatRecv(zLine,sizeof(zLine),1000);
      }
    }
    sprintf(zLine,"expect%d",i);
    zExpect[i] = Lookup(iRec,zLine,0);
    sprintf(zLine,"reply%d",i);
    zReply[i] = Lookup(iRec,zLine,0);
  }
  zPhone = Lookup(iRec,"phone",0);
  if( zPhone==0 ){
    WriteChatStat(iRec,"No phone number specified");
    return 1;
  }
  ChatSend(chatDelay,0,"atd", zPhone, "\r", 0);
  timeout = dial_timeout = atoi(Lookup(iRec,"dialtimeout",DIAL_TIMEOUT));
  time(&start);
  zLine[0] = 0;
  nTimeout = 0;
  while( 1 ){
    int nChar = ChatRecv(zLine,sizeof(zLine),timeout);
    time(&now);
    if( now-start > MAX_TIME ){
      WriteChatStat(iRec,"Login timeout at");
      break;
    }
    if( zLine[0]=='a' && strncmp(zLine,"atd",3)==0
    && zLine[3]==zPhone[0] && strncmp(&zLine[3],zPhone,strlen(zPhone))==0 ){
      /* Skip the echo of the dial command to the modem */
      zLine[0] = 0;
      continue;
    }
    if( nChar==0 ){
      nTimeout++;
      if( timeout==dial_timeout ){
        WriteChatStat(iRec,"Modem could not connect at");
        break;
      }else if( (nTimeout>2 && *Lookup(iRec,"autostart","yes")!='n')
                || zChat[0]=='n' ){
        rc = 0;
        break;
      }else{
        ChatSend(chatDelay,0,"\r",0);
        continue;
      }
    }
    timeout = atoi(Lookup(iRec,"chattimeout",CHAT_TIMEOUT));
    nTimeout = 0;
    for(i=0; i<=9; i++){
      if( zExpect[i]==0 || zReply[i]==0 ) continue;
      if( Match7(zLine,zExpect[i]) ){
        zLine[0] = 0;
        if( strcmp(zReply[i],"ACCEPT")==0 ){
          rc = 0;
          goto chat_done;
        }else if( strcmp(zReply[i],"FAIL")==0 ){
          goto chat_done;
        }else{
          ChatSend(chatDelay,0,zReply[i],"\r",0);
          nTimeout = 0;
          timeout = 3;
          break;
        }
      }
    }
    if( Match(zLine,"BUSY") ){
      WriteChatStat(iRec,"Busy signal at");
      break;
    }else if( Match(zLine,"NO CARRIER") ){
      WriteChatStat(iRec,"No carrier at");
      break;
    }else if( Match(zLine,"NO DIALTONE") ){
      WriteChatStat(iRec,"No dialtone at");
      break;
    }else if( Match(zLine,"NO ANSWER") ){
      WriteChatStat(iRec,"Ringing but no answer at");
      break;
    }else if( Match7(zLine,"ogin:") || Match7(zLine,"rname:") 
              || Match7(zLine,"-on:") || Match7(zLine,"serid:") ){
      zVal = Lookup(iRec,"user","anonymous");
      ChatSend(chatDelay,0,zVal,"\r",0);
      zLine[0] = 0;
    }else if( Match7(zLine,"ssword:") ){
      zVal = Lookup(iRec,"password","");
      ChatSend(chatDelay,1,zVal,"\r",0);
      zLine[0] = 0;
    }else if( Match(zLine,"~\377}#") || Match(zLine,"\300!}!") ){
      rc = 0;
      break;
    }else if( Match(zLine,"CONNECT") && zChat[0]=='n' ){
      rc = 0;
      break;
#if 0
    }else if( Match7(zLine,"destination:") || Match7(zLine,"response:") ){
      ChatSend(chatDelay,0,"ppp\r",0);
      zLine[0] = 0;
#endif
    }else if( IsPrompt(zLine) ){
      ChatSend(chatDelay,0,"ppp\r",0);
      zLine[0] = 0;
    }else{
      /* Ignore it */
    }
  }

chat_done:
  if( transcript ){
    WriteTimestamp();
    fprintf(transcript, rc ? " Fail\n" : " Accept\n");
    fclose(transcript);
  }
  fcntl(0,F_SETFL,flags);
  tcsetattr(0,TCSANOW,&tio_0_saved);
  tcsetattr(1,TCSANOW,&tio_1_saved);
  return rc;
}

/* Print status information for a given service.
*/
static void PrintStatus(int iRec){
  char *zService;
  char *zStatus;
  char *zDiald;
  int pidDiald;
  char zBuf[30];
  char zFile[sizeof(DB_DIR)+100];

  if( iRec<0 || iRec>=nRecord ) return;
  sprintf(zFile,"%s/diald.%d",DB_DIR,iRec);
  zDiald = ReadFile(zFile);
  if( zDiald==0 || (pidDiald=atoi(zDiald))<=0 
     || kill(pidDiald,0)!=0 ){
    zDiald = "";
  }else{
    zDiald = "(diald running) ";
  }
  sprintf(zBuf,"service #%d",iRec);
  zService = Lookup(iRec,"service",zBuf);
  sprintf(zFile,"%s/status.%d",DB_DIR,iRec);
  zStatus = ReadFile(zFile);
  if( zStatus==0 ){
    printf("%s: %sStatus Unknown\n",zService, zDiald);
  }else{
    printf("%s: %s%s",zService, zDiald, zStatus);
    free( zStatus );
  }
}

/*
** Return true if the given string contains a space or $ character
** and therefore needs to be quoted for the shell.
*/
static int NeedQuote(char *z){
  while( *z ){
    if( *z==' ' || *z=='$' || *z=='"' ) return 1;
    z++;
  }
  return 0;
}

/*
** Open the transcript for record N and write the given argv[] string
** to it.
*/
static void WriteArgvToTranscript(int iRec, char **argv){
  int size;
  int i;
  char *zStr, *z;
  char zFile[sizeof(DB_DIR)+20];

  size = 0;
  for(i=0; argv[i]; i++){
    size += strlen(argv[i]) + 3;
  }
  zStr = z = malloc( size );
  if( zStr ){
    for(i=0; argv[i]; i++){
      if( NeedQuote(argv[i]) ){
        *z++ = '\'';
        strcpy(z,argv[i]);
        z += strlen(z);
        *z++ = '\'';
      }else{
        strcpy(z,argv[i]);
        z += strlen(z);
      }
      if( argv[i+1] ){
        *z++ = ' ';
      }
    }
    *z = 0;
    sprintf(zFile,"%s/transcript.%d", DB_DIR, iRec);
    WriteToFile(zFile, 0600, "%s\n", zStr);
  }
}

/* Check to see if diald is already running.  Return a process ID
** for diald if it is running.  Or return 0 if there is no diald
** running.
*/
static int DialdPid(int iRec){
  char *zDiald;
  int pidDiald;
  char zDialdFile[sizeof(DB_DIR)+100];

  sprintf(zDialdFile, "%s/diald.%d", DB_DIR, iRec);
  zDiald = ReadFile(zDialdFile);
  if( zDiald==0 ) return 0;
  pidDiald = atoi(zDiald);
  free(zDiald);
  if( pidDiald==0 ){
    unlink(zDialdFile);
    return 0;
  }
  if( kill(pidDiald,0) ){
    unlink(zDialdFile);
    return 0;
  }
  return pidDiald;
}

/*
** Shutdown the diald process
*/
static int StopDiald(int iRec){
  int pidDiald;

  if( (pidDiald = DialdPid(iRec))!=0 ){
    kill(pidDiald,SIGTERM);
  }
  return 0;
}

/* Attempt to hang up the given port.
*/
static void Hangup(int iRec){
  char *zDev;
  char *zPpp;
  int pidPpp = -1;
  int pidDiald = -1;
  int fd;
  int tries;
  struct termios ios;
  char zFile[sizeof(DB_DIR)+20];

  WriteStatus(iRec,"hangup at");

  /* Send a SIGINT to any diald process that is running.
  ** This is suppose to make diald hangup the line.
  */
  pidDiald = DialdPid(iRec);
  if( pidDiald ){
    kill(pidDiald,SIGINT);
    return;
  }

  /* We only reach this point if diald is not running.
  ** Send a SIGTERM to any running ppp process.  This is suppose
  ** to make ppp hang up the line and exit
  */
  sprintf(zFile,"%s/ppp.%d", DB_DIR, iRec);
  zPpp = ReadFile(zFile);
  if( zPpp && (pidPpp=atoi(zPpp))>0 ){
    kill(pidPpp,SIGTERM);
    sleep(1);
  }

  /* Try to open the TTY.  Try as many as three times waiting for
  ** one second between each attempt.  After the second failed attempt,
  ** send a SIGKILL to any diald or ppp process out there.
  */
  zDev = Lookup(iRec,"tty","/dev/modem");
  tries = 0;
  while( (fd=open(zDev,O_RDWR|O_NONBLOCK))<0 && tries<2 ){
    if( tries==1 ){
      if( pidPpp>0 ){
        kill(pidPpp, SIGKILL);
      }else if( pidDiald>0 ){
        kill(pidDiald, SIGKILL);
      }
    }
    tries++;
    sleep(1);
  }

  /* Change the buad rate of the TTY to 0.  This is suppose to make
  ** the DTR line go low, which should make the modem hangup.
  */ 
  if( fd>=0 ){
    tcgetattr(fd, &ios);
    cfsetospeed(&ios, B0);
    tcsetattr(fd, TCSANOW, &ios);
    close(fd);
  }
}

/*
** Write into the given buffer the name of a file that will contain
** the PID of the PPP process that is calling on serial device zDev.
** Assume the size of the buffer into which we write is at least
** sizeof(DB_DIR)+100.
**
** The zDev probably contains '/' characters.  These must be converted
** to something different.
*/
static void MakeDeviceFile(char *zFile, char *zDev){
  int i;
  sprintf(zFile,"%s/device%.80s", DB_DIR, zDev);
  i = strlen(DB_DIR) + 5;
  while( zFile[i] ){
    if( zFile[i]=='/' ){ zFile[i] = '.'; }
    i++;
  }
}

/*
** Given a modem device name, find the service number.
*/
static int DeviceToRec(char *zDev){
  char *z;
  int iRec;
  char zFile[sizeof(DB_DIR)+100];

  MakeDeviceFile(zFile, zDev);
  z = ReadFile(zFile);
  if( z==0 || (iRec=atoi(z))<0 || iRec>=nRecord ){
    for(iRec=0; iRec<nRecord; iRec++){
      z = Lookup(iRec,"tty","/dev/modem");
      if( strcmp(z,zDev)==0 ) break;
    }
    if( iRec>nRecord ) iRec = FIND_ERROR;
  }
  return iRec;
}

/* This routine is called when "pppd" executes "ip-up".
*/
static void IpUp(char **argv){
  int iRec;
  int i;
  char zFile[sizeof(DB_DIR)+20];

  iRec = DeviceToRec(argv[1]);
  if( iRec<0 ) return;
  WriteStatus(iRec,"%s %s to %s up since",argv[0],argv[3],argv[4]);
  sprintf(zFile,"%s/pid.%d", DB_DIR, iRec);
  unlink(zFile);
  for(i=0; i<=9; i++){
    char *zNet;
    char zRoute[1000];
    char zLabel[20];
    sprintf(zLabel,"route%d",i);
    zNet = Lookup(iRec,zLabel,0);
    if( zNet==0 ) continue;
    sprintf(zRoute,"/sbin/route add -net %.100s %.100s", zNet, argv[0]);
    system(zRoute);
  }
}

/* This routine is called when "pppd" executes "ip-down".
*/
static void IpDown(char **argv){
  int iRec;
  char zFile[sizeof(DB_DIR)+80];

  iRec = DeviceToRec(argv[1]);
  if( iRec<0 ) return;
  WriteStatus(iRec,"down since");
  MakeDeviceFile(zFile, argv[1]);
  unlink(zFile);
  sprintf(zFile,"%s/ppp.%d", DB_DIR, iRec);
  unlink(zFile);
}

/*
** This routine creates a session log for the most recent login
** attempt.  The session log consists of two data sources:
**
**    1.  The modem dialing and logon transcript.
**
**    2.  All messages written to the /var/log/messages file by pppd.
**
** The session log is overwritten after every logon attempt.
*/
static void MakeSessionLog(time_t startTime, int pid, int iRec){
  FILE *out;
  char *z;
  struct tm *pTm;
  char zFile[sizeof(DB_DIR)+100];
  char zCmd[sizeof(DB_DIR)+100];

  out = fopen(SESSIONLOG,"w");
  if( out==0 ) return;
  fchmod(fileno(out),0644);
  fprintf(out,"This is a diagnostic log of the most recent login\n"
     "attempt by eznet.\n\n");
  z = Lookup(iRec,"service",0);
  if( z ){
    fprintf(out, "  ISP Name: \"%s\"\n", z);
  }
  pTm = localtime(&startTime);
  fprintf(out,"  Call Initiated: %s\n", asctime(pTm));
  sprintf(zFile,"%s/transcript.%d", DB_DIR, iRec);
  z = ReadFile(zFile);
  if( z ){
    int i;
    for(i=0; z[i] && z[i]!='\n'; i++){}
    fprintf(out,
      "The UNIX command used to initiate the connection attempt was: \n\n"
      "%.*s\n\n",
      i, z);
    if( z[i] ){ i++; }
    fprintf(out,"The interaction with the modem was as follows:\n\n"
      "%s\n", &z[i]);
    free(z);
  }else{
    fprintf(out,"The modem transcript could not be located!\n\n");
  }
  fprintf(out,
    "The following messages were written to the system log\n"
    "(in /var/log/messages) by pppd as it attempted to negotiate\n"
    "a PPP connection.");
  z = Lookup(iRec,"debug","n");
  if( z && *z=='y' ){
    fprintf(out,"  Details may be omitted by clearing the\n"
       "\"debug=%s\" property.\n\n", z);
  }else{
    fprintf(out,"  To see more detail, set the \"debug=yes\"\n"
       "property and reattempt the logon.\n\n");
  }
  fclose(out);
  sprintf(zCmd,"tail -200 /var/log/messages | grep 'pppd.%d.' >>%s",
    pid, SESSIONLOG);
  system(zCmd);
}

/* This routine is called in order to attempt to establish
** a connection.  Return 0 if successful and non-zero if we fail.
**
** The process ID of the PPP process is written to *piChild if piChild!=0.
*/
static int Dial(int iRec, int *piChild){
  char *zPw;
  char *zDevice;
  char *zUser;
  char *zPpp;
  char *zService;
  char *zIdle;
  int pidPpp;
  int pidDiald;
  int rc;
  int isup = 1;
  time_t startTime;
  char zDeviceFile[sizeof(DB_DIR)+100];
  char zChatStat[sizeof(DB_DIR)+100];
  char zPidFile[sizeof(DB_DIR)+100];
  char zConnect[200];
  char zServiceBuf[50];

  /* Check to see if we are already running directly.
  */
  startTime = time(0);
  sprintf(zPidFile, "%s/pid.%d", DB_DIR, iRec);
  zPpp = ReadFile(zPidFile);
  if( zPpp && (pidPpp=atoi(zPpp))>0 && kill(pidPpp,0)==0 ){
    return 0; 
  }

  /* Check to see if diald is running.  If it is, then
  ** just send it a SIGUSR1 to bring the link up.
  */
  if( (pidDiald = DialdPid(iRec))>0 ){
    kill(pidDiald,SIGUSR1);
    return 0;
  }

  /* Initialize the ipup, ipdown, chap-secrets and pap-secrets files.
  */
  SetIpUpDown("up");
  SetIpUpDown("down");
  sprintf(zServiceBuf,"x%d",iRec);
  zService = Lookup(iRec,"service",zServiceBuf);
  zUser = Lookup(iRec,"user","anonymous");
  zPw = Lookup(iRec, "password", "*");
  if( zPw ){
    SetSecrets("pap-secrets",zService,zUser,zPw);
    SetSecrets("chap-secrets",zService,zUser,zPw);
  }
  zDevice = Lookup(iRec, "tty", "/dev/modem");

  /* Set the device file to the current ordinal number.  This is needed
  ** by the ipup and ipdown scripts.
  */
  MakeDeviceFile(zDeviceFile, zDevice);
  WriteToFile(zDeviceFile, 0644, "%d\n", iRec);
  WriteToFile(zPidFile, 0644, "%d\n", getpid());

  /* Set the status to dialing.
  */
  WriteStatus(iRec, "dialing since");

  /* The child does the work.  The parent waits for it to finish.
  */
  rc = fork();
  if( rc<0 ){
    WriteStatus(iRec,"fork failed");
    return 1;
  }
  if( rc==0 ){
    /* The child process */
    int argc = 0;
    char *z;
    int i;
    char zLabel[100];
    char *argv[200];

    argv[argc++] = Lookup(iRec, "pppd", PPP_EXE);
    argv[argc++] = zDevice;
    argv[argc++] = Lookup(iRec,"baud","115200");
    argv[argc++] = "connect";
    sprintf(zConnect,SELF " chat %d",iRec);
    argv[argc++] = zConnect;
    z = Lookup(iRec, "defaultroute", "y");
    if( *z=='y' ){
      argv[argc++] = "defaultroute";
    }
    argv[argc++] = "lock";
    argv[argc++] = "modem";
    argv[argc++] = "mtu";
    argv[argc++] = Lookup(iRec,"mtu","552");
    argv[argc++] = "mru";
    argv[argc++] = Lookup(iRec,"mru","552");
    z = Lookup(iRec,"debug",0);
    if( z && *z=='y' ){
      argv[argc++] = "debug";
    }
    argv[argc++] = "crtscts";
    argv[argc++] = "-detach";
    z = Lookup(iRec,"persist","no");
    if( *z=='y' ){
      argv[argc++] = "persist";
    }else{
      zIdle = Lookup(iRec,"idle","300");
      if( atoi(zIdle)>0 ){
        z = Lookup(iRec,"pppversion",DFLT_PPP_VERSION);
        argv[argc++] = strcmp(z,"2.3")<0 ? "idle-disconnect" : "idle";
        argv[argc++] = zIdle;
      }
    }
    argv[argc++] = "user";
    argv[argc++] = zUser;
    argv[argc++] = "remotename";
    argv[argc++] = zService;
    for(i=0; i<10; i++){
      sprintf(zLabel,"pppopt%d",i);
      z = Lookup(iRec,zLabel,0);
      if( z==0 ) break;
      argv[argc++] = z;
    }
    argv[argc] = 0;
    WriteArgvToTranscript(iRec,argv);
    execv(argv[0],argv);
    exit(0);
  }else{ 
    /* The parent process */
    char zPppFile[sizeof(DB_DIR)+20];

    if( piChild ) *piChild = rc;
    sprintf(zPppFile,"%s/ppp.%d", DB_DIR, iRec);
    WriteToFile(zPppFile,0644,"%d\n",rc);
    while( access(zPidFile,0)==0 ){
      int status;
      if( waitpid(rc,&status,WNOHANG)>0 ){
        isup = 0;
        break;
      }
      sleep(1);
    }
    sprintf(zChatStat,"%s/chatstat.%d", DB_DIR, iRec);
    if( !isup ){
      char *z = ReadFile(zChatStat);
      if( z ){
        WriteStatus(iRec,z);
      }else{
        WriteStatus(iRec,"pppd failure at");
      }
      unlink(zDeviceFile);
      unlink(zPppFile);
    }
    unlink(zPidFile);
    unlink(zChatStat);
    MakeSessionLog(startTime, rc,iRec);
    PrintStatus(iRec);
  }
  return !isup;
}


/* This routine is called in order to active the diald daemon.
*/
static int StartDiald(int iRec){
  char *zPpp;
  char *zDiald;
  char *zPw;
  char *zUser;
  char *zService;
  char *z;
  int pidPpp;
  int pidDiald;
  int rc;
  int i;
  FILE *out;
  char zConfFile[sizeof(DB_DIR)+100];
  char zPidFile[sizeof(DB_DIR)+20];
  char zDialdFile[sizeof(DB_DIR)+100];
  char zServiceBuf[50];

  /* Check to see if diald is already running.
  */
  sprintf(zDialdFile, "%s/diald.%d", DB_DIR, iRec);
  zDiald = ReadFile(zDialdFile);
  if( zDiald && (pidDiald=atoi(zDiald))>0 && kill(pidDiald,0)==0 ){
    return 0;
  }

  /* Check to see if we are already running directly.  If so, then
  ** do a hangup before continuing.
  */
  sprintf(zPidFile, "%s/pid.%d", DB_DIR, iRec);
  zPpp = ReadFile(zPidFile);
  if( zPpp && (pidPpp=atoi(zPpp))>0 && kill(pidPpp,0)==0 ){
    Hangup(iRec);
  }

  /* Initialize the chap-secrets and pap-secrets files.
  */
  sprintf(zServiceBuf,"x%d",iRec);
  zService = Lookup(iRec,"service",zServiceBuf);
  zUser = Lookup(iRec,"user","anonymous");
  zPw = Lookup(iRec, "password", "*");
  if( zPw ){
    SetSecrets("pap-secrets",zService,zUser,zPw);
    SetSecrets("chap-secrets",zService,zUser,zPw);
  }

  /* Create the diald configuration script.
  */
  sprintf(zConfFile, "%s/diald.conf.%d", DB_DIR, iRec);
  out = fopen(zConfFile,"w");
  if( out==0 ){
    WriteStatus(iRec, "configuration failed");
    return 1;
  }
  fprintf(out,"mode ppp\n");
  fprintf(out,"device %s\n", Lookup(iRec,"tty","/dev/modem"));
  fprintf(out,"connect '" SELF " chat %d'\n", iRec);
  fprintf(out,"speed %s\n", Lookup(iRec,"baud","115200"));
  z = Lookup(iRec,"defaultroute","y");
  if( *z=='y' ){
    fprintf(out,"defaultroute\n");
  }
  fprintf(out,"local %s\n", Lookup(iRec,"local","127.0.0.100"));
  fprintf(out,"remote %s\n", Lookup(iRec,"remote","127.0.0.101"));
  fprintf(out,
    "lock\n"
    "modem\n"
    "crtscts\n"
    "-reroute\n"
    "dynamic\n"
    "-daemon\n"
  );
  fclose(out);

  /* Fire up the diald process
  */
  rc = fork();
  if( rc<0 ){
    WriteStatus(iRec,"fork failed");
    return 1;
  }
  if( rc==0 ){
    /* The child process */
    int argc = 0;
    char *argv[30];

    argv[argc++] = Lookup(iRec, "diald", DIALD_EXE);
    argv[argc++] = "-f";
    argv[argc++] = zConfFile;
    argv[argc++] = "--";
    argv[argc++] = "remotename";
    argv[argc++] = zService;
    argv[argc++] = "user";
    argv[argc++] = zUser;
    for(i=0; i<10; i++){
      char zLabel[100];
      sprintf(zLabel,"pppopt%d",i);
      z = Lookup(iRec,zLabel,0);
      if( z==0 ) break;
      argv[argc++] = z;
    }
    argv[argc] = 0;
    WriteArgvToTranscript(iRec,argv);
    close(0);
    close(1);
    close(2);
    open("/dev/null",O_RDWR);
    open("/dev/null",O_RDWR);
    open("/dev/null",O_RDWR);
    execv(argv[0],argv);
    exit(0);
  }
  /* The parent process */
  WriteToFile(zDialdFile,0644,"%d\n",rc);
  sleep(1);
  if( kill(rc,0) ){
    WriteStatus(iRec,"diald startup failed at");
    rc = 1;
  }else{
    WriteStatus(iRec,"diald started at");
    rc = 0;
  }
  return rc;
}

/*
** Given a configuration file entry, return a "safe" value for
** this entry.  If the entry is the password and the real user
** is anything other than root, return "*********".
*/
static char *SafeValue(ConfEntry *p){
  char *zVal;
  if( strcmp(p->zLabel,"password")==0 && getuid()!=0 ){
    zVal = "**********";
  }else{
    zVal = p->zValue;
  }
  return zVal;
}

/* Print a usage comment and die.  A call to this
** routine never returns.
*/
static void usage(char *argv0){
  fprintf(stderr,
    "User commands:\n"
    "  %s add ORDINAL FIELD=VALUE...\n"
    "  %s change ORDINAL|SERVICE ?FIELD=VALUE?...\n"
    "  %s delete ORDINAL|SERVICE|IP\n"
    "  %s dialdoff ORDINAL|IP|DEVICE\n"
    "  %s dialdon ORDINAL|IP|DEVICE\n"
    "  %s down ORDINAL|IP|DEVICE|all\n"
    "  %s list ?ORDINAL|SERVICE?\n"
    "  %s log\n"
    "  %s move ORDINAL ORDINAL\n"
    "  %s names\n"
    "  %s status\n"
    "  %s up SERVICE|IP|ORDINAL\n"
    "  %s wait SERVICE|IP|ORDINAL\n"
    "\n"
    "Commands for internal use only:\n"
    "  %s chat ORDINAL\n"
    "  %s ipdown INTERFACE DEVICE SPEED LOCAL-IP REMOTE-IP\n"
    "  %s ipup INTERFACE DEVICE SPEED LOCAL-IP REMOTE-IP\n"
    "\n"
    "Field names:\n"
    "  phone = phone number for modem to dial\n"
    "  user = user login name\n"
    "  password = login password\n"
    "  tty = serial port for the modem\n"
    "  baud = baud rate\n"
    "  service = name of ISP\n"
    "  chat = \"no\" to ignore \"login:\" prompts, etc.\n"
    "  autostart = \"no\" to prevent starting pppd if no \"login:\" seen\n"
    "  pppversion = what version of pppd is being used\n"
    "  debug = \"yes\" to turn on pppd debugging\n"
    "  expectN = Nth extra expect string (N between 1 and 9)\n"
    "  replyN = Reply to issue when expectN is seen\n"
    "  initN = Nth modem initialization string  (N between 1 and 9)\n"
    "  diald = name of diald executable\n"
    "  pppd = name of pppd executable\n"
    "  local = local IP address for diald slip link\n"
    "  remote = remote IP address for diald slip link\n"
    "  idle = number of seconds for idle timeout\n"
    "  persist = \"yes\" for a permanent connection\n"
    "  defaultroute = \"no\" to prevent creating a default route\n"
    "  pppoptN = Nth extra parameter to pppd\n"
    "  routeN = Add a route to the specified network when pppd comes up\n"
    "  ip = network that this service gives access to\n"
    "  netmask = netmask for the ip= network\n"
    "  dialtimeout = time allowed for the modem to connect\n"
    "  chattimeout = time allowed for chat responses\n"
    "  errordelay = \"wait\" command waits this long after an error\n",

    argv0, argv0, argv0, argv0, argv0, argv0, argv0, argv0,
    argv0, argv0, argv0, argv0, argv0, argv0, argv0, argv0
  );
  exit(1);
}


int main(int argc, char **argv){
  int iRec;
  int rc = 0;

  if( argc<2 ) usage(argv[0]);
  if( access(DB_DIR,0) ){
    mkdir(DB_DIR,0755);
    chown(DB_DIR,0,0);
    if( access(DB_DIR,0) ){
      fprintf(stderr,"%s: missing directory \"%s\"\n", argv[0], DB_DIR);
      exit(1);
    }
  }
  ReadDb();
  if( (argc==2 || argc==3) && strcmp(argv[1],"list")==0 ){
    int iRec = FindRecord(argv[2]);
    Unprivileged();
    if( iRec<0 || iRec>=nRecord ){
      int i;
      for(i=0; i<nRecord; i++){
        ConfEntry *p = aRecord[i] = sort(aRecord[i]);
        if( p==0 ) continue;
        printf("Record %d:\n",i);
        for(; p; p=p->next){
          printf("  %s = %s\n",p->zLabel, SafeValue(p));
        }
      }
    }else{
      ConfEntry *p = aRecord[iRec] = sort(aRecord[iRec]);
      for(; p; p=p->next){
        printf("  %s = %s\n",p->zLabel, SafeValue(p));
      }
    }    
  }else if( argc==2 && strcmp(argv[1],"log")==0 ){
    char *z;
    Unprivileged();
    z = ReadFile(SESSIONLOG);
    if( z ){
      printf("%s",z);
    }
  }else if( (argc==2 || argc==3 ) && strcmp(argv[1],"tcllist")==0 ){
    int i;
    Unprivileged();
    for(i=0; i<nRecord; i++){
      ConfEntry *p = aRecord[i] = sort(aRecord[i]);
      if( p==0 ) continue;
      for(; p; p=p->next){
        printf("set eznet(%d:%s) ",i,p->zLabel);
        WriteTcl(stdout,SafeValue(p));
        printf("\n");
      }
    }
  }else if( argc==2 && strcmp(argv[1],"names")==0 ){
    int i;
    Unprivileged();
    for(i=0; i<nRecord; i++){
      char *z = Lookup(i,"service",0);
      if( z ){ 
        printf("%s\n",z);
      }else{
        printf("%d\n",i);
      }
    }
  }else if( argc>=3 && strcmp(argv[1],"add")==0 ){
    int iRec;
    Unprivileged();
    iRec = FindRecord(argv[2]);
    if( iRec==FIND_ERROR ){
      AddEntries(nRecord, argc-2, argv+2);
    }else if( iRec>=nRecord ){
      AddEntries(nRecord, argc-3, argv+3);
    }else{
      AddEntries(nRecord, argc-3, argv+3);
      MoveRecord(nRecord, iRec);
    }
    WriteDb();
  }else if( argc==3 && strcmp(argv[1],"delete")==0 ){
    int iRec = FindRecord(argv[2]);
    Unprivileged();
    if( iRec==FIND_ALL ){
      nRecord = 0;
    }else if( iRec==FIND_ERROR ){
      usage(argv[0]);
    }else{
      MoveRecord(iRec,nRecord-1);
      aRecord[nRecord-1] = 0;
    }
    WriteDb();
  }else if( argc>=4 && strcmp(argv[1],"change")==0 ){
    int iRec;
    Unprivileged();
    iRec = FindRecord(argv[2]);
    if( iRec==FIND_ALL ){
      usage(argv[0]);
    }else{
      if( iRec==FIND_ERROR ){
        iRec = nRecord==1 ? 0 : nRecord;
      }
      AddEntries(iRec, argc-3, argv+3);
    }
    WriteDb();
  }else if( argc==4 && strcmp(argv[1],"move")==0 ){
    int from, to;
    Unprivileged();
    if( argc!=4 ) usage(argv[0]);
    from = GetInt(argv[2]);
    to = GetInt(argv[3]);
    if( from>=0 && to>=0 ) MoveRecord(from,to);
    WriteDb();
  }else if( argc==3 && strcmp(argv[1],"chat")==0 ){
    iRec = FindRecord(argv[2]);
    if( iRec<0 ) usage(argv[0]);
    rc = Chat(iRec);
  }else if( (argc==2 || argc==3) && strcmp(argv[1],"status")==0 ){
    iRec = FindRecord(argv[2]);
    Unprivileged();
    if( iRec<0 ){
      for(iRec=0; iRec<nRecord; iRec++){
        PrintStatus(iRec);
      }
    }else{
      PrintStatus(iRec);
    }
  }else if( (argc==2 || argc==3) && strcmp(argv[1],"down")==0 ){
    iRec = FindRecord(argv[2]);
    /*  Unprivileged();  --- Yeah.  Let anybody do a hangup... */
    if( iRec==FIND_ALL ){
      system("/bin/kill -TERM `/sbin/pidof pppd` >/dev/null 2>&1");
      system("/bin/kill -TERM `/sbin/pidof diald` >/dev/null 2>&1");
      for(iRec=0; iRec<nRecord; iRec++){
        Hangup(iRec);
      }
    }else{
      Hangup(iRec);
    }
  }else if( argc==7 && strcmp(argv[1],"ipup")==0 ){
    Unprivileged();
    IpUp(&argv[2]);
  }else if( argc==7 && strcmp(argv[1],"ipdown")==0 ){
    Unprivileged();
    IpDown(&argv[2]);
  }else if( (argc==2 || argc==3) && strcmp(argv[1],"up")==0 ){
    if( access(SELF,X_OK)!=0 ){
      fprintf(stderr,"eznet must be installed as \"" SELF "\"\n");
      exit(1);
    }
    iRec = FindRecord(argv[2]);
    if( iRec<0 ){ usage(argv[0]); }
    rc = Dial(iRec, 0);
  }else if( (argc==2 || argc==3) && strcmp(argv[1],"wait")==0 ){
    int child;
    if( access(SELF,X_OK)!=0 ){
      fprintf(stderr,"eznet must be installed as \"" SELF "\"\n");
      exit(1);
    }
    iRec = FindRecord(argv[2]);
    if( iRec<0 ){ usage(argv[0]); }
    rc = Dial(iRec, &child);
    if( rc==0 ){
      int status;
      waitpid(child, &status, 0);
    }else{
      int delay = atoi(Lookup(iRec,"errordelay","0"));
      if( delay>0 ) sleep(delay);
    }
  }else if( (argc==3 || argc==2) && strcmp(argv[1],"dialdon")==0 ){
    if( access(SELF,X_OK)!=0 ){
      fprintf(stderr,"eznet must be installed as \"" SELF "\"\n");
      exit(1);
    }
    iRec = FindRecord(argv[2]);
    if( iRec<0 ){ usage(argv[0]); }
    rc = StartDiald(iRec);
  }else if( (argc==3 || argc==2) && strcmp(argv[1],"dialdoff")==0 ){
    iRec = FindRecord(argv[2]);
    if( iRec<0 ){ usage(argv[0]); }
    rc = StopDiald(iRec);
  }else if( argc==3 && strcmp(argv[1],"delete")==0 ){
    Unprivileged();
    iRec = FindRecord(argv[2]);
    if( iRec>=0 && iRec<nRecord ){
      MoveRecord(iRec, nRecord-1);
      nRecord--;
      WriteDb();
    }
  }else{
    Unprivileged();
    usage(argv[0]);
  }
  return rc;
}
