/*

hostfile.c

Author: Tatu Ylonen <ylo@cs.hut.fi>

Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
                   All rights reserved

Created: Thu Jun 29 07:10:56 1995 ylo

Functions for manipulating the known hosts files.

*/

/*
 * $Id: hostfile.c,v 1.4 1999/02/21 19:52:20 ylo Exp $
 * $Log: hostfile.c,v $
 * Revision 1.4  1999/02/21 19:52:20  ylo
 * 	Intermediate commit of ssh1.2.27 stuff.
 * 	Main change is sprintf -> snprintf; however, there are also
 * 	many other changes.
 *
 * Revision 1.3  1998/07/08 00:43:25  kivinen
 *      Added ip number to mach_hostname. Changed it to use match_host
 *      instead of match_pattern.
 *
 * Revision 1.2  1997/03/19  21:13:51  kivinen
 *      Enlarged line buffer in check_host_in_hostfile to 16384.
 *
 * Revision 1.1.1.1  1996/02/18 21:38:12  ylo
 *      Imported ssh-1.2.13.
 *
 * Revision 1.2  1995/07/13  01:24:36  ylo
 *      Removed "Last modified" header.
 *      Added cvs log.
 *
 * $Endlog$
 */

#include "includes.h"
#include "packet.h"
#include "ssh.h"
#include "userfile.h"
#include "xmalloc.h"

/* Reads a multiple-precision integer in hex from the buffer, and advances the
   pointer.  The integer must already be initialized.  This function is
   permitted to modify the buffer.  This leaves *cpp to point just beyond
   the last processed (and maybe modified) character.  Note that this may
   modify the buffer containing the number. */

int auth_rsa_read_mp_int(char **cpp, MP_INT *value)
{
  char *cp = *cpp;
  int len, old;

  /* Skip any leading whitespace. */
  for (; *cp == ' ' || *cp == '\t'; cp++)
    ;

  /* Check that it begins with a hex digit. */
  if (*cp < '0' || *cp > '9')
    return 0;

  /* Save starting position. */
  *cpp = cp;

  /* Move forward until all hex digits skipped. */
  for (; *cp >= '0' && *cp <= '9'; cp++)
    ;

  /* Compute the length of the hex number. */
  len = cp - *cpp;

  /* Save the old terminating character, and replace it by \0. */
  old = *cp;
  *cp = 0;

  /* Parse the number. */
  if (mpz_set_str(value, *cpp, 10) != 0)
    return 0;

  /* Restore old terminating character. */
  *cp = old;

  /* Move beyond the number and return success. */
  *cpp = cp;
  return 1;
}

/* Parses an RSA key (number of bits, e, n) from a string.  Moves the pointer
   over the key.  Skips any whitespace at the beginning and at end. */

int auth_rsa_read_key(char **cpp, unsigned int *bitsp, MP_INT *e, MP_INT *n)
{
  unsigned int bits;
  char *cp;

  /* Skip leading whitespace. */
  for (cp = *cpp; *cp == ' ' || *cp == '\t'; cp++)
    ;

  /* Get number of bits. */
  if (*cp < '0' || *cp > '9')
    return 0; /* Bad bit count... */
  for (bits = 0; *cp >= '0' && *cp <= '9'; cp++)
    bits = 10 * bits + *cp - '0';

  /* Get public exponent. */
  if (!auth_rsa_read_mp_int(&cp, e))
    return 0;

  /* Get public modulus. */
  if (!auth_rsa_read_mp_int(&cp, n))
    return 0;

  /* Skip trailing whitespace. */
  for (; *cp == ' ' || *cp == '\t'; cp++)
    ;
  
  /* Return results. */
  *cpp = cp;
  *bitsp = bits;
  return 1;
}

/* Tries to match the host name (which must be in all lowercase) against the
   comma-separated sequence of subpatterns (each possibly preceded by ! to 
   indicate negation).  Returns true if there is a positive match; zero
   otherwise. */

int match_hostname(const char *host, const char *ip,
                   const char *pattern, unsigned int len)
{
  char sub[1024];
  int negated;
  int got_positive;
  unsigned int i, subi;

  got_positive = 0;
  for (i = 0; i < len;)
    {
      /* Check if the subpattern is negated. */
      if (pattern[i] == '!')
        {
          negated = 1;
          i++;
        }
      else
        negated = 0;
      
      /* Extract the subpattern up to a comma or end.  Convert the subpattern
         to lowercase. */
      for (subi = 0; 
           i < len && subi < sizeof(sub) - 1 && pattern[i] != ',';
           subi++, i++)
        sub[subi] = isupper(pattern[i]) ? tolower(pattern[i]) : pattern[i];
      /* If subpattern too long, return failure (no match). */
      if (subi >= sizeof(sub) - 1)
        return 0;

      /* If the subpattern was terminated by a comma, skip the comma. */
      if (i < len && pattern[i] == ',')
        i++;
      
      /* Null-terminate the subpattern. */
      sub[subi] = '\0';

      /* Try to match the subpattern against the host name. */
      if (match_host(host, ip, sub))
        if (negated)
          return 0;  /* Fail if host matches any negated subpattern. */
        else
          got_positive = 1;
    }

  /* Return success if got a positive match.  If there was a negative match,
     we have already returned zero and never get here. */
  return got_positive;
}

