/**
 * @file io.c
 * @author Radek Krejci <rkrejci@cesnet.cz>
 * @author Michal Vasko <mvasko@cesnet.cz>
 * @brief libnetconf2 - input/output functions
 *
 * @copyright
 * Copyright (c) 2015 - 2025 CESNET, z.s.p.o.
 *
 * This source code is licensed under BSD 3-Clause License (the "License").
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://opensource.org/licenses/BSD-3-Clause
 */

#define _GNU_SOURCE /* asprintf, signals */

#include <assert.h>
#include <errno.h>
#include <grp.h>
#include <inttypes.h>
#include <limits.h>
#include <pwd.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#include <libyang/libyang.h>

#include "compat.h"
#include "config.h"
#include "log_p.h"
#include "messages_p.h"
#include "netconf.h"
#include "session.h"
#include "session_p.h"

#ifdef NC_ENABLED_SSH_TLS

#include "session_wrapper.h"

#endif /* NC_ENABLED_SSH_TLS */

const char *nc_msgtype2str[] = {
    "error",
    "would block",
    "no message",
    "hello message",
    "bad hello message",
    "RPC message",
    "rpc-reply message",
    "rpc-reply message with wrong ID",
    "notification message",
};

static ssize_t
nc_read(struct nc_session *session, char *buf, uint32_t count, uint32_t inact_timeout, struct timespec *ts_act_timeout)
{
    uint32_t readd = 0;
    ssize_t r = -1;
    int fd, interrupted;
    struct timespec ts_inact_timeout;

    assert(session);
    assert(buf);

    if ((session->status != NC_STATUS_RUNNING) && (session->status != NC_STATUS_STARTING)) {
        return -1;
    }

    if (!count) {
        return 0;
    }

    nc_timeouttime_get(&ts_inact_timeout, inact_timeout);
    do {
        interrupted = 0;
        switch (session->ti_type) {
        case NC_TI_NONE:
            return 0;

        case NC_TI_FD:
        case NC_TI_UNIX:
            fd = (session->ti_type == NC_TI_FD) ? session->ti.fd.in : session->ti.unixsock.sock;
            /* read via standard file descriptor */
            r = read(fd, buf + readd, count - readd);
            if (r < 0) {
                if (errno == EAGAIN) {
                    r = 0;
                    break;
                } else if (errno == EINTR) {
                    r = 0;
                    interrupted = 1;
                    break;
                } else {
                    ERR(session, "Reading from file descriptor (%d) failed (%s).", fd, strerror(errno));
                    session->status = NC_STATUS_INVALID;
                    session->term_reason = NC_SESSION_TERM_OTHER;
                    return -1;
                }
            } else if (r == 0) {
                ERR(session, "Communication file descriptor (%d) unexpectedly closed.", fd);
                session->status = NC_STATUS_INVALID;
                session->term_reason = NC_SESSION_TERM_DROPPED;
                return -1;
            }
            break;

#ifdef NC_ENABLED_SSH_TLS
        case NC_TI_SSH:
            /* read via libssh */
            r = ssh_channel_read(session->ti.libssh.channel, buf + readd, count - readd, 0);
            if (r == SSH_AGAIN) {
                r = 0;
                break;
            } else if (r == SSH_ERROR) {
                ERR(session, "Reading from the SSH channel failed (%s).", ssh_get_error(session->ti.libssh.session));
                session->status = NC_STATUS_INVALID;
                session->term_reason = NC_SESSION_TERM_OTHER;
                return -1;
            } else if (r == 0) {
                if (ssh_channel_is_eof(session->ti.libssh.channel)) {
                    ERR(session, "SSH channel unexpected EOF.");
                    session->status = NC_STATUS_INVALID;
                    session->term_reason = NC_SESSION_TERM_DROPPED;
                    return -1;
                }
                break;
            }
            break;

        case NC_TI_TLS:
            r = nc_tls_read_wrap(session, (unsigned char *)buf + readd, count - readd);
            if (r < 0) {
                /* non-recoverable error */
                return r;
            }
            break;
#endif /* NC_ENABLED_SSH_TLS */
        }

        if (r == 0) {
            /* nothing read */
            if (!interrupted) {
                usleep(NC_TIMEOUT_STEP);
            }
            if ((nc_timeouttime_cur_diff(&ts_inact_timeout) < 1) || (nc_timeouttime_cur_diff(ts_act_timeout) < 1)) {
                if (nc_timeouttime_cur_diff(&ts_inact_timeout) < 1) {
                    ERR(session, "Inactive read timeout elapsed.");
                } else {
                    ERR(session, "Active read timeout elapsed.");
                }
                session->status = NC_STATUS_INVALID;
                session->term_reason = NC_SESSION_TERM_OTHER;
                return -1;
            }
        } else {
            /* something read */
            readd += r;

            /* reset inactive timeout */
            nc_timeouttime_get(&ts_inact_timeout, inact_timeout);
        }

    } while (readd < count);
    buf[count] = '\0';

    return (ssize_t)readd;
}

