/*

  authc-hostbased.c

  Author: Sami Lehtinen <sjl@ssh.com>

  Copyright (C) 1997-2000 SSH Communications Security Corp, Helsinki, Finland
  All rights reserved.
                  
  Hostbased authentication, client-side.

*/

#include "ssh2includes.h"
#include "sshauth.h"
#include "sshpacketstream.h"
#include "sshencode.h"
#include "sshmsgs.h"
#include "sshclient.h"
#include "sshunixpipestream.h"
#include "sshtcp.h"
#ifdef HAVE_SYS_PARAM_H
#include <sys/param.h>
#endif /* HAVE_SYS_PARAM_H */
#include "sshuserfiles.h"
#include "ssh2pubkeyencode.h"
#include "authc-hostbased.h"
#include "sshconfig.h"
#include "sshdsprintf.h"

#include "sshfsm.h"
#include "sshtimeouts.h"
#include "sshdsprintf.h"

#define SSH_DEBUG_MODULE "Ssh2AuthHostBasedClient"

typedef struct SshClientHostbasedAuthContextRec
{
  SshPacketWrapper wrapper;

  const unsigned char *session_id;
  size_t session_id_len;
  const char *user;
  char *pubkey_algorithm;
  unsigned char *pubkeyblob;
  size_t pubkeyblob_len;
  const char *local_user_name;
  char *local_host_name;
  SshAuthClientCompletionProc completion;
  void *completion_context;
  void **state_placeholder;
  
  unsigned char *packet;
  size_t packet_len;
  SshPacketType packet_type;
  Boolean packet_pending;
  
  SshConfig server_conf;

  SshTransportCompat compat_flags;
  
  SshFSM fsm;
  SshFSMThread main_thread;
  
} *SshClientHostbasedAuth;

/* Forward declarations. */
void auth_hostbased_can_send(void *context);

SSH_FSM_STEP(hostbased_suspend)
{
  return SSH_FSM_SUSPENDED;
}

SSH_FSM_STEP(hostbased_send_compat_flags)
{
  char *compat_flag_string = NULL, *to_be_deleted;
  SSH_FSM_GDATA(SshClientHostbasedAuth);
  
  SSH_FSM_SET_NEXT("hostbased_send_packet");

  if (*gdata->compat_flags->hostbased_service_name_draft_incompatibility)
    { 
      to_be_deleted = compat_flag_string;
      ssh_dsprintf(&compat_flag_string, "%s%s%s",
                   compat_flag_string ? compat_flag_string : "",
                   compat_flag_string ? "," : "",
                   HOSTBASED_REQUESTED_SERVICE_DRAFT_INCOMPAT);
      ssh_xfree(to_be_deleted);
    }
  
  if (*gdata->compat_flags->malformed_signatures_draft_incompatibility)
    { 
      to_be_deleted = compat_flag_string;
      ssh_dsprintf(&compat_flag_string, "%s%s%s",
                   compat_flag_string ? compat_flag_string : "",
                   compat_flag_string ? "," : "",
                   SIGNATURE_ENCODE_DRAFT_INCOMPAT);
      ssh_xfree(to_be_deleted);
    }

  if (!compat_flag_string)
    {
      return SSH_FSM_CONTINUE;
    }
  else
    {
      SSH_TRACE(2, ("Compat flags: \"%s\"", compat_flag_string));

      gdata->packet_pending = TRUE;
      gdata->packet_type = SSH_AUTH_HOSTBASED_COMPAT;
  
      gdata->packet_len =
        ssh_encode_array_alloc(&gdata->packet,
                               SSH_FORMAT_UINT32_STR, compat_flag_string,
                               strlen(compat_flag_string),
                               SSH_FORMAT_END);
      
      ssh_xfree(compat_flag_string);

      if (ssh_packet_wrapper_can_send(gdata->wrapper))
        ssh_register_timeout(0L, 0L, auth_hostbased_can_send, gdata);

      return SSH_FSM_SUSPENDED;
    }

  SSH_NOTREACHED;
}

