components/proftpd/mod_solaris_audit.c
author William.D.Johnston <William.D.Johnston@oracle.com>
Tue, 14 Jul 2015 12:09:08 -0700
branchs11-update
changeset 4646 2bb9a036a5f2
parent 598 398722c80922
permissions -rw-r--r--
20886490 proftpd can't open wtmpx after one login failure, doesn't register the ftp login 20717794 proftpd changes group ownership of file /etc/shadow after user login failure 19318572 root logins can't get all privilege when Solaris PrivilegeEngine enabled

/*
 * ProFTPD - FTP server daemon
 * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
 *
 * 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.
 *
 * As a special exemption, copyright holders give permission to link
 * this program with OpenSSL, and distribute the resulting executable,
 * without including the source code for OpenSSL in the source distribution.
 *
 */

#include "conf.h"
#include <bsm/adt.h>
#include <bsm/adt_event.h>
#include <security/pam_appl.h>
#include <sys/types.h>
#include <pwd.h>
#include <unistd.h>
#include <ucred.h>

#ifndef ADT_ftpd
#define ADT_ftpd	152
#endif

#ifndef ADT_ftpd_logout
#define ADT_ftpd_logout	153
#endif

module solaris_audit_module;

static adt_session_data_t *asession = NULL;

static int auth_retval = PAM_AUTH_ERR;

static void audit_autherr_ev(const void *event_data, void *user_data) {

  switch (*(int *)event_data) {
  case PR_AUTH_NOPWD:
    auth_retval = PAM_USER_UNKNOWN;
    break;
  case PR_AUTH_AGEPWD:
    auth_retval = PAM_CRED_EXPIRED;
    break;
  case PR_AUTH_DISABLEDPWD:
    auth_retval = PAM_ACCT_EXPIRED;
    break;
  case PR_AUTH_CRED_INSUFF:
    auth_retval = PAM_CRED_INSUFFICIENT;
    break;
  case PR_AUTH_CRED_UNAVAIL:
    auth_retval = PAM_CRED_UNAVAIL;
    break;
  case PR_AUTH_CRED_ERR:
    auth_retval = PAM_CRED_ERR;
    break;
  case PR_AUTH_UNAVAIL:
    auth_retval = PAM_AUTHINFO_UNAVAIL;
    break;
  case PR_AUTH_MAXTRIES:
    auth_retval = PAM_MAXTRIES;
    break;
  case PR_AUTH_INIT_FAIL:
    auth_retval = PAM_SESSION_ERR;
    break;
  case PR_AUTH_NEWTOK:
    auth_retval = PAM_NEW_AUTHTOK_REQD;
    break;
  case PR_AUTH_OPEN_ERR:
    auth_retval = PAM_OPEN_ERR;
    break;
  case PR_AUTH_SYMBOL_ERR:
    auth_retval = PAM_SYMBOL_ERR;
    break;
  case PR_AUTH_SERVICE_ERR:
    auth_retval = PAM_SERVICE_ERR;
    break;
  case PR_AUTH_SYSTEM_ERR:
    auth_retval = PAM_SYSTEM_ERR;
    break;
  case PR_AUTH_BUF_ERR:
    auth_retval = PAM_BUF_ERR;
    break;
  case PR_AUTH_CONV_ERR:
    auth_retval = PAM_CONV_ERR;
    break;
  case PR_AUTH_PERM_DENIED:
    auth_retval = PAM_PERM_DENIED;
    break;
  default: /* PR_AUTH_BADPWD */
    auth_retval = PAM_AUTH_ERR;
    break;
  }

}

static void audit_failure(pool *p, char *authuser) {
  adt_event_data_t *event = NULL;
  const char *how;
  int saved_errno = 0;
  struct passwd pwd;
  char *pwdbuf = NULL;
  size_t pwdbuf_len;
  long pwdbuf_len_max;
  uid_t uid = ADT_NO_ATTRIB;
  gid_t gid = ADT_NO_ATTRIB;

  if ((pwdbuf_len_max = sysconf(_SC_GETPW_R_SIZE_MAX)) == -1) {
    saved_errno = errno;
    how = "couldn't determine maximum size of password buffer";
    goto fail;
  }

  pwdbuf_len = (size_t)pwdbuf_len_max;
  pwdbuf = pcalloc(p, pwdbuf_len);

  if (adt_start_session(&asession, NULL, ADT_USE_PROC_DATA) != 0) {
    saved_errno = errno;
    how = "couldn't start adt session";
    goto fail;
  }

  if ((authuser != NULL) && (authuser[0] != NULL) &&
    (getpwnam_r(authuser, &pwd, pwdbuf, pwdbuf_len) != NULL)) {
    uid = pwd.pw_uid;
    gid = pwd.pw_gid;
  } 

  if (adt_set_user(asession, uid, gid, uid, gid, NULL, ADT_NEW) != 0) {
    saved_errno = errno;
    how = "couldn't set adt user";
    goto fail;
  }

  if ((event = adt_alloc_event(asession, ADT_ftpd)) == NULL) {
    saved_errno = errno;
    how = "couldn't allocate adt event";
    goto fail;
  }

  if (adt_put_event(event, ADT_FAILURE, ADT_FAIL_PAM + auth_retval) != 0) {
    saved_errno = errno;
    how = "couldn't put adt event";
    goto fail;
  }

  adt_free_event(event);
  (void) adt_end_session(asession);
  asession = NULL;
  return;

fail:
  pr_log_pri(PR_LOG_ERR, "Auditing of login failed: %s (%s)", how,
    strerror(saved_errno));

  adt_free_event(event);
  (void) adt_end_session(asession);
  asession = NULL;
}

