pam UPDATE auth using Linux PAM

Added the ability to authenticate via Linux PAM when using keyboard-interactive SSH authentication method. One new API call was added.
diff --git a/src/session_server_ssh.c b/src/session_server_ssh.c
index 6eeafd5..b9e567b 100644
--- a/src/session_server_ssh.c
+++ b/src/session_server_ssh.c
@@ -27,6 +27,7 @@
 #include <errno.h>
 #include <fcntl.h>
 #include <pwd.h>
+#include <security/pam_appl.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sys/stat.h>
@@ -176,6 +177,39 @@
     server_opts.interactive_auth_data_free = free_user_data;
 }
 
+API int
+nc_server_ssh_set_pam_conf_path(const char *conf_name, const char *conf_dir)
+{
+    free(server_opts.conf_name);
+    free(server_opts.conf_dir);
+    server_opts.conf_name = NULL;
+    server_opts.conf_dir = NULL;
+
+    if (conf_dir) {
+#ifdef LIBPAM_HAVE_CONFDIR
+        server_opts.conf_dir = strdup(conf_dir);
+        if (!(server_opts.conf_dir)) {
+            ERRMEM;
+            return -1;
+        }
+#else
+        ERR(NULL, "Failed to set PAM config directory because of old version of PAM. "
+                "Put the config file in the system directory (usually /etc/pam.d/).");
+        return -1;
+#endif
+    }
+
+    if (conf_name) {
+        server_opts.conf_name = strdup(conf_name);
+        if (!(server_opts.conf_name)) {
+            ERRMEM;
+            return -1;
+        }
+    }
+
+    return 0;
+}
+
 API void
 nc_server_ssh_set_pubkey_auth_clb(int (*pubkey_auth_clb)(const struct nc_session *session, ssh_key key, void *user_data),
         void *user_data, void (*free_user_data)(void *user_data))
@@ -403,6 +437,11 @@
 static int
 nc_server_ssh_set_auth_methods(int auth_methods, struct nc_server_ssh_opts *opts)
 {
+    if ((auth_methods & NC_SSH_AUTH_INTERACTIVE) && !server_opts.conf_name) {
+        /* path to a configuration file not set */
+        ERR(NULL, "Unable to use Keyboard-Interactive authentication method without setting the name of the PAM configuration file first.");
+        return 1;
+    }
     opts->auth_methods = auth_methods;
     return 0;
 }
@@ -907,33 +946,250 @@
     }
 }
 