/* Checks whether the given host (which must be in all lowercase) is 
   already in the list of our known hosts.
   Returns HOST_OK if the host is known and has the specified key,
   HOST_NEW if the host is not known, and HOST_CHANGED if the host is known
   but used to have a different host key. */

HostStatus check_host_in_hostfile(uid_t uid,
                                  const char *filename, 
                                  const char *host, unsigned int bits,
                                  MP_INT *e, MP_INT *n)
{
  UserFile uf;
  char line[16384];
  MP_INT ke, kn;
  unsigned int kbits, hostlen;
  char *cp, *cp2;
  HostStatus end_return;
  struct stat st;

  /* Open the file containing the list of known hosts. */
  uf = userfile_open(uid, filename, O_RDONLY, 0);
  if (uf == NULL)
    {
      if (userfile_stat(uid, filename, &st) >= 0)
        {
          packet_send_debug("Could not open %.900s for reading.", filename);
          packet_send_debug("If your home directory is on an NFS volume, it may need to be world-readable.");
        }
      return HOST_NEW;
    }

  /* Initialize mp-int variables. */
  mpz_init(&ke);
  mpz_init(&kn);
  
  /* Cache the length of the host name. */
  hostlen = strlen(host);
  
  /* Return value when the loop terminates.  This is set to HOST_CHANGED if
     we have seen a different key for the host and have not found the proper
     one. */
  end_return = HOST_NEW;

  /* Go trough the file. */
  while (userfile_gets(line, sizeof(line), uf))
    {
      cp = line;

      /* Skip any leading whitespace. */
      for (; *cp == ' ' || *cp == '\t'; cp++)
        ;

      /* Ignore comment lines and empty lines. */
      if (!*cp || *cp == '#' || *cp == '\n')
        continue;
      
      /* Find the end of the host name portion. */
      for (cp2 = cp; *cp2 && *cp2 != ' ' && *cp2 != '\t'; cp2++)
        ;

      /* Check if the host name matches. */
      if (!match_hostname(host, NULL, cp, (unsigned int)(cp2 - cp)))
        continue;
      
      /* Got a match.  Skip host name. */
      cp = cp2;
      
      /* Extract the key from the line.  This will skip any leading 
         whitespace.  Ignore badly formatted lines. */
      if (!auth_rsa_read_key(&cp, &kbits, &ke, &kn))
        continue;

      /* Check if the current key is the same as the previous one. */
      if (kbits == bits && mpz_cmp(&ke, e) == 0 && mpz_cmp(&kn, n) == 0)
        {
          /* Ok, they match. */
          mpz_clear(&ke);
          mpz_clear(&kn);
          userfile_close(uf);
          return HOST_OK;
        }
      
      /* They do not match.  We will continue to go through the file; however,
         we note that we will not return that it is new. */
      end_return = HOST_CHANGED;
    }
  /* Clear variables and close the file. */
  mpz_clear(&ke);
  mpz_clear(&kn);
  userfile_close(uf);

  /* Return either HOST_NEW or HOST_CHANGED, depending on whether we saw a
     different key for the host. */
  return end_return;
}

/* Appends an entry to the host file.  Returns false if the entry
   could not be appended.  All I/O will be done with the give uid using
   userfile. */

int add_host_to_hostfile(uid_t uid, const char *filename, const char *host,
                         unsigned int bits, MP_INT *e, MP_INT *n)
{
  UserFile uf;
  char buf[1000], *cp;
 
  /* Open the file for appending. */
  uf = userfile_open(uid, filename, O_WRONLY|O_APPEND|O_CREAT, 0600);
  if (uf == NULL)
    return 0;

  /* Print the host name and key to the file. */
  snprintf(buf, sizeof(buf), "%.500s %u ", host, bits);
  userfile_write(uf, buf, strlen(buf));
  
  cp = xmalloc(mpz_sizeinbase(e, 10) + 2);
  mpz_get_str(cp, 10, e);
  userfile_write(uf, cp, strlen(cp));
  xfree(cp);

  userfile_write(uf, " ", 1);

  cp = xmalloc(mpz_sizeinbase(n, 10) + 2);
  mpz_get_str(cp, 10, n);
  userfile_write(uf, cp, strlen(cp));
  xfree(cp);

  userfile_write(uf, "\n", 1);

  /* Close the file. */
  userfile_close(uf);
  return 1;
}