static void audit_success(void) {
  adt_event_data_t *event = NULL;
  const char *how;
  int saved_errno = 0;

  if (adt_start_session(&asession, NULL, ADT_USE_PROC_DATA) != 0) {
    saved_errno = errno;
    how = "couldn't start adt session";
    goto fail;
  }

  if ((event = adt_alloc_event(asession, ADT_ftpd)) == NULL) {
    saved_errno = errno;
    how = "couldn't allocate adt event";
    goto fail;
  }

  if (adt_put_event(event, ADT_SUCCESS, ADT_SUCCESS) != 0) {
    saved_errno = errno;
    how = "couldn't put adt event";
    goto fail;
  }

  adt_free_event(event);

  /* Don't end adt session - leave for when logging out. */
  return;

fail:
  pr_log_pri(PR_LOG_ERR, "Auditing of login failed: %s (%s)", how,
    strerror(saved_errno));

  adt_free_event(event);

  /* Don't end adt session - leave for when logging out. */

}

static void audit_logout(void) {
  adt_event_data_t *event = NULL;
  const char *how;
  int saved_errno = 0;

  /* If audit session was not created during login then leave */
  if (asession == NULL)
    return;

  if ((event = adt_alloc_event(asession, ADT_ftpd_logout)) == NULL) {
    saved_errno = errno;
    how = "couldn't allocate adt event";
    goto fail;
  }

  if (adt_put_event(event, ADT_SUCCESS, ADT_SUCCESS) != 0) {
    saved_errno = errno;
    how = "couldn't put adt event";
    goto fail;
  }

  adt_free_event(event);
  (void) adt_end_session(asession);
  asession = NULL;
  return;

fail:
  pr_log_pri(PR_LOG_ERR, "Auditing of logout failed: %s (%s)", how,
    strerror(saved_errno));

  adt_free_event(event);
  (void) adt_end_session(asession);
  asession = NULL;
}

/* Logout */
static void audit_exit_ev(const void *event_data, void *user_data) {
  audit_logout();
}

/* Login passed */
MODRET solaris_audit_post_pass(cmd_rec *cmd) {

  audit_success();

  /* Set handler for logout/timeout */
  pr_event_register(&solaris_audit_module, "core.exit", audit_exit_ev, NULL);

  return PR_DECLINED(cmd);
}

/* Login failed */
MODRET solaris_audit_post_fail(cmd_rec *cmd) {
  char *login_user;

  login_user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);

  audit_failure(cmd->tmp_pool, login_user);
  return PR_DECLINED(cmd);
}

static int audit_sess_init(void) {
  adt_session_data_t *aht;
  adt_termid_t *termid;
  priv_set_t *privset;
  int rval = -1;					

  /* add privs for audit init */
  if ((privset = priv_allocset()) == NULL) {
    pr_log_pri(PR_LOG_ERR, "Auditing privilege initialization failed");
    return rval;
  }

  (void) getppriv(PRIV_INHERITABLE, privset);
  priv_addset(privset, PRIV_PROC_AUDIT);
  (void) setppriv(PRIV_SET, PRIV_INHERITABLE, privset);

  (void) getppriv(PRIV_EFFECTIVE, privset);
  priv_addset(privset, PRIV_SYS_AUDIT);
  (void) setppriv(PRIV_SET, PRIV_EFFECTIVE, privset);

  /* basic terminal id setup */
  if (adt_start_session(&aht, NULL, 0) != 0) {
    pr_log_pri(PR_LOG_ERR, "pam adt_start_session: %s", strerror(errno));
    goto out;
  }
  if (adt_load_termid(session.c->rfd, &termid) != 0) {
    pr_log_pri(PR_LOG_ERR, "adt_load_termid: %s", strerror(errno));
    (void) adt_end_session(aht);
    goto out;
  }

  if (adt_set_user(aht, ADT_NO_AUDIT, ADT_NO_AUDIT, 0, ADT_NO_AUDIT, termid,
    ADT_SETTID) != 0) {
    pr_log_pri(PR_LOG_ERR, "adt_set_user: %", strerror(errno));
    free(termid);
    (void) adt_end_session(aht);
    goto out;
  }
  free(termid);
  if (adt_set_proc(aht) != 0) {
    pr_log_pri(PR_LOG_ERR, "adt_set_proc: %", strerror(errno));
    (void) adt_end_session(aht);
    goto out;
  }
  (void) adt_end_session(aht);

  /* Set handler for authentication error */
  pr_event_register(&solaris_audit_module, "mod_auth.authentication-code",
    audit_autherr_ev, NULL);

  rval = 0;

out:

  /* remove unneeded privileges */
  priv_delset(privset, PRIV_SYS_AUDIT);
  (void) setppriv(PRIV_SET, PRIV_EFFECTIVE, privset);
  (void) setpflags(PRIV_AWARE_RESET, 1);
  priv_freeset(privset);

  return rval;
}