/**
 * @brief Read data until a specific characters sequence is found.
 *
 * @param[in] session Session to read from.
 * @param[in] endtag End tag to match, no characters read after that.
 * @param[in] inact_timeout Inactive timeout to use.
 * @param[in] ts_act_timeout Active timeout to use.
 * @param[in,out] buf Buffer for read data, may be NULL.
 * @param[in,out] buf_size Current size of @p buf - 1 (actual size is one byte larger).
 * @return Number of characters read;
 * @return -1 on error.
 */
static int
nc_read_until(struct nc_session *session, const char *endtag, uint32_t inact_timeout, struct timespec *ts_act_timeout,
        char **buf, uint32_t *buf_size)
{
    char *local_buf = NULL;
    ssize_t r;
    uint32_t len, i, matched = 0, count = 0, local_size = 0;

    assert(session);
    assert(endtag);

    if (!buf) {
        buf = &local_buf;
        buf_size = &local_size;
    }

    len = strlen(endtag);
    do {
        /* resize buffer if needed */
        if ((count + (len - matched)) > *buf_size) {
            *buf_size += NC_READ_BUF_SIZE_STEP;
            *buf = nc_realloc(*buf, (*buf_size + 1) * sizeof **buf);
            NC_CHECK_ERRMEM_GOTO(!*buf, count = -1, cleanup);
        }

        /* get another character */
        r = nc_read(session, *buf + count, len - matched, inact_timeout, ts_act_timeout);
        if (r != len - matched) {
            count = -1;
            goto cleanup;
        }

        count += len - matched;

        for (i = len - matched; i > 0; i--) {
            if (!strncmp(endtag + matched, *buf + (count - i), i)) {
                /*part of endtag found */
                matched += i;
                break;
            } else {
                matched = 0;
            }
        }

        /* whole endtag found */
    } while (matched < len);

    /* terminating null byte */
    (*buf)[count] = 0;

cleanup:
    free(local_buf);
    return count;
}

int
nc_read_msg_io(struct nc_session *session, int io_timeout, int passing_io_lock, char **buf, uint32_t *buf_len)
{
    int ret = -1, r, io_locked = 0;
    char *frame_size_buf = NULL;
    uint32_t inact_timeout, frame_buf_len, chunk_len, buf_used = 0;
    struct timespec ts_act_timeout;

    assert(session && buf && buf_len);

    /* use timeout in milliseconds instead seconds */
    inact_timeout = NC_READ_INACT_TIMEOUT * 1000;

    if ((session->status != NC_STATUS_RUNNING) && (session->status != NC_STATUS_STARTING)) {
        ERR(session, "Invalid session to read from.");
        ret = -1;
        goto cleanup;
    }

    nc_timeouttime_get(&ts_act_timeout, NC_READ_ACT_TIMEOUT * 1000);

    if (!passing_io_lock) {
        /* SESSION IO LOCK */
        ret = nc_mutex_lock(session->io_lock, io_timeout, __func__);
        if (ret < 1) {
            goto cleanup;
        }
        io_locked = 1;
    }

    /* read the message */
    switch (session->version) {
    case NC_PROT_VERSION_10:
        r = nc_read_until(session, NC_VERSION_10_ENDTAG, inact_timeout, &ts_act_timeout, buf, buf_len);
        if (r == -1) {
            ret = r;
            goto cleanup;
        }

        /* cut off the end tag */
        ret = r - NC_VERSION_10_ENDTAG_LEN;
        (*buf)[ret] = '\0';
        break;
    case NC_PROT_VERSION_11:
        /* prepare the buffer large enough for reading the framing chunk size */
        frame_buf_len = 11;
        frame_size_buf = malloc(frame_buf_len + 1);
        NC_CHECK_ERRMEM_GOTO(!frame_size_buf, ret = -1, cleanup);

        while (1) {
            r = nc_read_until(session, "\n#", inact_timeout, &ts_act_timeout, NULL, NULL);
            if (r == -1) {
                ret = r;
                goto cleanup;
            }
            r = nc_read_until(session, "\n", inact_timeout, &ts_act_timeout, &frame_size_buf, &frame_buf_len);
            if (r == -1) {
                ret = r;
                goto cleanup;
            }

            if (!strcmp(frame_size_buf, "#\n")) {
                /* end of chunked framing message */
                if (!buf_used) {
                    ERR(session, "Invalid frame chunk delimiters.");
                    ret = -2;
                    goto cleanup;
                }
                break;
            }

            /* convert string to the size of the following chunk */
            chunk_len = strtoul(frame_size_buf, NULL, 10);
            if (!chunk_len) {
                ERR(session, "Invalid frame chunk size, fatal error.");
                ret = -2;
                goto cleanup;
            } else if (chunk_len > NC_READ_CHUNK_SIZE_MAX) {
                ERR(session, "Maximum frame chunk size %" PRIu32 " B exceeded.", (uint32_t)NC_READ_CHUNK_SIZE_MAX);
                ret = -2;
                goto cleanup;
            }

            /* now we have size of next chunk, prepare a buffer large enough */
            if (UINT32_MAX - buf_used < chunk_len + 1) {
                ERR(session, "Maximum supported message size %" PRIu32 " B exceeded.", UINT32_MAX);
                ret = -2;
                goto cleanup;
            } else if (*buf_len < buf_used + chunk_len + 1) {
                *buf_len = buf_used + chunk_len + 1;
                *buf = nc_realloc(*buf, *buf_len);
                NC_CHECK_ERRMEM_GOTO(!*buf, ret = -1, cleanup);
            }

            /* read the next chunk */
            r = nc_read(session, *buf + buf_used, chunk_len, inact_timeout, &ts_act_timeout);
            if (r == -1) {
                ret = r;
                goto cleanup;
            }

            /* update data length and terminate the data */
            buf_used += chunk_len;
            (*buf)[buf_used] = '\0';
        }

        ret = buf_used;
        break;
    }

    DBG(session, "Received message:\n%s\n", *buf);

cleanup:
    if (io_locked) {
        /* SESSION IO UNLOCK */
        nc_mutex_unlock(session->io_lock, __func__);
    }
    free(frame_size_buf);
    return ret;
}

