/*

  auths-common.c

  Author: Sami Lehtinen <sjl@ssh.com>

  Copyright (C) 1997-2000 SSH Communications Security Corp, Helsinki, Finland
  All rights reserved.
                  
  Common functions for both pubkey- and password-authentication on the
  server side.

*/

#include "sshincludes.h"
#include "sshuser.h"
#include "auths-common.h"
#include "sshcommon.h"
#include "sshmatch.h"
#include "sshdsprintf.h"

#define SSH_DEBUG_MODULE "Ssh2AuthCommonServer"

/* Returns FALSE if user&host are allowed to connect and authenticate. */
Boolean ssh_server_auth_check(SshUser *ucp, const char *user, SshConfig config,
                              SshCommon common, char *auth_method_name)
{
  if (ssh_server_auth_check_user(ucp, user, common))
    {
      /* failure */
      char *s = "%s authentication failed. Login to account %.100s not "
        "allowed or account non-existent.";
      
      ssh_log_event(config->log_facility,
                    SSH_LOG_WARNING,
                    s,
                    auth_method_name, user);
      SSH_TRACE(1, (s, auth_method_name, user));
      /* Reject the login if the user is not allowed to log in. */
      return TRUE;
    }
  
  /* Check whether logins from remote host are allowed */
  if (ssh_server_auth_check_host(common))
    {
      /* failure */
      char *s;
      s = "%s authentication failed. Connection from %.100s denied. "
        "Authentication as user %.100s was attempted.";
      
      /* logins from remote host are not allowed. */
      ssh_log_event(config->log_facility, SSH_LOG_WARNING,
                    s, auth_method_name,
                    common->remote_host, ssh_user_name(*ucp));
      SSH_TRACE(1, (s, auth_method_name, common->remote_host,   \
                    ssh_user_name(*ucp)));
      
      /* Reject the login if the user is not allowed to log in from
         specified host. */
      return TRUE;
    }
  return FALSE;
}

/* Match a given 'string' with the given 'pattern' using
   SshRegex. Return 'TRUE' if a match is found, 'FALSE' otherwise. */
Boolean match_string(const char *string, const char *pattern,
                     SshRegexContext rex_ctx)
{
  SshRegexMatcher regex_matcher;
  Boolean match = FALSE;
  
  SSH_PRECOND(string);
  SSH_PRECOND(pattern);
  
  regex_matcher = ssh_regex_create(rex_ctx,
                                   pattern,
                                   SSH_REGEX_SYNTAX_ZSH_FILEGLOB);
  if (!regex_matcher)
    {
      /* Failure to create matcher. */
      ssh_warning("Pattern \"%s\" is invalid.", pattern);

      return FALSE;
    }
  
  match = ssh_regex_match_cstr(regex_matcher, string);  
  SSH_TRACE(5, ("match: %s", ssh_regex_get_submatch
                (regex_matcher, 0)));
  
  if (match)
    match = (strcmp(string, ssh_regex_get_submatch
                    (regex_matcher, 0)) ? FALSE : TRUE);
  
  ssh_regex_free(regex_matcher);

  return match;  
}

/* Function to check whether given 'string' matches with a
   pattern in 'list'. Uses egrep-like-syntax. Returns TRUE if a match
   is found, and FALSE otherwise. Skips NULL list-entries. */
Boolean ssh_match_string_in_list(const char *string, SshDlList list,
                                 SshRegexContext rex_ctx)
{
  char *current;
  Boolean ret = FALSE;
  
  ssh_dllist_rewind(list);
  
  do {
    SshRegexMatcher regex_matcher;
    Boolean match;
    
    if ((current = ssh_dllist_current(list)) ==
        NULL)
      continue;

    regex_matcher = ssh_regex_create(rex_ctx,
                                     current,
                                     SSH_REGEX_SYNTAX_ZSH_FILEGLOB);
    if (!regex_matcher)
      {
        /* Failure to create matcher. */
        ssh_warning("Pattern \"%s\" is invalid, and is ignored.", current);
        
        continue;
      }

    match = ssh_regex_match_cstr(regex_matcher, string);  
    ssh_debug("match: %s", ssh_regex_get_submatch
              (regex_matcher, 0));
    
    if (match)
      match = (strcmp(string, ssh_regex_get_submatch
                      (regex_matcher, 0)) ? FALSE : TRUE);
                      
    ssh_regex_free(regex_matcher);

    if (match)
      {
        ret = TRUE;
        break;
      }
  } while (ssh_dllist_fw(list, 1) ==
           SSH_DLLIST_OK);
  
  return ret;
}