#define EVENT_KEY       "event"

/* Helper functions and global variables
 * for the file transfer command handlers.
 * {
 */

static char src_realpath[PATH_MAX];
static char dst_realpath[PATH_MAX];


/*
 * If an error occurs in any of the file transfer handlers,
 * and the handler wants to return PR_ERROR(cmd), then it is necessary
 * to send some FTP error message to user. This is in order to prevent
 * a hang-up of the user's ftp client.
 *
 * This function sends the 451 error message to the user.
 * It is only called in the "pre-" handlers. When a "pre-" handler
 * returns PR_ERROR(cmd), then the corresponding "post_err-"
 * handler is also called. Therefore it can happen that an error condition
 * (such as no memory) can be logged (with the pr_log_pri() routine) twice.
 * Once in the "pre-" handler, and once in the "post_err-" handler.
 */
static void error_451(void)
{
  pr_response_add_err(R_451,
    "Requested action aborted: local error in processing.\n");
}

/*
 * Allocate resources to process a command outcome.
 *
 * All file transfer command handlers need to allocate adt_event_data_t
 * structure and also make a copy of the command argument.
 * This function does both. If it can't, it logs an error and returns NULL.
 * On success, it returns the pointer (event) to the allocated adt_event_data_t
 * structure.
 *
 * If arg2 is not NULL, it makes a copy of the first (and only) command
 * argument (using the memory pool "pool" from "cmd") and stores it to *arg2.
 * There must be always exactly one command argument, otherwise it is an error.
 *
 * On success, the pointer to the created event structure is stored
 * into cmd under "notes" variable, so that it is accessible
 * by the subsequent corresponding "post-" or "post_err-" command handler.
 */
adt_event_data_t* __solaris_audit_pre_arg2(
    cmd_rec *cmd, const char* description, int event_type, char **arg2) {

  adt_event_data_t *event = NULL;
  const char *how = "";
  char *tmp = NULL;

  /* The ftp server code will save errno into this variable
   * in case an error happens, and there is a valid errno for it.
   */
  cmd->error_code = ADT_FAILURE;

  if (cmd->arg == NULL) {
    pr_log_pri(PR_LOG_ERR, "Auditing of %s failed: %s",
      description, "bad argument");
    goto err;
  }

  if (arg2 != NULL) {
    *arg2 = NULL;

    if ((tmp = pstrdup(cmd->pool, cmd->arg)) == NULL) {
      how = "no memory";
      pr_log_pri(PR_LOG_ERR, "Auditing of %s(%s) failed: %s",
        description, cmd->arg, how);
      goto err;
    }
    *arg2 = tmp;
  }

  if (cmd->notes == NULL) {
    pr_log_pri(PR_LOG_ERR, "Auditing of %s(%s) failed: %s",
      description, cmd->arg, "API error, notes is NULL");
    goto err;
  }

  if ((event = adt_alloc_event(asession, event_type)) == NULL) {
    how = "couldn't allocate adt event";
    pr_log_pri(PR_LOG_ERR, "Auditing of %s(%s) failed: %s(%s)",
      description, cmd->arg, how, strerror(errno));
    goto err;
  }

  if (pr_table_add(cmd->notes, EVENT_KEY, event, sizeof(*event)) == -1) {
    how = "pr_table_add() failed";
    pr_log_pri(PR_LOG_ERR, "Auditing of %s(%s) failed: %s",
      description, cmd->arg, how);
    adt_free_event(event);
    goto err;
  }
  
  return event;

err:
  return NULL;
}

/*
 * This function implements logic that is common to most "post-"
 * and "post_err-" file transfer command handlers.
 *
 * It retrieves the pointer (event) to the adt_event_data_t structure
 * from "cmd->notes" and logs it. This structure has been created by the
 * __solaris_audit_pre_arg2() function.
 * 
 * Some audit event structures contain an optional *_stat member.
 * If "fill_attr" is not NULL, it is called to fill in this member,
 * before the audit event is logged.
 *
 * This function always returns PR_DECLINED, even if it failed
 * to log the audit event. The reason is that it is called in the
 * "post-" file transfer command handlers, which means that the command
 * has been already successfully executed by the ftp server.
 */