/* return -1 means either poll error or that session was invalidated (socket error), EINTR is handled inside */
static int
nc_read_poll(struct nc_session *session, int io_timeout)
{
    int ret = -2;
    struct pollfd fds;

    if ((session->status != NC_STATUS_RUNNING) && (session->status != NC_STATUS_STARTING)) {
        ERR(session, "Invalid session to poll.");
        return -1;
    }

    switch (session->ti_type) {
#ifdef NC_ENABLED_SSH_TLS
    case NC_TI_SSH:
        if (io_timeout == -1) {
            /* BUG libssh 0.11.0 replaces timeout -1 with 0 for non-blocking sessions */
            io_timeout = INT_MAX;
        }

        /* EINTR is handled, it resumes waiting */
        ret = ssh_channel_poll_timeout(session->ti.libssh.channel, io_timeout, 0);
        if (ret == SSH_ERROR) {
            ERR(session, "SSH channel poll error (%s).", ssh_get_error(session->ti.libssh.session));
            session->status = NC_STATUS_INVALID;
            session->term_reason = NC_SESSION_TERM_OTHER;
            return -1;
        } else if (ret == SSH_EOF) {
            ERR(session, "SSH channel unexpected EOF.");
            session->status = NC_STATUS_INVALID;
            session->term_reason = NC_SESSION_TERM_DROPPED;
            return -1;
        } else if (ret > 0) {
            /* fake it */
            ret = 1;
            fds.revents = POLLIN;
        } else { /* ret == 0 */
            fds.revents = 0;
        }
        break;
    case NC_TI_TLS:
        ret = nc_tls_get_num_pending_bytes_wrap(session->ti.tls.session);
        if (ret) {
            /* some buffered TLS data available */
            ret = 1;
            fds.revents = POLLIN;
            break;
        }

        fds.fd = nc_tls_get_fd_wrap(session);
#endif /* NC_ENABLED_SSH_TLS */
    /* fallthrough */
    case NC_TI_FD:
    case NC_TI_UNIX:
        if (session->ti_type == NC_TI_FD) {
            fds.fd = session->ti.fd.in;
        } else if (session->ti_type == NC_TI_UNIX) {
            fds.fd = session->ti.unixsock.sock;
        }

        fds.events = POLLIN;
        fds.revents = 0;

        ret = nc_poll(&fds, 1, io_timeout);
        break;

    default:
        ERRINT;
        return -1;
    }

    /* process the poll result, unified ret meaning for poll and ssh_channel poll */
    if (ret < 0) {
        /* poll failed - something really bad happened, close the session */
        ERR(session, "poll error (%s).", strerror(errno));
        session->status = NC_STATUS_INVALID;
        session->term_reason = NC_SESSION_TERM_OTHER;
        return -1;
    } else {
        /* in case of standard (non-libssh) poll, there still can be an error */
        if (fds.revents & POLLERR) {
            ERR(session, "Communication channel error.");
            session->status = NC_STATUS_INVALID;
            session->term_reason = NC_SESSION_TERM_OTHER;
            return -1;
        }
        /* Some poll() implementations may return POLLHUP|POLLIN when the other
         * side has closed but there is data left to read in the buffer. */
        if ((fds.revents & POLLHUP) && !(fds.revents & POLLIN)) {
            ERR(session, "Communication channel unexpectedly closed.");
            session->status = NC_STATUS_INVALID;
            session->term_reason = NC_SESSION_TERM_DROPPED;
            return -1;
        }
    }

    return ret;
}