+/**
+ * @brief PAM conversation function, which serves as a callback for exchanging messages between the client and a PAM module.
+ *
+ * @param[in] n_messages Number of messages.
+ * @param[in] msg PAM module's messages.
+ * @param[out] resp User responses.
+ * @param[in] appdata_ptr Callback's data.
+ * @return PAM_SUCCESS on success;
+ * @return PAM_BUF_ERR on memory allocation error;
+ * @return PAM_CONV_ERR otherwise.
+ */
+static int
+nc_pam_conv_clb(int n_messages, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr)
+{
+    int i, j, t, r = PAM_SUCCESS, n_answers, n_requests = n_messages;
+    const char **prompts = NULL;
+    char *echo = NULL;
+    const char *name = "Keyboard-Interactive Authentication";
+    const char *instruction = "Please enter your authentication token";
+    ssh_message reply = NULL;
+    struct nc_pam_thread_arg *clb_data = appdata_ptr;
+    ssh_session libssh_session;
+    struct timespec ts_timeout;
+    struct nc_server_ssh_opts *opts;
+
+    libssh_session = clb_data->session->ti.libssh.session;
+    opts = clb_data->session->data;
+
+    /* PAM_MAX_NUM_MSG == 32 by default */
+    if ((n_messages <= 0) || (n_messages >= PAM_MAX_NUM_MSG)) {
+        ERR(NULL, "Bad number of PAM messages (#%d).", n_messages);
+        r = PAM_CONV_ERR;
+        goto cleanup;
+    }
+
+    /* only accepting these 4 types of messages */
+    for (i = 0; i < n_messages; i++) {
+        t = msg[i]->msg_style;
+        if ((t != PAM_PROMPT_ECHO_OFF) && (t != PAM_PROMPT_ECHO_ON) && (t != PAM_TEXT_INFO) && (t != PAM_ERROR_MSG)) {
+            ERR(NULL, "PAM conversation callback received an unexpected type of message.");
+            r = PAM_CONV_ERR;
+            goto cleanup;
+        }
+    }
+
+    /* display messages with errors and/or some information and count the amount of actual authentication challenges */
+    for (i = 0; i < n_messages; i++) {
+        if (msg[i]->msg_style == PAM_TEXT_INFO) {
+            VRB(NULL, "PAM conversation callback received a message with some information for the client (%s).", msg[i]->msg);
+            n_requests--;
+        }
+        if (msg[i]->msg_style == PAM_ERROR_MSG) {
+            ERR(NULL, "PAM conversation callback received an error message (%s).", msg[i]->msg);
+            r = PAM_CONV_ERR;
+            goto cleanup;
+        }
+    }
+
+    /* there are no requests left for the user, only messages with some information for the client were sent */
+    if (n_requests <= 0) {
+        r = PAM_SUCCESS;
+        goto cleanup;
+    }
+
+    /* it is the PAM module's responsibility to release both, this array and the responses themselves */
+    *resp = calloc(n_requests, sizeof **resp);
+    prompts = calloc(n_requests, sizeof *prompts);
+    echo = calloc(n_requests, sizeof *echo);
+    if (!(*resp) || !prompts || !echo) {
+        ERRMEM;
+        r = PAM_BUF_ERR;
+        goto cleanup;
+    }
+
+    /* set the prompts for the user */
+    j = 0;
+    for (i = 0; i < n_messages; i++) {
+        if ((msg[i]->msg_style == PAM_PROMPT_ECHO_ON) || (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF)) {
+            prompts[j++] = msg[i]->msg;
+        }
+    }
+
+    /* iterate over all the messages and adjust the echo array accordingly */
+    j = 0;
+    for (i = 0; i < n_messages; i++) {
+        if (msg[i]->msg_style == PAM_PROMPT_ECHO_ON) {
+            echo[j++] = 1;
+        }
+        if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF) {
+            /* no need to set to 0 because of calloc */
+            j++;
+        }
+    }
+
+    /* print all the keyboard-interactive challenges to the user */
+    r = ssh_message_auth_interactive_request(clb_data->msg, name, instruction, n_requests, prompts, echo);
+    if (r != SSH_OK) {
+        ERR(NULL, "Failed to send an authentication request.");
+        r = PAM_CONV_ERR;
+        goto cleanup;
+    }
+
+    if (opts->auth_timeout) {
+        nc_gettimespec_mono_add(&ts_timeout, opts->auth_timeout * 1000);
+    }
+
+    /* get user's replies */
+    do {
+        if (!nc_session_is_connected(clb_data->session)) {
+            ERR(NULL, "Communication SSH socket unexpectedly closed.");
+            r = PAM_CONV_ERR;
+            goto cleanup;
+        }
+
+        reply = ssh_message_get(libssh_session);
+        if (reply) {
+            break;
+        }
+
+        usleep(NC_TIMEOUT_STEP);
+    } while ((opts->auth_timeout) && (nc_difftimespec_cur(&ts_timeout) >= 1));
+
+    if (!reply) {
+        ERR(NULL, "Authentication timeout.");
+        r = PAM_CONV_ERR;
+        goto cleanup;
+    }
+
+    /* check if the amount of replies matches the amount of requests */
+    n_answers = ssh_userauth_kbdint_getnanswers(libssh_session);
+    if (n_answers != n_requests) {
+        ERR(NULL, "Expected %d response(s), got %d.", n_requests, n_answers);
+        r = PAM_CONV_ERR;
+        goto cleanup;
+    }
+
+    /* give the replies to a PAM module */
+    for (i = 0; i < n_answers; i++) {
+        (*resp)[i].resp = strdup(ssh_userauth_kbdint_getanswer(libssh_session, i));
+        /* it should be the caller's responsibility to free this, however if mem alloc fails,
+         * it is safer to free the responses here and set them to NULL */
+        if ((*resp)[i].resp == NULL) {
+            for (j = 0; j < i; j++) {
+                free((*resp)[j].resp);
+                (*resp)[j].resp = NULL;
+            }
+            ERRMEM;
+            r = PAM_BUF_ERR;
+            goto cleanup;
+        }
+    }
+
+cleanup:
+    ssh_message_free(reply);
+    free(prompts);
+    free(echo);
+    return r;
+}
+
+/**
+ * @brief Handles authentication via Linux PAM.
+ *
+ * @param[in] session NETCONF session.
+ * @param[in] ssh_msg SSH message with a keyboard-interactive authentication request.
+ * @return PAM_SUCCESS on success;
+ * @return PAM error otherwise.
+ */
+static int
+nc_pam_auth(struct nc_session *session, ssh_message ssh_msg)
+{
+    pam_handle_t *pam_h = NULL;
+    int ret;
+    struct nc_pam_thread_arg clb_data;
+    struct pam_conv conv;
+
+    /* structure holding callback's data */
+    clb_data.msg = ssh_msg;
+    clb_data.session = session;
+
+    /* PAM conversation structure holding the callback and it's data */
+    conv.conv = nc_pam_conv_clb;
+    conv.appdata_ptr = &clb_data;
+
+    /* initialize PAM and see if the given configuration file exists */
+#ifdef LIBPAM_HAVE_CONFDIR
+    /* PAM version >= 1.4 */
+    ret = pam_start_confdir(server_opts.conf_name, session->username, &conv, server_opts.conf_dir, &pam_h);
+#else
+    /* PAM version < 1.4 */
+    ret = pam_start(server_opts.conf_name, session->username, &conv, &pam_h);
+#endif
+    if (ret != PAM_SUCCESS) {
+        ERR(NULL, "PAM error occurred (%s).\n", pam_strerror(pam_h, ret));
+        goto cleanup;
+    }
+
+    /* authentication based on the modules listed in the configuration file */
+    ret = pam_authenticate(pam_h, 0);
+    if (ret != PAM_SUCCESS) {
+        if (ret == PAM_ABORT) {
+            ERR(NULL, "PAM error occurred (%s).\n", pam_strerror(pam_h, ret));
+            goto cleanup;
+        } else {
+            VRB(NULL, "PAM error occurred (%s).\n", pam_strerror(pam_h, ret));
+            goto cleanup;
+        }
+    }
+
+    /* correct token entered, check other requirements(the time of the day, expired token, ...) */
+    ret = pam_acct_mgmt(pam_h, 0);
+    if ((ret != PAM_SUCCESS) && (ret != PAM_NEW_AUTHTOK_REQD)) {
+        VRB(NULL, "PAM error occurred (%s).\n", pam_strerror(pam_h, ret));
+        goto cleanup;
+    }
+
+    /* if a token has expired a new one will be generated */
+    if (ret == PAM_NEW_AUTHTOK_REQD) {
+        VRB(NULL, "PAM warning occurred (%s).\n", pam_strerror(pam_h, ret));
+        ret = pam_chauthtok(pam_h, PAM_CHANGE_EXPIRED_AUTHTOK);
+        if (ret == PAM_SUCCESS) {
+            VRB(NULL, "The authentication token of user \"%s\" updated successfully.", session->username);
+        } else {
+            ERR(NULL, "PAM error occurred (%s).\n", pam_strerror(pam_h, ret));
+            goto cleanup;
+        }
+    }
+
+cleanup:
+    /* destroy the PAM context */
+    if (pam_end(pam_h, ret) != PAM_SUCCESS) {
+        ERR(NULL, "PAM error occurred (%s).\n", pam_strerror(pam_h, ret));
+    }
+    return ret;
+}
+
 static void
 nc_sshcb_auth_kbdint(struct nc_session *session, ssh_message msg)
 {
     int auth_ret = 1;
-    char *pass_hash;
 
     if (server_opts.interactive_auth_clb) {
         auth_ret = server_opts.interactive_auth_clb(session, msg, server_opts.interactive_auth_data);
-    } else {
-        if (!ssh_message_auth_kbdint_is_response(msg)) {
-            const char *prompts[] = {"Password: "};
-            char echo[] = {0};
-
-            ssh_message_auth_interactive_request(msg, "Interactive SSH Authentication", "Type your password:", 1, prompts, echo);
-            auth_ret = -1;
-        } else {
-            if (ssh_userauth_kbdint_getnanswers(session->ti.libssh.session) != 1) {// failed session
-                ssh_message_reply_default(msg);
-                return;
-            }
-            pass_hash = auth_password_get_pwd_hash(session->username);// get hashed password
-            if (pass_hash) {
-                /* Normalize auth_password_compare_pwd result to 0 or 1 */
-                auth_ret = !!auth_password_compare_pwd(pass_hash, ssh_userauth_kbdint_getanswer(session->ti.libssh.session, 0));
-                free(pass_hash);// free hashed password
-            }
-        }
+    } else if (nc_pam_auth(session, msg) == PAM_SUCCESS) {
+        auth_ret = 0;
     }
 
     /* We have already sent a reply */
@@ -1451,6 +1707,7 @@
     struct nc_server_ssh_opts *opts;
     int libssh_auth_methods = 0, ret;
     struct timespec ts_timeout;
+    ssh_message msg;
 
     opts = session->data;
 
@@ -1487,7 +1744,6 @@
         return -1;
     }
 
-    ssh_set_message_callback(session->ti.libssh.session, nc_sshcb_msg, session);
     /* remember that this session was just set as nc_sshcb_msg() parameter */
     session->flags |= NC_SESSION_SSH_MSG_CB;
 
@@ -1529,10 +1785,12 @@
             return -1;
         }
 
-        if (ssh_execute_message_callbacks(session->ti.libssh.session) != SSH_OK) {
-            ERR(session, "Failed to receive SSH messages on a session (%s).",
-                    ssh_get_error(session->ti.libssh.session));
-            return -1;
+        msg = ssh_message_get(session->ti.libssh.session);
+        if (msg) {
+            if (nc_sshcb_msg(session->ti.libssh.session, msg, (void *) session)) {
+                ssh_message_reply_default(msg);
+            }
+            ssh_message_free(msg);
         }
 
         if (session->flags & NC_SESSION_SSH_AUTHENTICATED) {
@@ -1561,6 +1819,9 @@
         return 0;
     }
 
+    /* set the message callback after a successful authentication */
+    ssh_set_message_callback(session->ti.libssh.session, nc_sshcb_msg, session);
+
     /* open channel */
     ret = nc_open_netconf_channel(session, timeout);
     if (ret < 1) {