MODRET __solaris_audit_post(cmd_rec *cmd,
  const char* description, int exit_status, int __unused,
  const char* (*fill_event)(cmd_rec *cmd, adt_event_data_t *event))
{
  adt_event_data_t *event = NULL;
  const char* how = "";
  const char* msg = NULL;
  size_t size = 0;
  int exit_error = cmd->error_code;

  event = (adt_event_data_t*)pr_table_remove(cmd->notes, EVENT_KEY, &size);
  if (event == NULL) {
    how = "event is NULL";
    pr_log_pri(PR_LOG_ERR, "Auditing of %s failed: %s", description, how);
    goto out;
  }

  if (size != sizeof(*event)) {
    how = "bad event size";
    pr_log_pri(PR_LOG_ERR, "Auditing of %s failed: %s", description, how);
    goto out;
  }

  if (fill_event != NULL) {
    msg = fill_event(cmd, event);
    if (msg != NULL) {
      pr_log_pri(PR_LOG_ERR, "Auditing of %s failed: %s", description, msg);
      goto out;
    }
  }

  /* It can happen, that the ftp command succeeds but only to some degree.
   * In such case, the exit_error might contain the errno number
   * of the failure.
   */
  if (exit_status == ADT_SUCCESS) {
    if (exit_error == ADT_FAILURE)
      exit_error = ADT_SUCCESS;
  }

  if (adt_put_event(event, exit_status, exit_error) != 0) {
    how = "couldn't put adt event";
    pr_log_pri(PR_LOG_ERR, "Auditing of %s failed: %s (%s)",
      description, how, strerror(errno));
  }

  adt_free_event(event);

out:
  return PR_DECLINED(cmd);
}

/*
 * This is a generic function to fill in the given "stat" member
 * of some audit event structure. The path and the member are specified
 * by the caller. The pointer to cmd is supplied, because the stat64
 * structure has to be allocated (the "stat" member is a pointer).
 *
 * The function returns NULL on success.
 * In case of an error, it returns a descriptive message.
 * This message is used by the caller to log an error.
 *
 * For some file transfer commands, the "stat" member is filled in
 * the "pre-" handler (because the file is expected to exist prior
 * to the execution of the command). For other file transfer commands,
 * the "stat" member is filled in the "post-" handler (because
 * the file is expected _not_ to exist prior to the execution of the command,
 * but to exist after the command execution).
 */
static const char* __fill_attr
(
  cmd_rec *cmd, const char* path, adt_stat_t **ret)
{
  struct stat64 *ptr;
  int err;

  if (ret == NULL)
    return "NULL pointer";

  *ret = NULL;

  ptr = palloc(cmd->pool, sizeof(*ptr));
  if (ptr == NULL)
    return "no memory";

  err = stat64(path, ptr);
  if (err == -1)
    return "stat64() failed";

  *ret = ptr;
  return NULL;
}
/* } */


/* Delete file. { */
static const char* dele_fill_attr(cmd_rec *cmd, adt_event_data_t *event) {
  return __fill_attr(
    cmd, event->adt_ft_remove.f_path, &(event->adt_ft_remove.f_attr)
  );
}

MODRET solaris_audit_pre_dele(cmd_rec *cmd) {
  adt_event_data_t *event = NULL;
  char* ptr = NULL;
  char* rp = NULL;

  event = __solaris_audit_pre_arg2(cmd, "remove", ADT_ft_remove, &ptr);
  if (event == NULL) {
    error_451();
    return PR_ERROR(cmd);
  }

  rp = realpath(ptr, src_realpath);
  if (rp == NULL) {
    if (errno != ENOENT) {
      pr_log_pri(PR_LOG_ERR, "Auditing of %s(%s) failed: %s",
        "remove", ptr, "realpath() failed");
      cmd->error_code = errno;
      error_451();
      return PR_ERROR(cmd);
    }
    /* If rp is NULL and errno is ENOENT, it means that 
     * the file to be deleted does not exist. In this case,
     * the post_dele_err callback will be called to log this.
     */
  }

  if (rp != NULL)
    ptr = rp;    

  event->adt_ft_remove.f_path = ptr;
  (void) dele_fill_attr(cmd, event);

  return PR_DECLINED(cmd);
}

MODRET solaris_audit_post_dele(cmd_rec *cmd) {
  return __solaris_audit_post(
    cmd, "remove", ADT_SUCCESS, ADT_SUCCESS, NULL);
}

MODRET solaris_audit_post_dele_err(cmd_rec *cmd) {
  return __solaris_audit_post(cmd, "remove", ADT_FAILURE, ADT_FAILURE, NULL);
}
/* } */


/* Make directory. { */
MODRET solaris_audit_pre_mkd(cmd_rec *cmd) {
  adt_event_data_t *event = NULL;
  char* ptr = NULL;

  event = __solaris_audit_pre_arg2(cmd, "mkdir", ADT_ft_mkdir, &ptr);
  if (event == NULL) {
    error_451();
    return PR_ERROR(cmd);
  }

  event->adt_ft_mkdir.d_path = ptr;
  event->adt_ft_mkdir.d_attr = NULL;

  /* Value 0777 is hardcoded in the ftp server. */
  event->adt_ft_mkdir.arg = 0777;
  event->adt_ft_mkdir.arg_id = 2;
  event->adt_ft_mkdir.arg_desc = "mode";

  return PR_DECLINED(cmd);
}