int
nc_read_msg_poll_io(struct nc_session *session, int io_timeout, struct ly_in **msg)
{
    int ret;
    uint32_t buf_len = 0;
    char *buf = NULL;

    assert(msg);
    *msg = NULL;

    if ((session->status != NC_STATUS_RUNNING) && (session->status != NC_STATUS_STARTING)) {
        ERR(session, "Invalid session to read from.");
        return -1;
    }

    /* SESSION IO LOCK */
    ret = nc_mutex_lock(session->io_lock, io_timeout, __func__);
    if (ret < 1) {
        return ret;
    }

    ret = nc_read_poll(session, io_timeout);
    if (ret < 1) {
        /* timed out or error */

        /* SESSION IO UNLOCK */
        nc_mutex_unlock(session->io_lock, __func__);
        return ret;
    }

    /* read msg */
    ret = nc_read_msg_io(session, io_timeout, 1, &buf, &buf_len);

    /* SESSION IO UNLOCK */
    nc_mutex_unlock(session->io_lock, __func__);

    if (ret > 0) {
        ret = 1;
    } else {
        free(buf);
        return ret;
    }

    /* create input */
    if (ly_in_new_memory(buf, msg)) {
        free(buf);
        return -1;
    }

    return ret;
}

/* does not really log, only fatal errors */
int
nc_session_is_connected(const struct nc_session *session)
{
    int ret;
    struct pollfd fds;

    switch (session->ti_type) {
    case NC_TI_FD:
        fds.fd = session->ti.fd.in;
        break;
    case NC_TI_UNIX:
        fds.fd = session->ti.unixsock.sock;
        break;
#ifdef NC_ENABLED_SSH_TLS
    case NC_TI_SSH:
        fds.fd = ssh_get_fd(session->ti.libssh.session);
        break;
    case NC_TI_TLS:
        fds.fd = nc_tls_get_fd_wrap(session);
        break;
#endif /* NC_ENABLED_SSH_TLS */
    default:
        return 0;
    }

    if (fds.fd == -1) {
        return 0;
    }

    fds.events = POLLIN;
    fds.revents = 0;

    ret = nc_poll(&fds, 1, 0);
    if (ret == -1) {
        return 0;
    } else if ((ret > 0) && (fds.revents & (POLLHUP | POLLERR))) {
        return 0;
    }

    return 1;
}

/**
 * @brief Write to a NETCONF session.
 *
 * @param[in] session Session to write to.
 * @param[in] buf Buffer to write.
 * @param[in] count Count of bytes from @p buf to write.
 * @return Number of bytes written.
 * @return -1 on error.
 */
