components/proftpd/mod_solaris_audit.c
changeset 305 e95b65443448
child 598 398722c80922
child 2251 310a361c2ce7
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/proftpd/mod_solaris_audit.c	Wed Jun 15 01:09:08 2011 -0700
@@ -0,0 +1,1173 @@
+/*
+ * ProFTPD - FTP server daemon
+ * Copyright (c) 2011, 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_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->argc != 2) {
+    pr_log_pri(PR_LOG_ERR, "Auditing of %s failed: %s",
+      description, "bad arguments");
+    goto err;
+  }
+
+  if (arg2 != NULL) {
+    *arg2 = NULL;
+
+    if ((tmp = pstrdup(cmd->pool, cmd->argv[1])) == NULL) {
+      how = "no memory";
+      pr_log_pri(PR_LOG_ERR, "Auditing of %s(%s) failed: %s",
+        description, cmd->argv[1], how);
+      goto err;
+    }
+    *arg2 = tmp;
+  }
+
+  if (cmd->notes == NULL ) {
+    pr_log_pri(PR_LOG_ERR, "Auditing of %s(%s) failed: %s",
+      description, cmd->argv[1], "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->argv[1], 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->argv[1], 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->argv[1]);
+  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 */
+};