static const char* mkd_fill_event(cmd_rec *cmd, adt_event_data_t *event) {
  char *rp = NULL;

  rp = realpath(event->adt_ft_mkdir.d_path, src_realpath);
  if (rp == NULL) {
    cmd->error_code = errno;
    return "realpath() failed";
  }

  event->adt_ft_mkdir.d_path = rp;
  return __fill_attr(
    cmd, event->adt_ft_mkdir.d_path, &(event->adt_ft_mkdir.d_attr)
  );
}

static const char* mkd_fill_event_err(cmd_rec *cmd, adt_event_data_t *event) {
  char *rp = NULL;

  rp = realpath(event->adt_ft_mkdir.d_path, src_realpath);
  if (rp != NULL) {
    event->adt_ft_mkdir.d_path = rp;
    (void) __fill_attr(
      cmd, event->adt_ft_mkdir.d_path, &(event->adt_ft_mkdir.d_attr)); 
  }

  return NULL;
}

MODRET solaris_audit_post_mkd(cmd_rec *cmd) {
  return __solaris_audit_post(
    cmd, "mkdir", ADT_SUCCESS, ADT_SUCCESS, mkd_fill_event);
}

MODRET solaris_audit_post_mkd_err(cmd_rec *cmd) {
  return __solaris_audit_post(
    cmd, "mkdir", ADT_FAILURE, ADT_FAILURE, mkd_fill_event_err);
}
/* } */

/* Remove directory. { */
static const char* rmd_fill_attr(cmd_rec *cmd, adt_event_data_t *event) {
  return __fill_attr(
    cmd, event->adt_ft_rmdir.f_path, &(event->adt_ft_rmdir.f_attr)
  );
}

MODRET solaris_audit_pre_rmd(cmd_rec *cmd) {
  adt_event_data_t *event = NULL;
  char* ptr = NULL;
  char* rp = NULL;
 
  event = __solaris_audit_pre_arg2(cmd, "rmdir", ADT_ft_rmdir, &ptr);
  if (event == NULL) {
    error_451();
    return PR_ERROR(cmd);
  }

  rp = realpath(ptr, src_realpath);
  if (rp == NULL) {
    if (errno != ENOENT) {
      cmd->error_code = errno;
      pr_log_pri(PR_LOG_ERR, "Auditing of %s(%s) failed: %s",
        "rmdir", ptr, "realpath() failed");
      error_451();
      return PR_ERROR(cmd);
    }
  }

  if (rp != NULL)
    ptr = rp;

  event->adt_ft_rmdir.f_path = ptr;
  (void) rmd_fill_attr(cmd, event);

  return PR_DECLINED(cmd);
}

MODRET solaris_audit_post_rmd(cmd_rec *cmd) {
  return __solaris_audit_post(cmd, "rmdir", ADT_SUCCESS, ADT_SUCCESS, NULL);
}

MODRET solaris_audit_post_rmd_err(cmd_rec *cmd) {
  return __solaris_audit_post(cmd, "rmdir", ADT_FAILURE, ADT_FAILURE, NULL);
}
/* } */

/* Get modification time and date. { */
MODRET solaris_audit_pre_mdtm(cmd_rec *cmd) {
  adt_event_data_t *event = NULL;
  char* ptr = NULL;
  char* rp = NULL;
  
  event = __solaris_audit_pre_arg2(cmd, "utimes", ADT_ft_utimes, &ptr);
  if (event == NULL) {
    error_451();
    return PR_ERROR(cmd);
  }

  rp = realpath(ptr, src_realpath);
  if (rp == NULL) {
    if (errno != ENOENT) {
      cmd->error_code = errno;
      pr_log_pri(PR_LOG_ERR, "Auditing of %s(%s) failed: %s",
        "utimes", ptr, "realpath() failed");
      error_451();
      return PR_ERROR(cmd);
    }
  }

  if (rp != NULL)
    ptr = rp;

  event->adt_ft_utimes.f_path = ptr;
  event->adt_ft_utimes.f_attr = NULL;

  return PR_DECLINED(cmd);
}

static const char* mdtm_fill_attr(cmd_rec *cmd, adt_event_data_t *event) {
  return __fill_attr(
    cmd, event->adt_ft_utimes.f_path, &(event->adt_ft_utimes.f_attr)
  );
}

MODRET solaris_audit_post_mdtm(cmd_rec *cmd) {
  return __solaris_audit_post(
    cmd, "utimes", ADT_SUCCESS, ADT_SUCCESS, mdtm_fill_attr);
}

MODRET solaris_audit_post_mdtm_err(cmd_rec *cmd) {
  return __solaris_audit_post(cmd, "utimes", ADT_FAILURE, ADT_FAILURE, NULL);
}
/* } */

/* Upload file. { */
MODRET solaris_audit_pre_put(cmd_rec *cmd) {
  adt_event_data_t *event = NULL;
  char* ptr = NULL;
  
  event = __solaris_audit_pre_arg2(cmd, "put", ADT_ft_put, &ptr);
  if (event == NULL) {
    error_451();
    return PR_ERROR(cmd);
  }

  event->adt_ft_put.f_path = ptr;
  event->adt_ft_put.f_attr = NULL;

  return PR_DECLINED(cmd);
}