static int
nc_write(struct nc_session *session, const void *buf, uint32_t count)
{
    int c, fd, interrupted;
    uint32_t written = 0;

    if ((session->status != NC_STATUS_RUNNING) && (session->status != NC_STATUS_STARTING)) {
        return -1;
    }

    /* prevent SIGPIPE this way */
    if (!nc_session_is_connected(session)) {
        ERR(session, "Communication socket unexpectedly closed.");
        session->status = NC_STATUS_INVALID;
        session->term_reason = NC_SESSION_TERM_DROPPED;
        return -1;
    }

    DBG(session, "Sending message:\n%.*s\n", (int)count, (char *)buf);

    do {
        interrupted = 0;
        switch (session->ti_type) {
        case NC_TI_FD:
        case NC_TI_UNIX:
            fd = session->ti_type == NC_TI_FD ? session->ti.fd.out : session->ti.unixsock.sock;
            c = write(fd, (char *)buf + written, count - written);
            if ((c < 0) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) {
                c = 0;
            } else if ((c < 0) && (errno == EINTR)) {
                c = 0;
                interrupted = 1;
            } else if (c < 0) {
                ERR(session, "Socket error (%s).", strerror(errno));
                return -1;
            }
            break;

#ifdef NC_ENABLED_SSH_TLS
        case NC_TI_SSH:
            if (ssh_channel_is_closed(session->ti.libssh.channel)) {
                ERR(session, "SSH channel unexpectedly closed.");
                session->status = NC_STATUS_INVALID;
                session->term_reason = NC_SESSION_TERM_DROPPED;
                return -1;
            }
            c = ssh_channel_write(session->ti.libssh.channel, (char *)buf + written, count - written);
            if ((c == SSH_ERROR) || (c == -1)) {
                ERR(session, "SSH channel write failed.");
                return -1;
            }
            break;
        case NC_TI_TLS:
            c = nc_tls_write_wrap(session, (const unsigned char *)buf + written, count - written);
            if (c < 0) {
                /* possible client dc, or some socket/TLS communication error */
                return -1;
            }
            break;
#endif /* NC_ENABLED_SSH_TLS */
        default:
            ERRINT;
            return -1;
        }

        if ((c == 0) && !interrupted) {
            /* we must wait */
            usleep(NC_TIMEOUT_STEP);
        }

        written += c;
    } while (written < count);

    return written;
}

/**
 * @brief Write the start tag and the message part of a chunked-framing NETCONF message.
 *
 * @param[in] session Session to write to.
 * @param[in] buf Message buffer to write.
 * @param[in] count Count of bytes from @p buf to write.
 * @return Number of bytes written.
 * @return -1 on error.
 */
static int
nc_write_starttag_and_msg(struct nc_session *session, const void *buf, uint32_t count)
{
    int ret = 0, r;
    char chunksize[24];

    if (session->version == NC_PROT_VERSION_11) {
        r = sprintf(chunksize, "\n#%" PRIu32 "\n", count);

        r = nc_write(session, chunksize, r);
        if (r < 0) {
            return -1;
        }
        ret += r;
    }

    r = nc_write(session, buf, count);
    if (r < 0) {
        return -1;
    }
    ret += r;

    return ret;
}

/**
 * @brief Write the end tag part of a chunked-framing NETCONF message.
 *
 * @param[in] session Session to write to.
 * @return Number of bytes written.
 * @return -1 on error.
 */
static int
nc_write_endtag(struct nc_session *session)
{
    int ret;

    if (session->version == NC_PROT_VERSION_11) {
        ret = nc_write(session, "\n##\n", 4);
    } else {
        ret = nc_write(session, "]]>]]>", 6);
    }

    return ret;
}

/**
 * @brief Flush all the data buffered for writing.
 *
 * @param[in] warg Write callback structure to flush.
 * @return Number of written bytes.
 * @return -1 on error.
 */
static int
nc_write_clb_flush(struct nc_wclb_arg *warg)
{
    int ret = 0;

    /* flush current buffer */
    if (warg->len) {
        ret = nc_write_starttag_and_msg(warg->session, warg->buf, warg->len);
        warg->len = 0;
    }

    return ret;
}

int
nc_write_clb(struct nc_wclb_arg *arg, const void *buf, uint32_t count, int xmlcontent)
{
    int ret = 0, c;
    uint32_t l;

    if (!buf) {
        c = nc_write_clb_flush(arg);
        if (c == -1) {
            return -1;
        }
        ret += c;

        /* endtag */
        c = nc_write_endtag(arg->session);
        if (c == -1) {
            return -1;
        }
        ret += c;

        return ret;
    }

    if (arg->len && (arg->len + count > NC_WRITE_CHUNK_SIZE_MAX)) {
        /* dump current buffer */
        c = nc_write_clb_flush(arg);
        if (c == -1) {
            return -1;
        }
        ret += c;
    }

    if (!xmlcontent && (count > NC_WRITE_CHUNK_SIZE_MAX)) {
        /* write directly */
        c = nc_write_starttag_and_msg(arg->session, buf, count);
        if (c == -1) {
            return -1;
        }
        ret += c;
    } else {
        /* keep in buffer and write later */
        if (xmlcontent) {
            for (l = 0; l < count; l++) {
                if (arg->len + 5 >= NC_WRITE_CHUNK_SIZE_MAX) {
                    /* buffer is full */
                    c = nc_write_clb_flush(arg);
                    if (c == -1) {
                        return -1;
                    }
                }

                switch (((char *)buf)[l]) {
                case '&':
                    ret += 5;
                    memcpy(&arg->buf[arg->len], "&amp;", 5);
                    arg->len += 5;
                    break;
                case '<':
                    ret += 4;
                    memcpy(&arg->buf[arg->len], "&lt;", 4);
                    arg->len += 4;
                    break;
                case '>':
                    /* not needed, just for readability */
                    ret += 4;
                    memcpy(&arg->buf[arg->len], "&gt;", 4);
                    arg->len += 4;
                    break;
                default:
                    ret++;
                    memcpy(&arg->buf[arg->len], &((char *)buf)[l], 1);
                    arg->len++;
                }
            }
        } else {
            memcpy(&arg->buf[arg->len], buf, count);
            arg->len += count; /* is <= NC_WRITE_CHUNK_SIZE_MAX */
            ret += count;
        }
    }

    return ret;
}