/* Helper function to check whether given 'string' or 'number' matches
   with a pattern in 'list'. Uses egrep-like-syntax. Returns FALSE if a
   match is found, and TRUE otherwise. Skips NULL list-entries. */
Boolean ssh_match_string_or_number_in_list(const char *string,
                                           unsigned long number,
                                           SshDlList list,
                                           SshRegexContext rex_ctx)
{
  char *num_str;
  Boolean ret;
  
  if (ssh_match_string_in_list(string, list, rex_ctx))
    return FALSE;
  
  ssh_dsprintf(&num_str, "%lu", number);

  ret = ssh_match_string_in_list(num_str, list, rex_ctx);
  
  ssh_xfree(num_str);
  
  return ret ? FALSE : TRUE;
}

/* Helper function to check whether user (as user name, uid, remote
   host name and remote ip (as string)) matches with a pattern in
   'list'. Uses egrep-like-syntax. Returns TRUE if a match is found,
   and FALSE otherwise. Skips NULL list-entries. */
Boolean ssh_match_user_in_list(const char *user, uid_t uid, 
                               const char *remote_host, const char *remote_ip,
                               SshDlList list,
                               SshRegexContext rex_ctx)
{
  char *current, *copy, *uid_str, *host_pat;
  Boolean match = FALSE;

  ssh_dsprintf(&uid_str, "%lu", uid);
  
  ssh_dllist_rewind(list);
  
  do {    
    if ((current = ssh_dllist_current(list)) ==
        NULL)
      continue;    

    copy = ssh_xstrdup(current);
    
    if ((host_pat = strchr(copy, '@')) != NULL)
      {
        SSH_TRACE(6, ("'%s' is a user@host pattern.", copy));
        *host_pat = '\0';
        host_pat++;
      }

    if (match_string(user, copy, rex_ctx))
      {
        SSH_TRACE(5, ("Found match with '%s' and '%s'.", user, copy));
      }
    else if (!match_string(uid_str, copy, rex_ctx))
      {
        SSH_TRACE(5, ("User '%s', uid %lu didn't match with '%s'.",
                      user, uid, copy));
        match = FALSE;
        ssh_xfree(copy);
        continue;
      }
    else
      {
        SSH_TRACE(5, ("Found match with '%s' and '%s'.",
                      uid_str, copy));
      }
    
    match = TRUE;
    
    if (host_pat)
      {
        if (match_string(remote_ip, host_pat, rex_ctx))
          {
            SSH_TRACE(5, ("Found match with '%s' and '%s'.", remote_ip, host_pat));
          }
        else if (!match_string(remote_host, host_pat, rex_ctx))
          {
            SSH_TRACE(5, ("host '%s', ip '%s' didn't match with '%s'.",
                          remote_host, remote_ip, host_pat));
            match = FALSE;
            ssh_xfree(copy);
            continue;
          }
        else
          {
            SSH_TRACE(5, ("Found match with '%s' and '%s'.",
                          remote_host, host_pat));
          }
      }

    ssh_xfree(copy);
    SSH_ASSERT(match);
    break;
  } while (ssh_dllist_fw(list, 1) ==
           SSH_DLLIST_OK);
  

  ssh_xfree(uid_str);
  
  return match;
}