static const char* put_fill_event(cmd_rec *cmd, adt_event_data_t *event) {
  char *rp = NULL;

  rp = realpath(event->adt_ft_put.f_path, src_realpath);
  if (rp == NULL) {
    cmd->error_code = errno;
    return "realpath() failed";
  }

  event->adt_ft_put.f_path = rp;
  return __fill_attr(
    cmd, event->adt_ft_put.f_path, &(event->adt_ft_put.f_attr)
  );
}

MODRET solaris_audit_post_put(cmd_rec *cmd) {
  return __solaris_audit_post(
    cmd, "put", ADT_SUCCESS, ADT_SUCCESS, put_fill_event);
}

MODRET solaris_audit_post_put_err(cmd_rec *cmd) {
  return __solaris_audit_post(cmd, "put", ADT_FAILURE, ADT_FAILURE, NULL);
}
/* } */

/* Download file. { */
MODRET solaris_audit_pre_get(cmd_rec *cmd) {
  adt_event_data_t *event = NULL;
  char* ptr = NULL;
  char* rp = NULL;
  
  event = __solaris_audit_pre_arg2(cmd, "get", ADT_ft_get, &ptr);
  if (event == NULL) {
    error_451();
    return PR_ERROR(cmd);
  }

  rp = realpath(ptr, src_realpath);
  if (rp == NULL) {
    if (errno != ENOENT) {
      cmd->error_code = errno;
      pr_log_pri(PR_LOG_ERR, "Auditing of %s(%s) failed: %s",
        "get", ptr, "realpath() failed");
      error_451();
      return PR_ERROR(cmd);
    }
  }

  if (rp != NULL)
    ptr = rp;

  event->adt_ft_get.f_path = ptr;
  event->adt_ft_get.f_attr = NULL;

  return PR_DECLINED(cmd);
}

static const char* get_fill_attr(cmd_rec *cmd, adt_event_data_t *event) {
  return __fill_attr(
    cmd, event->adt_ft_get.f_path, &(event->adt_ft_get.f_attr)
  );
}

MODRET solaris_audit_post_get(cmd_rec *cmd) {
  return __solaris_audit_post(
    cmd, "get", ADT_SUCCESS, ADT_SUCCESS, get_fill_attr);
}

MODRET solaris_audit_post_get_err(cmd_rec *cmd) {
  return __solaris_audit_post(cmd, "get", ADT_FAILURE, ADT_FAILURE, NULL);
}
/* } */

/* Rename file. { */
/*
 * The rename file implementation uses malloc()/free(),
 * which the ProFTP module interface prohibits. I do not see another way.
 * 
 * Any memory allocation method provided by the ProFTP API uses a memory pool.
 * To avoid malloc()/free() a persistent memory pool is needed.
 */

/*
 * To successfully log the rename audit event, a cooperation
 * of RNFR and RNTO command handlers is necessary.
 * The RNFR command specifies the source file name,
 * and the RNTO command specifies the destination file name.
 * 
 * The RNFR command handlers save the source file in the "src_path"
 * variable, so that it is available to the RNTO command handler,
 * which logs the audit event.
 */
static char* src_path = NULL;

/* RNFR. { */
static void __solaris_audit_rnfr_err(cmd_rec *cmd)
{
  adt_event_data_t *event = NULL;

  if (src_path == NULL)
    return;

  event = __solaris_audit_pre_arg2(cmd, "RNFR", ADT_ft_rename, NULL);
  if (event == NULL) {
    error_451();
    goto out;
  }

  event->adt_ft_rename.src_path = src_path;
  event->adt_ft_rename.src_attr = NULL;
  event->adt_ft_rename.dst_path = NULL;

  (void) __solaris_audit_post(cmd, "RNFR", ADT_FAILURE, ADT_FAILURE, NULL);

out:
  free(src_path);
  src_path = NULL;
}

MODRET solaris_audit_pre_rnfr(cmd_rec *cmd) {
  adt_event_data_t *event = NULL;
  char* ptr = NULL;

  /*
   * If src_path is not NULL, it means that this RNFR command immediatelly
   * follows a successfull RNFR command not terminated with a RNTO command.
   * In such case, log an audit error for this unterminated RNFR command,
   * and then continue normally.
   *
   * A correctly working ftp client can not cause this situation to happen.
   * But this situation can be created, for instance, by manually sending
   * commands to the ftp server with a telnet client.
   */
  if (src_path != NULL)
    __solaris_audit_rnfr_err(cmd);

  /*
   * Prepare the audit event structure and remember the new src_path.
   * This audit event structure will be used, if the RNFR command fails.
   * It will be unused, if it succeeds.
   */
  event = __solaris_audit_pre_arg2(cmd, "get", ADT_ft_rename, &ptr);
  if (event == NULL)
    goto err;

  event->adt_ft_rename.src_path = ptr;
  event->adt_ft_rename.src_attr = NULL;
  event->adt_ft_rename.dst_path = "";

  src_path = strdup(cmd->arg);
  if (src_path == NULL) {
    pr_log_pri(PR_LOG_ERR, "Auditing of %s(%s) failed: %s",
      "RNFR", ptr, "no memory");
    goto err;
  }

  return PR_DECLINED(cmd);
err:
  return PR_ERROR(cmd);
}