/**
 * @brief Write print callback used by libyang.
 */
static ssize_t
nc_write_xmlclb(void *arg, const void *buf, size_t count)
{
    ssize_t r;

    r = nc_write_clb(arg, buf, count, 0);
    if (r == -1) {
        return -1;
    }

    /* always return what libyang expects, simply that all the characters were printed */
    return count;
}

/* return NC_MSG_ERROR can change session status, acquires IO lock as needed */
NC_MSG_TYPE
nc_write_msg_io(struct nc_session *session, int io_timeout, int type, ...)
{
    va_list ap;
    int count, ret;
    const char *attrs, *str;
    struct lyd_node *op, *reply_envp, *node, *next;
    struct lyd_node_opaq *rpc_envp;
    struct nc_server_notif *notif;
    struct nc_server_reply *reply;
    char *buf;
    struct nc_wclb_arg arg;
    const char **capabilities;
    uint32_t *sid = NULL, i, wd = 0, str_len;
    LY_ERR lyrc;

    assert(session);

    if ((session->status != NC_STATUS_RUNNING) && (session->status != NC_STATUS_STARTING)) {
        ERR(session, "Invalid session to write to.");
        return NC_MSG_ERROR;
    }

    arg.session = session;
    arg.len = 0;

    /* SESSION IO LOCK */
    ret = nc_mutex_lock(session->io_lock, io_timeout, __func__);
    if (ret < 0) {
        return NC_MSG_ERROR;
    } else if (!ret) {
        return NC_MSG_WOULDBLOCK;
    }

    va_start(ap, type);

    switch (type) {
    case NC_MSG_RPC:
        op = va_arg(ap, struct lyd_node *);
        attrs = va_arg(ap, const char *);

        if (session->ctx != LYD_CTX(op)) {
            ERR(session, "RPC \"%s\" was created in different context than that of the session.", LYD_NAME(op));
            ret = NC_MSG_ERROR;
            goto cleanup;
        }

        /* <rpc> open */
        count = asprintf(&buf, "<rpc xmlns=\"%s\" message-id=\"%" PRIu64 "\"%s>",
                NC_NS_BASE, session->opts.client.msgid + 1, attrs ? attrs : "");
        NC_CHECK_ERRMEM_GOTO(count == -1, ret = NC_MSG_ERROR, cleanup);
        nc_write_clb((void *)&arg, buf, count, 0);
        free(buf);

        if (op->schema && (op->schema->nodetype & (LYS_CONTAINER | LYS_LIST))) {
            /* <action> open */
            str = "<action xmlns=\"urn:ietf:params:xml:ns:yang:1\">";
            nc_write_clb((void *)&arg, str, strlen(str), 0);
        }

        /* rpc data */
        if (lyd_print_clb(nc_write_xmlclb, (void *)&arg, op, LYD_XML, LYD_PRINT_SHRINK | LYD_PRINT_EMPTY_CONT)) {
            ret = NC_MSG_ERROR;
            goto cleanup;
        }

        if (op->schema && (op->schema->nodetype & (LYS_CONTAINER | LYS_LIST))) {
            /* <action> close */
            str = "</action>";
            nc_write_clb((void *)&arg, str, strlen(str), 0);
        }

        /* <rpc> close */
        str = "</rpc>";
        nc_write_clb((void *)&arg, str, strlen(str), 0);

        session->opts.client.msgid++;
        break;

    case NC_MSG_REPLY:
        rpc_envp = va_arg(ap, struct lyd_node_opaq *);
        reply = va_arg(ap, struct nc_server_reply *);

        /* build a rpc-reply opaque node that can be simply printed */
        if (rpc_envp) {
            if (lyd_new_opaq2(NULL, session->ctx, "rpc-reply", NULL, rpc_envp->name.prefix, rpc_envp->name.module_ns,
                    &reply_envp)) {
                ERRINT;
                ret = NC_MSG_ERROR;
                goto cleanup;
            }
        } else {
            if (lyd_new_opaq2(NULL, session->ctx, "rpc-reply", NULL, NULL, NC_NS_BASE,
                    &reply_envp)) {
                ERRINT;
                ret = NC_MSG_ERROR;
                goto cleanup;
            }
        }

        switch (reply->type) {
        case NC_RPL_OK:
            assert(rpc_envp != NULL);
            if (lyd_new_opaq2(reply_envp, NULL, "ok", NULL, rpc_envp->name.prefix, rpc_envp->name.module_ns, NULL)) {
                lyd_free_tree(reply_envp);

                ERRINT;
                ret = NC_MSG_ERROR;
                goto cleanup;
            }
            break;
        case NC_RPL_DATA:
            switch (((struct nc_server_reply_data *)reply)->wd) {
            case NC_WD_UNKNOWN:
            case NC_WD_EXPLICIT:
                wd = LYD_PRINT_WD_EXPLICIT;
                break;
            case NC_WD_TRIM:
                wd = LYD_PRINT_WD_TRIM;
                break;
            case NC_WD_ALL:
                wd = LYD_PRINT_WD_ALL;
                break;
            case NC_WD_ALL_TAG:
                wd = LYD_PRINT_WD_ALL_TAG;
                break;
            }

            node = ((struct nc_server_reply_data *)reply)->data;
            assert(node->schema->nodetype & (LYS_RPC | LYS_ACTION));
            LY_LIST_FOR_SAFE(lyd_child(node), next, node) {
                /* temporary */
                lyd_insert_child(reply_envp, node);
            }
            break;
        case NC_RPL_ERROR:
            /* temporary */
            lyd_insert_child(reply_envp, ((struct nc_server_reply_error *)reply)->err);
            break;
        default:
            ERRINT;
            nc_write_clb((void *)&arg, NULL, 0, 0);
            ret = NC_MSG_ERROR;
            goto cleanup;
        }

        /* temporary */
        if (rpc_envp) {
            ((struct lyd_node_opaq *)reply_envp)->attr = rpc_envp->attr;
        }

        /* print */
        lyrc = lyd_print_clb(nc_write_xmlclb, (void *)&arg, reply_envp, LYD_XML, LYD_PRINT_SHRINK | wd);
        ((struct lyd_node_opaq *)reply_envp)->attr = NULL;

        /* cleanup */
        switch (reply->type) {
        case NC_RPL_OK:
            /* just free everything */
            lyd_free_tree(reply_envp);
            break;
        case NC_RPL_DATA:
            LY_LIST_FOR_SAFE(lyd_child(reply_envp), next, node) {
                /* connect back to the reply structure */
                lyd_insert_child(((struct nc_server_reply_data *)reply)->data, node);
            }
            lyd_free_tree(reply_envp);
            break;
        case NC_RPL_ERROR:
            /* unlink from the data reply */
            lyd_unlink_tree(lyd_child(reply_envp));
            lyd_free_tree(reply_envp);
            break;
        default:
            break;
        }

        if (lyrc) {
            ret = NC_MSG_ERROR;
            goto cleanup;
        }
        break;

    case NC_MSG_NOTIF:
        notif = va_arg(ap, struct nc_server_notif *);

        nc_write_clb((void *)&arg, "<notification xmlns=\""NC_NS_NOTIF "\">", 21 + 47 + 2, 0);
        nc_write_clb((void *)&arg, "<eventTime>", 11, 0);
        nc_write_clb((void *)&arg, notif->eventtime, strlen(notif->eventtime), 0);
        nc_write_clb((void *)&arg, "</eventTime>", 12, 0);
        if (lyd_print_clb(nc_write_xmlclb, (void *)&arg, notif->ntf, LYD_XML, LYD_PRINT_SHRINK)) {
            ret = NC_MSG_ERROR;
            goto cleanup;
        }
        nc_write_clb((void *)&arg, "</notification>", 15, 0);
        break;

    case NC_MSG_HELLO:
        if (session->version != NC_PROT_VERSION_10) {
            ret = NC_MSG_ERROR;
            goto cleanup;
        }
        capabilities = va_arg(ap, const char **);
        sid = va_arg(ap, uint32_t *);

        count = asprintf(&buf, "<hello xmlns=\"%s\"><capabilities>", NC_NS_BASE);
        NC_CHECK_ERRMEM_GOTO(count == -1, ret = NC_MSG_ERROR, cleanup);
        nc_write_clb((void *)&arg, buf, count, 0);
        free(buf);
        for (i = 0; capabilities[i]; i++) {
            nc_write_clb((void *)&arg, "<capability>", 12, 0);
            nc_write_clb((void *)&arg, capabilities[i], strlen(capabilities[i]), 1);
            nc_write_clb((void *)&arg, "</capability>", 13, 0);
        }
        if (sid) {
            count = asprintf(&buf, "</capabilities><session-id>%" PRIu32 "</session-id></hello>", *sid);
            NC_CHECK_ERRMEM_GOTO(count == -1, ret = NC_MSG_ERROR, cleanup);
            nc_write_clb((void *)&arg, buf, count, 0);
            free(buf);
        } else {
            nc_write_clb((void *)&arg, "</capabilities></hello>", 23, 0);
        }
        break;

    case NC_MSG_RAW:
        str = va_arg(ap, const char *);
        str_len = va_arg(ap, uint32_t);

        /* write the message */
        nc_write_clb((void *)&arg, str, str_len, 0);

        session->opts.client.msgid++;
        break;

    default:
        ret = NC_MSG_ERROR;
        goto cleanup;
    }

    /* flush message */
    nc_write_clb((void *)&arg, NULL, 0, 0);

    if ((session->status != NC_STATUS_RUNNING) && (session->status != NC_STATUS_STARTING)) {
        /* error was already written */
        ret = NC_MSG_ERROR;
    } else {
        /* specific message successfully sent */
        ret = type;
    }

cleanup:
    va_end(ap);
    nc_mutex_unlock(session->io_lock, __func__);
    return ret;
}