/* returns FALSE on success. */
Boolean ssh_server_auth_check_user(SshUser *ucp, const char *user,
                                   SshCommon common)
{
  SshUser uc = *ucp;
  uid_t uid;
  gid_t gid;
  const char *group;
  Boolean ret = FALSE;
  SshRegexContext rex_ctx = NULL;
  SshConfig config = common->config;

  SSH_PRECOND(common);
  SSH_PRECOND(config);
  
  /* If user context not yet allocated, do it now. */
  if (uc == NULL)
    {
      uc = ssh_user_initialize(user, TRUE);
      if (!uc)
        {
          return TRUE;
        }       
      *ucp = uc;
    }

  /* Reject the login if the user is not allowed to log in. */
  if (!ssh_user_login_is_allowed(uc))
    {
      return TRUE;
    }

  uid = ssh_user_uid(uc);
  gid = ssh_user_gid(uc);
  group = ssh_group_name(uc);
  rex_ctx = ssh_regex_create_context();
  
  if (config->denied_users)
    if (ssh_match_user_in_list(user, uid, common->remote_host,
                               common->remote_ip, config->denied_users,
                               rex_ctx))
      {
        ret = TRUE;
        goto free_and_return;
      }
  
  if (config->allowed_users)
    {
      ret = TRUE;
      if (ssh_match_user_in_list(user, uid, common->remote_host,
                                 common->remote_ip, config->allowed_users,
                                 rex_ctx))
        ret = FALSE;

      goto free_and_return;
    }
  
  if (config->denied_groups)
    if (!ssh_match_string_or_number_in_list(group, gid, config->denied_groups,
                                            rex_ctx))
      {
        ret = TRUE;
        goto free_and_return;
      }
  
  if (config->allowed_groups)
    {
      ret = TRUE;
      if (!ssh_match_string_or_number_in_list(group, gid,
                                              config->allowed_groups,
                                              rex_ctx))
        ret = FALSE;
      
      goto free_and_return;
    }

 free_and_return:
  ssh_regex_free_context(rex_ctx);
  return ret;
}

/* Helper function to check whether given host name or ip-address
   matches a specified pattern. Returns FALSE if a match is found, and
   TRUE otherwise. */
Boolean match_host_id(char *host_name, char *host_ip, char *pattern,
                      SshRegexContext rex_ctx)
{
  SSH_PRECOND(host_name);
  SSH_PRECOND(host_ip);
  SSH_PRECOND(pattern);

  if (match_string(host_ip, pattern, rex_ctx))
    return FALSE;
  else
    return !match_string(host_name, pattern, rex_ctx);
}

/* Helper function to check whether given host name or ip-address
   is found in list. Returns FALSE if a match is found, and
   TRUE otherwise. */
Boolean ssh_match_host_in_list(char *host_name, char *host_ip, SshDlList list)
{
  int i = 0, length = 0;
  Boolean ret = TRUE;
  SshRegexContext rex_ctx = NULL;  

  rex_ctx = ssh_regex_create_context();

  for (ssh_dllist_rewind(list), i = 0,
         length = ssh_dllist_length(list);
       i < length; ssh_dllist_fw(list, 1), i++)
    if (!match_host_id(host_name, host_ip,
                       (char *)ssh_dllist_current(list), rex_ctx))
      {
        ret = FALSE;
        break;
      }
  
  ssh_regex_free_context(rex_ctx);
  
  return ret;
}

/* This is the function for checking a host{name,ip} against
   {Allow,Deny}Hosts parameters. Also checks remote host against
   statements in the AllowDenyHostsFile. Returns FALSE if connection
   from host is allowed, TRUE otherwise. */
Boolean ssh_server_auth_check_host(SshCommon common)
{
  SshConfig config = common->config;
  
  /* XXX AllowDenyHostsFile */
  /* XXX subnet masks "130.240.0.0/16" */
  /* XXX address ranges "130.240.20.15-130.240.21.76" */


  /* wildcards use SSH_REGEX_SYNTAX_ZSH_FILEGLOB */

  /* Check whether host is denied. Use ssh1-style policy, ie. if host
     is in DenyHosts, connection is denied even if the same host
     matches in AllowHosts.*/
  if (config->denied_hosts)
    {
      if (!ssh_match_host_in_list(common->remote_host, common->remote_ip,
                                  config->denied_hosts))
        return TRUE;
    }

  if (config->allowed_hosts)
    {
      if (!ssh_match_host_in_list(common->remote_host, common->remote_ip,
                                  config->allowed_hosts))
        return FALSE;
      return TRUE;
    }
  
  
  /* RequireReverseMapping */
  if (config->require_reverse_mapping)
    {
      if (strcmp(common->remote_host, common->remote_ip) == 0)
        {
          /* If remote host's ip-address couldn't be mapped to a
             hostname and RequireReverseMapping = 'yes', deny
             connection.*/
          return TRUE;
        }
    }
  
  return FALSE;
}