/*
 * On success, the RNFR command handlers do not log any audit event.
 * A success means that a rename command is in progress and that
 * the immediatelly following command is to be RNTO. 
 */
MODRET solaris_audit_post_rnfr(cmd_rec *cmd) {

  char *ptr;

  ptr = realpath(src_path, src_realpath);
  if (ptr == NULL) {
    pr_log_pri(PR_LOG_ERR, "Auditing of %s(%s) failed: %s",
      "RNFR", src_path, "realpath() failed");
    error_451();
    return PR_ERROR(cmd);
  }

  /*
   * The argument to RNFR command is saved in src_path.
   * It will be used in the subsequent RNTO command, or RNFR command.
   */
  return PR_DECLINED(cmd);
}

/* It can happen, that RNFR command fails, but the source path exists.
 * Therefore make an attempt to resolve its realpath before doing
 * the audit log.
 *
 * Even if the realpath() call fails, the src_path contents are still
 * copied to src_realpath buffer. This makes them available to the RNTO
 * command handlers.
 */
static const char* rnfr_err_fill_event(cmd_rec *cmd, adt_event_data_t *event) {
  char *ptr = NULL;

  if (src_path != NULL) {
    ptr = realpath(src_path, src_realpath);
    if (ptr != NULL)
      event->adt_ft_rename.src_path = ptr;
  }

  return NULL;
}

/*
 * On error, an audit event is logged, specifying that a rename
 * command failed. The destination path in the audit event structure
 * is empty, simply because the corresponding RNTO command did not yet
 * happen, and it is not suppossed to happen.
 */
MODRET solaris_audit_post_rnfr_err(cmd_rec *cmd) {
  MODRET ret;

  ret = __solaris_audit_post(cmd, "RNFR", ADT_FAILURE, ADT_FAILURE,
    rnfr_err_fill_event);

  free(src_path);
  src_path = NULL;

  return ret;
}
/* } RNFR. */

/* RNTO. { */
static const char* rnto_fill_attr(cmd_rec *cmd, adt_event_data_t *event) {
  return __fill_attr(
    cmd, event->adt_ft_rename.src_path, &(event->adt_ft_rename.src_attr)
  );
}

MODRET solaris_audit_pre_rnto(cmd_rec *cmd) {
  adt_event_data_t *event = NULL;
  const char* msg = NULL;
  char* ptr = NULL;

  event = __solaris_audit_pre_arg2(cmd, "get", ADT_ft_rename, &ptr);
  if (event == NULL)
    goto err;

  /*
   * If src_path is NULL, this means that there is no previous
   * successfull RNFR command. The ftp server should know about this
   * and terminate this RNTO command with an error (call the error callback).
   */
  event->adt_ft_rename.src_path = (src_path)?src_path:"";
  event->adt_ft_rename.dst_path = ptr;

  /*
   * If the code executes here, it means that there is a successfully finished
   * RNFR command immediatelly before us, which means that the src_path exists,
   * and it should be therefore possible to get its status.
   */
  msg = rnto_fill_attr(cmd, event);  
  if (msg != NULL) {
    pr_log_pri(PR_LOG_ERR, "Auditing of %s(%s,%s) failed: %s",
      "RNTO", event->adt_ft_rename.src_path, ptr, msg);
    goto err;
  }

  return PR_DECLINED(cmd);

err:
  error_451();
  return PR_ERROR(cmd);
}

static const char* rnto_fill_event(cmd_rec *cmd, adt_event_data_t *event) {
  char *ptr;

  ptr = realpath(event->adt_ft_rename.dst_path, dst_realpath);
  if (ptr == NULL) {
    return "realpath() failed";
  }

  event->adt_ft_rename.src_path = src_realpath;
  event->adt_ft_rename.dst_path = dst_realpath;

  return NULL;
}

MODRET solaris_audit_post_rnto(cmd_rec *cmd) {
   MODRET retval;

  /* NULL means that there is no preceeding successfull RNFR command. */
  if (src_path == NULL)
    return PR_ERROR(cmd);

  retval = __solaris_audit_post(cmd, "RNTO", ADT_SUCCESS, ADT_SUCCESS,
    rnto_fill_event);
  
  free(src_path);
  src_path = NULL;

  return retval;
}

/* It can happen, that RNTO command fails, but the destination path exists.
 * Therefore make an attempt to resolve its realpath before doing
 * the audit log.
 */
static const char* rnto_err_fill_event(cmd_rec *cmd, adt_event_data_t *event) {

  (void) realpath(event->adt_ft_rename.dst_path, dst_realpath);
  event->adt_ft_rename.src_path = src_realpath;
  event->adt_ft_rename.dst_path = dst_realpath;

  return NULL;
}

MODRET solaris_audit_post_rnto_err(cmd_rec *cmd) {
  MODRET retval;
  retval = __solaris_audit_post(cmd, "RNTO", ADT_FAILURE, ADT_FAILURE,
    rnto_err_fill_event);
  if (src_path != NULL) {
    free(src_path);
    src_path = NULL;
  }
  return retval;
}
/* } RNTO. */