void *
nc_realloc(void *ptr, size_t size)
{
    void *ret;

    ret = realloc(ptr, size);
    if (!ret) {
        free(ptr);
    }

    return ret;
}

struct passwd *
nc_getpw(uid_t uid, const char *username, struct passwd *pwd_buf, char **buf, size_t *buf_size)
{
    struct passwd *pwd = NULL;
    long sys_size;
    int ret;

    do {
        if (!*buf_size) {
            /* learn suitable buffer size */
            sys_size = sysconf(_SC_GETPW_R_SIZE_MAX);
            *buf_size = (sys_size == -1) ? 2048 : sys_size;
        } else {
            /* enlarge buffer */
            *buf_size += 2048;
        }

        /* allocate some buffer */
        *buf = nc_realloc(*buf, *buf_size);
        NC_CHECK_ERRMEM_RET(!*buf, NULL);

        if (username) {
            ret = getpwnam_r(username, pwd_buf, *buf, *buf_size, &pwd);
        } else {
            ret = getpwuid_r(uid, pwd_buf, *buf, *buf_size, &pwd);
        }
    } while (ret && (ret == ERANGE));

    if (ret || !pwd) {
        if (username) {
            ERR(NULL, "Retrieving username \"%s\" passwd entry failed (%s).", username,
                    ret ? strerror(ret) : "user not found");
        } else {
            ERR(NULL, "Retrieving UID \"%lu\" passwd entry failed (%s).", (unsigned long)uid,
                    ret ? strerror(ret) : "uid not found");
        }
    }
    return pwd;
}