SSH_FSM_STEP(hostbased_process_error)
{
  char *error_message = NULL;
  unsigned int error_code = 0;
  size_t len;
  
  SSH_FSM_GDATA(SshClientHostbasedAuth);
  
  len = ssh_decode_array(gdata->packet, gdata->packet_len,
                         SSH_FORMAT_CHAR, &error_code,
                         SSH_FORMAT_UINT32_STR, &error_message, NULL,
                         SSH_FORMAT_UINT32_STR, NULL, NULL,
                         SSH_FORMAT_END);
  
  if (len == 0 || len != gdata->packet_len)
    {
      ssh_warning("Received malformed error packet from ssh-signer.");
    }
  else
    {
      ssh_warning("Received error %d, message: \"%s\".",
                  error_code, error_message);
    }

  ssh_packet_wrapper_can_receive(gdata->wrapper, FALSE);

  SSH_FSM_SET_NEXT("hostbased_finish");
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(hostbased_send_end)
{
  SSH_FSM_GDATA(SshClientHostbasedAuth);
  
  ssh_packet_wrapper_send_encode(gdata->wrapper,
                                 SSH_AUTH_HOSTBASED_END,
                                 SSH_FORMAT_END);
  
  if (gdata->wrapper)
    {
      ssh_packet_wrapper_destroy(gdata->wrapper);
      gdata->wrapper = NULL;
    }
  
  SSH_FSM_SET_NEXT("hostbased_finish");
  
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(hostbased_send_packet)
{
  char *service;
  SSH_FSM_GDATA(SshClientHostbasedAuth);
  
  SSH_FSM_SET_NEXT("hostbased_suspend");
  
  if (!(*gdata->compat_flags->hostbased_service_name_draft_incompatibility))
    service = SSH_CONNECTION_SERVICE;
  else
    service = SSH_USERAUTH_SERVICE;

  gdata->packet_pending = TRUE;
  gdata->packet_type = SSH_AUTH_HOSTBASED_PACKET;
  
  gdata->packet_len =
    ssh_encode_array_alloc(&gdata->packet,
                           /* session_id*/
                           SSH_FORMAT_UINT32_STR, gdata->session_id,
                           gdata->session_id_len,
                           /* SSH_MSG_USERAUTH_REQUEST */
                           SSH_FORMAT_CHAR,
                           (unsigned int) SSH_MSG_USERAUTH_REQUEST,
                           /* user name */
                           SSH_FORMAT_UINT32_STR, gdata->user,
                           strlen(gdata->user),
                           /* service */
                           SSH_FORMAT_UINT32_STR,
                           service, strlen(service),
                           /* "hostbased" */
                           SSH_FORMAT_UINT32_STR,
                           SSH_AUTH_HOSTBASED,
                           strlen(SSH_AUTH_HOSTBASED),
                           /* public key algorithm (string) */
                           SSH_FORMAT_UINT32_STR,
                           gdata->pubkey_algorithm,
                           strlen(gdata->pubkey_algorithm),
                           /* public key (string) */
                           SSH_FORMAT_UINT32_STR, gdata->pubkeyblob,
                           gdata->pubkeyblob_len,
                           /* client host name (FQDN, string) */
                           SSH_FORMAT_UINT32_STR, gdata->local_host_name,
                           strlen(gdata->local_host_name),
                           /* user name at client side */
                           SSH_FORMAT_UINT32_STR, gdata->local_user_name,
                           strlen(gdata->local_user_name),
                           SSH_FORMAT_END);

  SSH_DEBUG(3, ("Sending packet..."));
  
  if (ssh_packet_wrapper_can_send(gdata->wrapper))
    ssh_register_timeout(0L, 0L, auth_hostbased_can_send, gdata);
  
  return SSH_FSM_SUSPENDED;
}

SSH_FSM_STEP(hostbased_process_signature)
{
  SshBuffer buffer;
  SSH_FSM_GDATA(SshClientHostbasedAuth);
  
  /* We've got a signature. */
  buffer = ssh_buffer_allocate();
  
  ssh_encode_buffer(buffer,
                    /* public key algorithm (string) */
                    SSH_FORMAT_UINT32_STR,
                    gdata->pubkey_algorithm,
                    strlen(gdata->pubkey_algorithm),
                    /* public key (string) */
                    SSH_FORMAT_UINT32_STR, gdata->pubkeyblob,
                    gdata->pubkeyblob_len,
                    /* client host name (FQDN, string) */
                    SSH_FORMAT_UINT32_STR, gdata->local_host_name,
                    strlen(gdata->local_host_name),
                    /* user name at client side */
                    SSH_FORMAT_UINT32_STR, gdata->local_user_name,
                    strlen(gdata->local_user_name),
                    /* signature */
                    SSH_FORMAT_UINT32_STR, gdata->packet, gdata->packet_len,
                    SSH_FORMAT_END);

  /* Call the authentication method completion procedure. */
  *gdata->state_placeholder = NULL;
  (*gdata->completion)(SSH_AUTH_CLIENT_SEND, gdata->user, buffer,
                       gdata->completion_context);
  
  /* Free the buffer */
  ssh_buffer_free(buffer);

  SSH_FSM_SET_NEXT("hostbased_send_end");
  
  return SSH_FSM_CONTINUE;  
}

SSH_FSM_STEP(hostbased_finish)
{
  SSH_FSM_GDATA(SshClientHostbasedAuth);

  if (gdata->wrapper)
    ssh_packet_wrapper_can_receive(gdata->wrapper, FALSE);

  return SSH_FSM_FINISH;
}

/* Callback, which is used to notify that our packetstream has a
   packet for us.*/
void auth_hostbased_received_packet(SshPacketType type,
                                    const unsigned char *packet,
                                    size_t packet_len,
                                    void *context)
{
  SshClientHostbasedAuth state = (SshClientHostbasedAuth) context;

  ssh_packet_wrapper_can_receive(state->wrapper, FALSE);
  
  switch (type)
    {
    case SSH_AUTH_HOSTBASED_PACKET:
      /* signer shouldn't send this to us, so this is an error.*/
      SSH_TRACE(2, ("ssh-signer returned SSH_AUTH_HOSTBASED_PACKET "\
                    "(this is an error)"));
      goto send_error_to_server;
      break;
    case SSH_AUTH_HOSTBASED_COMPAT:
      /* signer shouldn't send this to us, so this is an error.*/
      SSH_TRACE(2, ("ssh-signer returned SSH_AUTH_HOSTBASED_COMPAT "\
                    "(this is an error)"));
      goto send_error_to_server;
      break;      
    case SSH_AUTH_HOSTBASED_END:
      /* signer shouldn't send this to us, so this is an error.*/
      SSH_TRACE(2, ("ssh-signer returned SSH_AUTH_HOSTBASED_END "\
                    "(this is an error)"));
    send_error_to_server:
      ssh_warning("Error communicating with ssh-signer.");

      ssh_fsm_set_next(state->main_thread, "hostbased_finish");
      ssh_fsm_continue(state->main_thread);
      
      *state->state_placeholder = NULL;
      (*state->completion)(SSH_AUTH_CLIENT_FAIL_AND_DISABLE_METHOD,
                           state->user, NULL,
                           state->completion_context);
      return;
    case SSH_AUTH_HOSTBASED_SIGNATURE:
      SSH_TRACE(2, ("ssh-signer returned SSH_AUTH_HOSTBASED_SIGNATURE"));

      ssh_fsm_set_next(state->main_thread, "hostbased_process_signature");
      ssh_fsm_continue(state->main_thread);
      break;
    case SSH_AUTH_HOSTBASED_ERROR:
      SSH_TRACE(0, ("ssh-signer returned SSH_AUTH_HOSTBASED_ERROR"));
      /* Send failure message to server, and return. */
      *state->state_placeholder = NULL;
      (*state->completion)(SSH_AUTH_CLIENT_FAIL_AND_DISABLE_METHOD,
                           state->user, NULL,
                           state->completion_context);

      ssh_fsm_set_next(state->main_thread, "hostbased_process_error");
      ssh_fsm_continue(state->main_thread);
      break;
    }
  if (state->packet)
    ssh_xfree(state->packet);
  
  state->packet = ssh_xmemdup(packet, packet_len);
  state->packet_len = packet_len;
}

/* Callback, which notifies that packetstream has received EOF from
   the other side. */
void auth_hostbased_received_eof(void *context)
{
  SshClientHostbasedAuth state = (SshClientHostbasedAuth) context;
  
  SSH_TRACE(0, ("received EOF from ssh-signer2."));
  
  ssh_packet_wrapper_send_eof(state->wrapper);

  /* Send failure message up, and return. */
  *state->state_placeholder = NULL;
  (*state->completion)(SSH_AUTH_CLIENT_FAIL_AND_DISABLE_METHOD,
                       state->user, NULL,
                       state->completion_context);

  ssh_fsm_set_next(state->main_thread, "hostbased_finish");
  ssh_fsm_continue(state->main_thread);
}

/* Callback, which notifies that packetstream is ready for sending. */
void auth_hostbased_can_send(void *context)
{
  SshClientHostbasedAuth state = (SshClientHostbasedAuth)context;

  if (!state->packet_pending)
    return;

  state->packet_pending = FALSE;
  
  SSH_PRECOND(state);
  SSH_PRECOND(state->packet_type);
  SSH_PRECOND(state->packet);
  SSH_PRECOND(state->packet_len);
    
  ssh_packet_wrapper_send(state->wrapper,
                          state->packet_type,
                          state->packet,
                          state->packet_len);

  state->packet_type = (SshPacketType)0;
  ssh_xfree(state->packet);
  state->packet = NULL;
  state->packet_len = 0L;

  ssh_packet_wrapper_can_receive(state->wrapper, TRUE);
  
  ssh_fsm_continue(state->main_thread);  
}

void hostbased_destructor(void *gdata)
{
  SshClientHostbasedAuth state = (SshClientHostbasedAuth)gdata;
  
  if (state->wrapper)
    {
      ssh_packet_wrapper_destroy(state->wrapper);
      state->wrapper = NULL;
    }
  
  ssh_xfree(state->pubkey_algorithm);
  ssh_xfree(state->pubkeyblob);
  ssh_xfree(state->local_host_name);

  ssh_xfree(state->packet);
  ssh_config_free(state->server_conf);

  memset(state, 'F', sizeof(*state));
}

SshFSMStateMapItem hostbased_states[] =
{
  { "hostbased_suspend", "Suspend thread", hostbased_suspend },
  
  { "hostbased_send_compat_flags", "Send compatibility flags",
    hostbased_send_compat_flags },
  { "hostbased_process_error", "Process received error packet",
    hostbased_process_error },  
  { "hostbased_process_signature", "Process received signature",
    hostbased_process_signature },  
  { "hostbased_send_end", "Send END packet",
    hostbased_send_end },
  
  { "hostbased_send_packet", "Send hostbased-authentication packet",
    hostbased_send_packet },

  { "hostbased_finish", "We're ready", hostbased_finish }
};

void ssh_client_auth_hostbased(SshAuthClientOperation op,
                               const char *user,
                               unsigned int packet_type,
                               SshBuffer packet_in,
                               const unsigned char *session_id,
                               size_t session_id_len,
                               void **state_placeholder,
                               SshAuthClientCompletionProc completion,
                               void *completion_context,
                               void *method_context)
{
  SshClientHostbasedAuth state;
  SshClient client;
  SshStream stdio_stream;
  char hostkeyfile[512];
  /*  char *keytype;
  SshPublicKey pubkey;*/
  char **signer_argv;
  char config_filename[512];
  size_t hostname_len;
  SshFSM fsm;
  
  SSH_DEBUG(6, ("auth_hostbased op = %d  user = %s", op, user));

  client = (SshClient)method_context;
  state = *state_placeholder;

  switch (op)
    {
      /* This operation is always non-interactive, as hostkeys
         shouldn't have passphrases. Check for it, though. XXX */
    case SSH_AUTH_CLIENT_OP_START_NONINTERACTIVE:
      /* XXX There is a bug in sshauthc.c (or
         elsewhere). Authentication methods, that are not allowed,
         should not be tried. Now it calls
         SSH_AUTH_CLIENT_OP_START_NONINTERACTIVE for every
         authentication method before checking.*/
      (*completion)(SSH_AUTH_CLIENT_FAIL, user, NULL, completion_context);
      break;
      
    case SSH_AUTH_CLIENT_OP_START:
      /* This is the first operation for doing hostbased authentication.
         We should not have any previous saved state when we come here. */
      SSH_ASSERT(*state_placeholder == NULL);

      fsm = ssh_fsm_allocate(sizeof(*state),
                             hostbased_states,
                             SSH_FSM_NUM_STATES(hostbased_states),
                             hostbased_destructor);

      state = ssh_fsm_get_gdata_fsm(fsm);

      memset(state, 0, sizeof(*state));
      
      state->fsm = fsm;
      
      state->session_id = session_id;
      state->session_id_len = session_id_len;
      state->user = ssh_xstrdup(user);
      state->state_placeholder = state_placeholder;

      state->compat_flags = client->common->compat_flags;
      
      /* We have to dig up the server configuration to get the place
         for the client host's publickey. This is a very kludgeish
         solution. XXX*/
      state->server_conf = ssh_server_create_config();
      
      /* Dig up hosts publickey. */
      snprintf(config_filename, sizeof(config_filename), "%s/%s",
               SSH_SERVER_DIR, SSH_SERVER_CONFIG_FILE);
      
      if (!ssh_config_read_file(client->user_data, state->server_conf,
                                NULL, config_filename, NULL))
        SSH_TRACE(2, ("Failed to read config file %s", \
                      config_filename));


      if(state->server_conf->public_host_key_file[0] != '/')
        {
          snprintf(hostkeyfile, sizeof(hostkeyfile), "%s/%s", SSH_SERVER_DIR,
                   state->server_conf->public_host_key_file);
        }
      else
        {
          snprintf(hostkeyfile, sizeof(hostkeyfile), "%s",
                   state->server_conf->public_host_key_file);  
        }

      /* This pubkey*-stuff is for the client _host's_ public
         hostkey. */
      SSH_DEBUG(4, ("Reading pubkey-blob from %s...", hostkeyfile));
      if (ssh2_key_blob_read(client->user_data, hostkeyfile, TRUE, NULL,
                             &state->pubkeyblob,
                             &state->pubkeyblob_len, NULL) 
          != SSH_KEY_MAGIC_PUBLIC)
        {         
          goto error;
        }
      
      SSH_DEBUG(4, ("done."));
      if ((state->pubkey_algorithm =
           ssh_pubkeyblob_type(state->pubkeyblob,
                               state->pubkeyblob_len))
          == NULL)
        {
          goto error;
        }
      
      state->local_user_name = ssh_user_name(client->user_data);
      state->local_host_name = ssh_xcalloc(1, MAXHOSTNAMELEN + 1);
      ssh_tcp_get_host_name(state->local_host_name, MAXHOSTNAMELEN + 1);
      hostname_len = strlen(state->local_host_name);
      /* Check for cases where the hostname doesn't contain "." */
      if (!strchr(state->local_host_name, '.'))
        {
          char *temp_name;
          if (!client->config->default_domain)
            {
              ssh_warning("Hostbased authentication is DISABLED.");
              ssh_warning("Hostname \"%s\" is not a fully qualified domain name (FQDN),", state->local_host_name);
              ssh_warning("and DefaultDomain configuration parameter is not set. Ask your ");
              ssh_warning("sysadmin to set it to the systemwide configuration file.");
              ssh_warning("(which is %s .)", SSH_CLIENT_GLOBAL_CONFIG_FILE);
              goto error;
            }

          ssh_dsprintf(&temp_name, "%s.%s", state->local_host_name,
                       client->config->default_domain);
          ssh_xfree(state->local_host_name);
          state->local_host_name = temp_name;
          hostname_len = strlen(state->local_host_name);
        }
      
      /* Sanity check */
      SSH_ASSERT(hostname_len + 2 < MAXHOSTNAMELEN);
      /* We want FQDN. */
      state->local_host_name[hostname_len] = '.';
      state->local_host_name[hostname_len + 1] = '\0';
      
      state->completion = completion;
      state->completion_context = completion_context;
      
      /* Assign the state to the placeholder that survives across
         calls.  (this is needed only for detecting error conditions.)  */
      *state_placeholder = state;

      /* Open a pipestream connection to ssh-signer. */
      switch (ssh_pipe_create_and_fork(&stdio_stream, NULL))
        {
        case SSH_PIPE_ERROR:
          /* Something went wrong. */
          ssh_warning("Couldn't create pipe to connect to %s.",
                      client->config->signer_path);
          goto error;
          break;
        case SSH_PIPE_CHILD_OK:
          /* Exec ssh-signer */
          SSH_TRACE(0, ("Child: Execing ssh-signer...(path: %s)", \
                        client->config->signer_path));

          signer_argv = ssh_xcalloc(2, sizeof(char *));
          signer_argv[0] = client->config->signer_path;
          signer_argv[1] = NULL;
          
          execvp(client->config->signer_path, signer_argv);
          fprintf(stderr, "Couldn't exec '%s' (System error message: %s)",
                  client->config->signer_path, strerror(errno));
          ssh_fatal("Executing ssh-signer failed.");
          break;
        case SSH_PIPE_PARENT_OK:
          state->wrapper = ssh_packet_wrap(stdio_stream,
                                           auth_hostbased_received_packet,
                                           auth_hostbased_received_eof,
                                           auth_hostbased_can_send,
                                           state);
          /* We don't check wrapper's validity, as ssh_packet_wrap
             should always succeed.*/
          break;
        }

      ssh_packet_wrapper_can_receive(state->wrapper, FALSE);

      /* Here we continue as parent. */
      state->main_thread = ssh_fsm_spawn(state->fsm,
                                         0, "hostbased_send_compat_flags",
                                         NULL, NULL);
      
      /* Rest is done in callbacks. */
      break;
      
    case SSH_AUTH_CLIENT_OP_CONTINUE:          
      SSH_TRACE(2, ("Invalid message. We didn't return " \
                    "SSH_AUTH_CLIENT_SEND_AND_CONTINUE at any stage!"));
      /* Send failure message.*/
      (*completion)(SSH_AUTH_CLIENT_FAIL_AND_DISABLE_METHOD,
                    user, NULL, completion_context);

      if (state && state->main_thread)
        {
          ssh_fsm_set_next(state->main_thread, "hostbased_finish");
          ssh_fsm_continue(state->main_thread);
        }
      
      return;

    case SSH_AUTH_CLIENT_OP_ABORT:
      /* Abort the authentication operation immediately. */
      if (state && state->main_thread)
        {
          ssh_fsm_set_next(state->main_thread, "hostbased_finish");
          ssh_fsm_continue(state->main_thread);
        }
      *state_placeholder = NULL;
      break;

    default:
      /* something weird is going on.. */
      ssh_fatal("ssh_client_auth_hostbased: unknown op %d", (int)op);
    }
  return;

 error:
  /* Destroy state. */
  ssh_fsm_destroy(fsm);
  *state_placeholder = NULL;
  
  /* Send failure message.*/
  (*completion)(SSH_AUTH_CLIENT_FAIL_AND_DISABLE_METHOD, user,
                NULL, completion_context);
  return;
}