static cmdtable solaris_audit_commands[] = {
    /* Login, logout. */
    { POST_CMD, C_PASS, G_NONE, solaris_audit_post_pass, FALSE, FALSE },
    { POST_CMD_ERR, C_PASS, G_NONE, solaris_audit_post_fail, FALSE, FALSE },

    /* Delete file. */
    { PRE_CMD, C_DELE, G_NONE, solaris_audit_pre_dele, FALSE, FALSE },
    { POST_CMD, C_DELE, G_NONE, solaris_audit_post_dele, FALSE, FALSE },
    { POST_CMD_ERR, C_DELE, G_NONE, solaris_audit_post_dele_err,
        FALSE, FALSE },

    /* Make directory. */
    { PRE_CMD, C_MKD, G_NONE, solaris_audit_pre_mkd, FALSE, FALSE },
    { POST_CMD, C_MKD, G_NONE, solaris_audit_post_mkd, FALSE, FALSE },
    { POST_CMD_ERR, C_MKD, G_NONE, solaris_audit_post_mkd_err,
        FALSE, FALSE },

    /* Remove directory. */
    { PRE_CMD, C_RMD, G_NONE, solaris_audit_pre_rmd, FALSE, FALSE },
    { POST_CMD, C_RMD, G_NONE, solaris_audit_post_rmd, FALSE, FALSE },
    { POST_CMD_ERR, C_RMD, G_NONE, solaris_audit_post_rmd_err,
        FALSE, FALSE },

    { PRE_CMD, C_XRMD, G_NONE, solaris_audit_pre_rmd, FALSE, FALSE },
    { POST_CMD, C_XRMD, G_NONE, solaris_audit_post_rmd, FALSE, FALSE },
    { POST_CMD_ERR, C_XRMD, G_NONE, solaris_audit_post_rmd_err,
        FALSE, FALSE },

    /* Get modification time. */
    { PRE_CMD, C_MDTM, G_NONE, solaris_audit_pre_mdtm, FALSE, FALSE },
    { POST_CMD, C_MDTM, G_NONE, solaris_audit_post_mdtm, FALSE, FALSE },
    { POST_CMD_ERR, C_MDTM, G_NONE, solaris_audit_post_mdtm_err,
        FALSE, FALSE },

    /* Upload file. */
    { PRE_CMD, C_STOR, G_WRITE, solaris_audit_pre_put, FALSE, FALSE },
    { POST_CMD, C_STOR, G_WRITE, solaris_audit_post_put, FALSE, FALSE },
    { POST_CMD_ERR, C_STOR, G_WRITE, solaris_audit_post_put_err,
        FALSE, FALSE },

    { PRE_CMD, C_STOU, G_WRITE, solaris_audit_pre_put, FALSE, FALSE },
    { POST_CMD, C_STOU, G_WRITE, solaris_audit_post_put, FALSE, FALSE },
    { POST_CMD_ERR, C_STOU, G_WRITE, solaris_audit_post_put_err,
        FALSE, FALSE },

    { PRE_CMD, C_APPE, G_WRITE, solaris_audit_pre_put, FALSE, FALSE },
    { POST_CMD, C_APPE, G_WRITE, solaris_audit_post_put, FALSE, FALSE },
    { POST_CMD_ERR, C_APPE, G_WRITE, solaris_audit_post_put_err,
        FALSE, FALSE },

    /* Download file. */
    { PRE_CMD, C_RETR, G_READ, solaris_audit_pre_get, FALSE, FALSE },
    { POST_CMD, C_RETR, G_READ, solaris_audit_post_get, FALSE, FALSE },
    { POST_CMD_ERR, C_RETR, G_READ, solaris_audit_post_get_err,
        FALSE, FALSE },

    /* Rename file. */
    { PRE_CMD, C_RNFR, G_NONE, solaris_audit_pre_rnfr, FALSE, FALSE },
    { POST_CMD, C_RNFR, G_NONE, solaris_audit_post_rnfr, FALSE, FALSE },
    { POST_CMD_ERR, C_RNFR, G_NONE, solaris_audit_post_rnfr_err,
        FALSE, FALSE },

    { PRE_CMD, C_RNTO, G_NONE, solaris_audit_pre_rnto, FALSE, FALSE },
    { POST_CMD, C_RNTO, G_NONE, solaris_audit_post_rnto, FALSE, FALSE },
    { POST_CMD_ERR, C_RNTO, G_NONE, solaris_audit_post_rnto_err,
        FALSE, FALSE },

	{ 0, NULL }
};

module solaris_audit_module = {
	NULL, NULL,		/* Always NULL */
	0x20,			/* API Version 2.0 */
	"solaris_audit",
	NULL,			/* configuration table */
	solaris_audit_commands,	/* command table is for local use only */
	NULL,			/* No authentication handlers */
	NULL,			/* No initialization function */
	audit_sess_init		/* Post-fork "child mode" init */
};