struct group *
nc_getgr(gid_t gid, const char *grpname, struct group *grp_buf, char **buf, size_t *buf_size)
{
    struct group *grp = NULL;
    long sys_size;
    int ret;

    do {
        if (!*buf_size) {
            /* learn suitable buffer size */
            sys_size = sysconf(_SC_GETGR_R_SIZE_MAX);
            *buf_size = (sys_size == -1) ? 2048 : sys_size;
        } else {
            /* enlarge buffer */
            *buf_size += 2048;
        }

        /* allocate some buffer */
        *buf = nc_realloc(*buf, *buf_size);
        NC_CHECK_ERRMEM_RET(!*buf, NULL);

        if (grpname) {
            ret = getgrnam_r(grpname, grp_buf, *buf, *buf_size, &grp);
        } else {
            ret = getgrgid_r(gid, grp_buf, *buf, *buf_size, &grp);
        }
    } while (ret && (ret == ERANGE));

    if (ret || !grp) {
        if (grpname) {
            ERR(NULL, "Retrieving group \"%s\" group entry failed (%s).", grpname,
                    ret ? strerror(ret) : "group not found");
        } else {
            ERR(NULL, "Retrieving GID \"%lu\" group entry failed (%s).", (unsigned long)gid,
                    ret ? strerror(ret) : "gid not found");
        }
    }
    return grp;
}
